├── .cargo └── audit.toml ├── .devcontainer ├── Dockerfile ├── devcontainer.json └── token-haver │ └── devcontainer.json ├── .github └── workflows │ ├── audit-sec3.yml │ ├── cargo-audit.yaml │ └── lint-test.yaml ├── .gitignore ├── .npmignore ├── .vscode └── settings.json ├── Anchor.toml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── migrations └── deploy.ts ├── package.json ├── programs ├── bonk-plugin │ ├── Cargo.toml │ ├── Xargo.toml │ ├── idls │ │ └── spl_token_staking.json │ ├── src │ │ ├── error.rs │ │ ├── instructions │ │ │ ├── create_registrar.rs │ │ │ ├── create_voter_weight_record.rs │ │ │ ├── mod.rs │ │ │ └── update_voter_weight_record.rs │ │ ├── lib.rs │ │ ├── state │ │ │ ├── mod.rs │ │ │ ├── registrar.rs │ │ │ ├── stake_deposit_record.rs │ │ │ └── voter_weight_record.rs │ │ └── utils │ │ │ ├── anchor.rs │ │ │ ├── mod.rs │ │ │ ├── stake_deposit_receipt.rs │ │ │ └── stake_pool.rs │ └── tests │ │ ├── create_registrar.rs │ │ ├── create_voter_weight_record.rs │ │ ├── fixtures │ │ ├── spl_governance.so │ │ └── spl_token_staking.so │ │ ├── program_test │ │ ├── bonk_plugin_test.rs │ │ ├── governance_test.rs │ │ ├── mod.rs │ │ ├── program_test_bench.rs │ │ ├── spl_token_staking_test.rs │ │ └── tools.rs │ │ └── update_voter_weight_record.rs ├── gateway │ ├── Cargo.toml │ ├── Xargo.toml │ ├── src │ │ ├── error.rs │ │ ├── instructions │ │ │ ├── configure_registrar.rs │ │ │ ├── create_registrar.rs │ │ │ ├── create_voter_weight_record.rs │ │ │ ├── mod.rs │ │ │ └── update_voter_weight_record.rs │ │ ├── lib.rs │ │ ├── state │ │ │ ├── generic_voter_weight.rs │ │ │ ├── mod.rs │ │ │ ├── registrar.rs │ │ │ ├── token_owner_record.rs │ │ │ └── voter_weight_record.rs │ │ └── tools │ │ │ ├── anchor.rs │ │ │ └── mod.rs │ └── tests │ │ ├── configure_registrar.rs │ │ ├── create_registrar.rs │ │ ├── create_voter_weight_record.rs │ │ ├── fixtures │ │ ├── solana_gateway.so │ │ ├── spl_governance.so │ │ └── spl_governance_addin_mock.so │ │ ├── program_test │ │ ├── gateway_voter_test.rs │ │ ├── governance_test.rs │ │ ├── mod.rs │ │ ├── predecessor_plugin_test.rs │ │ ├── program_test_bench.rs │ │ └── tools.rs │ │ ├── update_voter_weight_record_predecessor_plugin.rs │ │ └── update_voter_weight_record_token_deposit.rs ├── nft-voter │ ├── Cargo.toml │ ├── Xargo.toml │ ├── src │ │ ├── error.rs │ │ ├── instructions │ │ │ ├── cast_nft_vote.rs │ │ │ ├── configure_collection.rs │ │ │ ├── create_max_voter_weight_record.rs │ │ │ ├── create_registrar.rs │ │ │ ├── create_voter_weight_record.rs │ │ │ ├── mod.rs │ │ │ ├── relinquish_nft_vote.rs │ │ │ └── update_voter_weight_record.rs │ │ ├── lib.rs │ │ ├── state │ │ │ ├── collection_config.rs │ │ │ ├── idl_types.rs │ │ │ ├── max_voter_weight_record.rs │ │ │ ├── mod.rs │ │ │ ├── nft_vote_record.rs │ │ │ ├── registrar.rs │ │ │ └── voter_weight_record.rs │ │ └── tools │ │ │ ├── anchor.rs │ │ │ ├── governance.rs │ │ │ ├── mod.rs │ │ │ ├── spl_token.rs │ │ │ └── token_metadata.rs │ └── tests │ │ ├── cast_nft_vote.rs │ │ ├── configure_collection.rs │ │ ├── create_max_voter_weight_record.rs │ │ ├── create_registrar.rs │ │ ├── create_voter_weight_record.rs │ │ ├── fixtures │ │ ├── mpl_token_metadata.so │ │ └── spl_governance.so │ │ ├── program_test │ │ ├── governance_test.rs │ │ ├── mod.rs │ │ ├── nft_voter_test.rs │ │ ├── program_test_bench.rs │ │ ├── token_metadata_test.rs │ │ └── tools.rs │ │ ├── relinquish_nft_vote.rs │ │ └── update_voter_weight_record.rs ├── quadratic │ ├── Cargo.toml │ ├── Xargo.toml │ ├── src │ │ ├── error.rs │ │ ├── instructions │ │ │ ├── configure_registrar.rs │ │ │ ├── create_registrar.rs │ │ │ ├── create_voter_weight_record.rs │ │ │ ├── mod.rs │ │ │ └── update_voter_weight_record.rs │ │ ├── lib.rs │ │ ├── state │ │ │ ├── mod.rs │ │ │ ├── quadratic_coefficients.rs │ │ │ ├── registrar.rs │ │ │ └── voter_weight_record.rs │ │ └── util │ │ │ └── mod.rs │ └── tests │ │ ├── configure_registrar.rs │ │ ├── create_registrar.rs │ │ ├── create_voter_weight_record.rs │ │ ├── fixtures │ │ ├── spl_governance.so │ │ └── spl_governance_addin_mock.so │ │ ├── program_test │ │ ├── governance_test.rs │ │ ├── mod.rs │ │ ├── predecessor_plugin_test.rs │ │ ├── program_test_bench.rs │ │ ├── quadratic_voter_test.rs │ │ └── tools.rs │ │ ├── update_voter_weight_record_predecessor_plugin.rs │ │ └── update_voter_weight_record_token_deposit.rs ├── realm-voter │ ├── Cargo.toml │ ├── Xargo.toml │ ├── src │ │ ├── error.rs │ │ ├── instructions │ │ │ ├── configure_governance_program.rs │ │ │ ├── configure_voter_weights.rs │ │ │ ├── create_max_voter_weight_record.rs │ │ │ ├── create_registrar.rs │ │ │ ├── create_voter_weight_record.rs │ │ │ ├── mod.rs │ │ │ └── update_voter_weight_record.rs │ │ ├── lib.rs │ │ ├── state │ │ │ ├── governance_program_config.rs │ │ │ ├── max_voter_weight_record.rs │ │ │ ├── mod.rs │ │ │ ├── registrar.rs │ │ │ └── voter_weight_record.rs │ │ └── tools │ │ │ ├── anchor.rs │ │ │ └── mod.rs │ └── tests │ │ ├── configure_governance_program.rs │ │ ├── configure_voter_weights.rs │ │ ├── create_max_voter_weight_record.rs │ │ ├── create_registrar.rs │ │ ├── create_voter_weight_record.rs │ │ ├── fixtures │ │ └── spl_governance.so │ │ ├── program_test │ │ ├── governance_test.rs │ │ ├── mod.rs │ │ ├── program_test_bench.rs │ │ ├── realm_voter_test.rs │ │ └── tools.rs │ │ └── update_voter_weight_record.rs ├── shared │ ├── Cargo.toml │ ├── Xargo.toml │ └── src │ │ ├── anchor.rs │ │ ├── compose.rs │ │ ├── error.rs │ │ ├── generic_max_voter_weight.rs │ │ ├── generic_voter_weight.rs │ │ ├── lib.rs │ │ ├── mint.rs │ │ └── token_owner_record.rs ├── token-haver │ ├── .tests │ │ ├── configure_mints.rs │ │ ├── create_registrar.rs │ │ ├── create_voter_weight_record.rs │ │ ├── fixtures │ │ │ └── spl_governance.so │ │ ├── program_test │ │ │ ├── governance_test.rs │ │ │ ├── mod.rs │ │ │ ├── program_test_bench.rs │ │ │ ├── token_haver_test.rs │ │ │ └── tools.rs │ │ └── update_voter_weight_record.rs │ ├── Cargo.toml │ ├── Xargo.toml │ ├── readme.md │ └── src │ │ ├── error.rs │ │ ├── instructions │ │ ├── configure_mints.rs │ │ ├── create_registrar.rs │ │ ├── create_voter_weight_record.rs │ │ ├── mod.rs │ │ └── update_voter_weight_record.rs │ │ ├── lib.rs │ │ ├── state │ │ ├── max_voter_weight_record.rs │ │ ├── mod.rs │ │ ├── registrar.rs │ │ └── voter_weight_record.rs │ │ └── tools │ │ ├── anchor.rs │ │ └── mod.rs └── token-voter │ ├── Cargo.toml │ ├── Xargo.toml │ ├── src │ ├── error.rs │ ├── governance.rs │ ├── instructions │ │ ├── close_voter.rs │ │ ├── configure_mint_config.rs │ │ ├── create_max_voter_weight_record.rs │ │ ├── create_registrar.rs │ │ ├── create_voter_weight_record.rs │ │ ├── deposit.rs │ │ ├── mod.rs │ │ ├── resize_registrar.rs │ │ └── withdraw.rs │ ├── lib.rs │ ├── state │ │ ├── deposit_entry.rs │ │ ├── mod.rs │ │ ├── registrar.rs │ │ ├── voter.rs │ │ └── voting_mint_config.rs │ └── tools │ │ ├── mod.rs │ │ └── spl_token.rs │ └── tests │ ├── cast_token_vote.rs │ ├── close_voter.rs │ ├── configure_voter_weights.rs │ ├── create_max_voter_weight_record.rs │ ├── create_registrar.rs │ ├── deposit.rs │ ├── fixtures │ ├── spl_governance.so │ └── spl_transfer_hook_example.so │ ├── program_test │ ├── governance_test.rs │ ├── mod.rs │ ├── program_test_bench.rs │ ├── token_voter_test.rs │ └── tools.rs │ ├── resize_registrar.rs │ └── withdraw.rs ├── run-release.sh ├── rust-toolchain.toml ├── scripts ├── getQuadraticVoterWeight.ts ├── updateVoterWeightRecord.ts └── utils │ ├── common.ts │ ├── constants.ts │ └── plugin.ts ├── src ├── common │ ├── Client.ts │ └── types.ts ├── gateway │ ├── client.ts │ ├── gateway.json │ └── gateway.ts ├── index.ts ├── nftVoter │ ├── client.ts │ ├── nft_voter.json │ └── nft_voter.ts ├── quadraticVoter │ ├── client.ts │ ├── quadratic.json │ └── quadratic.ts ├── realmVoter │ ├── client.ts │ ├── realm_voter.json │ └── realm_voter.ts ├── tokenHaver │ ├── client.ts │ ├── token_haver.json │ └── token_haver.ts └── tokenVoter │ ├── client.ts │ ├── token_voter.json │ └── token_voter.ts ├── tests ├── bonk-plugin │ ├── stake-idl.ts │ └── test.ts ├── nft-voter.ts └── token-voter │ └── test.ts ├── tsconfig.json └── yarn.lock /.cargo/audit.toml: -------------------------------------------------------------------------------- 1 | # RUSTSEC-2022-0093 ed25519-dalek imported by latest version of solana 2 | 3 | [advisories] 4 | ignore = [ 5 | "RUSTSEC-2020-0159", 6 | "RUSTSEC-2020-0071", # Potential segfault in the time crate 7 | "RUSTSEC-2022-0093", 8 | "RUSTSEC-2024-0344" # curve25519-dalek 9 | ] 10 | informational_warnings = ["unmaintained"] # warn for categories of informational advisories 11 | severity_threshold = "medium" # CVSS severity ("none", "low", "medium", "high", "critical") 12 | 13 | # Advisory Database Configuration 14 | [database] 15 | path = "~/.cargo/advisory-db" # Path where advisory git repo will be cloned 16 | url = "https://github.com/RustSec/advisory-db.git" # URL to git repo 17 | fetch = true # Perform a `git fetch` before auditing (default: true) 18 | stale = false # Allow stale advisory DB (i.e. no commits for 90 days, default: false) 19 | 20 | # Output Configuration 21 | [output] 22 | deny = [] # exit on error if unmaintained dependencies are found 23 | format = "terminal" # "terminal" (human readable report) or "json" 24 | quiet = false # Only print information on error 25 | show_tree = true # Show inverse dependency trees along with advisories (default: true) 26 | 27 | # Target Configuration 28 | [target] 29 | arch = "x86_64" # Ignore advisories for CPU architectures other than this one 30 | os = "linux" # Ignore advisories for operating systems other than this one 31 | 32 | # [packages] 33 | # source = "all" # "all", "public" or "local" 34 | 35 | [yanked] 36 | enabled = false # Warn for yanked crates in Cargo.lock (default: true) 37 | update_index = true # Auto-update the crates.io index (default: true) 38 | -------------------------------------------------------------------------------- /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # 2 | # Docker image to generate deterministic, verifiable builds of Anchor programs. 3 | # This must be run *after* a given ANCHOR_CLI version is published and a git tag 4 | # is released on GitHub. 5 | # 6 | 7 | FROM ubuntu:22.04 8 | 9 | ARG DEBIAN_FRONTEND=noninteractive 10 | 11 | ARG SOLANA_CLI="1.18.18" 12 | ARG ANCHOR_CLI="0.30.1" 13 | ARG NODE_VERSION="v18.16.0" 14 | 15 | ENV HOME="/root" 16 | ENV PATH="${HOME}/.cargo/bin:${PATH}" 17 | ENV PATH="${HOME}/.local/share/solana/install/active_release/bin:${PATH}" 18 | ENV PATH="${HOME}/.nvm/versions/node/${NODE_VERSION}/bin:${PATH}" 19 | 20 | # Install base utilities. 21 | RUN mkdir -p /workdir && mkdir -p /tmp && \ 22 | apt-get update -qq && apt-get upgrade -qq && apt-get install -qq \ 23 | build-essential git curl wget jq pkg-config python3-pip \ 24 | libssl-dev libudev-dev 25 | 26 | RUN wget http://nz2.archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb 27 | RUN dpkg -i libssl1.1_1.1.1f-1ubuntu2_amd64.deb 28 | 29 | # Install rust. 30 | RUN curl "https://sh.rustup.rs" -sfo rustup.sh && \ 31 | sh rustup.sh -y && \ 32 | rustup component add rustfmt clippy 33 | 34 | # Install node / npm / yarn. 35 | RUN curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash 36 | ENV NVM_DIR="${HOME}/.nvm" 37 | RUN . $NVM_DIR/nvm.sh && \ 38 | nvm install ${NODE_VERSION} && \ 39 | nvm use ${NODE_VERSION} && \ 40 | nvm alias default node && \ 41 | npm install -g yarn && \ 42 | yarn add ts-mocha 43 | 44 | # Install Solana tools. 45 | RUN sh -c "$(curl -sSfL https://release.solana.com/v${SOLANA_CLI}/install)" 46 | 47 | # Install anchor. 48 | RUN cargo install --git https://github.com/coral-xyz/anchor avm --locked --force 49 | RUN avm install ${ANCHOR_CLI} && avm use ${ANCHOR_CLI} 50 | 51 | WORKDIR /workdir 52 | #be sure to add `/root/.avm/bin` to your PATH to be able to run the installed binaries -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { "dockerfile": "Dockerfile" } 3 | } 4 | -------------------------------------------------------------------------------- /.devcontainer/token-haver/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { "dockerfile": "Dockerfile" } 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/audit-sec3.yml: -------------------------------------------------------------------------------- 1 | name: Sec3 Pro Audit 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | audit: 13 | runs-on: ubuntu-latest 14 | timeout-minutes: 15 15 | steps: 16 | - name: Check-out the repository 17 | uses: actions/checkout@v2 18 | - name: Sec3 Pro Audit 19 | continue-on-error: false # set to true if you don't want to fail jobs 20 | uses: sec3dev/pro-action@v1 21 | with: 22 | sec3-token: ${{ secrets.SEC3_TOKEN }} 23 | path: programs 24 | - name: Upload Sarif Report 25 | uses: github/codeql-action/upload-sarif@v1 26 | with: 27 | sarif_file: sec3-report.sarif -------------------------------------------------------------------------------- /.github/workflows/cargo-audit.yaml: -------------------------------------------------------------------------------- 1 | name: Cargo Audit 2 | 3 | on: 4 | push: 5 | branches: master 6 | pull_request: 7 | branches: master 8 | 9 | jobs: 10 | cargo-audit: 11 | name: Cargo Audit 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Install Cargo Audit 17 | uses: actions-rs/install@v0.1 18 | with: 19 | crate: cargo-audit 20 | version: latest 21 | 22 | # Run cargo audit using args from .cargo/audit.toml 23 | - name: Run Cargo Audit 24 | run: cargo audit -c always 25 | -------------------------------------------------------------------------------- /.github/workflows/lint-test.yaml: -------------------------------------------------------------------------------- 1 | name: Lint and Test 2 | 3 | on: 4 | push: 5 | branches: master 6 | pull_request: 7 | branches: master 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | SOLANA_VERSION: '1.18.18' 12 | RUST_TOOLCHAIN: stable 13 | 14 | jobs: 15 | lint: 16 | name: Run Linters 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v2 21 | - name: Install Rust 22 | uses: actions-rs/toolchain@v1 23 | with: 24 | override: true 25 | profile: minimal 26 | toolchain: ${{ env.RUST_TOOLCHAIN }} 27 | components: rustfmt, clippy 28 | 29 | - name: Cache dependencies 30 | uses: Swatinem/rust-cache@v1 31 | 32 | - name: Run fmt 33 | run: cargo fmt -- --check 34 | 35 | - name: Run clippy 36 | run: cargo clippy -- -A clippy::pedantic --deny=warnings 37 | 38 | test: 39 | name: Run Tests 40 | runs-on: ubuntu-latest 41 | 42 | steps: 43 | - uses: actions/checkout@v2 44 | - name: Install Linux dependencies 45 | run: sudo apt-get update && sudo apt-get install -y pkg-config build-essential libudev-dev 46 | 47 | - name: Install Rust 48 | uses: actions-rs/toolchain@v1 49 | with: 50 | override: true 51 | profile: minimal 52 | toolchain: ${{ env.RUST_TOOLCHAIN }} 53 | 54 | - name: Cache dependencies 55 | uses: Swatinem/rust-cache@v1 56 | 57 | - name: Cache Solana binaries 58 | uses: actions/cache@v2 59 | with: 60 | path: | 61 | ~/.cache/solana 62 | ~/.local/share/solana 63 | key: ${{ runner.os }}-${{ env.SOLANA_VERSION }} 64 | 65 | - name: Install Solana 66 | run: | 67 | echo Installing Solana v${{ env.SOLANA_VERSION }}...🧬 68 | sh -c "$(curl -sSfL https://release.solana.com/v${{ env.SOLANA_VERSION }}/install)" 69 | echo "$HOME/.local/share/solana/install/active_release/bin" >> $GITHUB_PATH 70 | export PATH="/home/runner/.local/share/solana/install/active_release/bin:$PATH" 71 | echo Generating keypair... 72 | solana-keygen new -o "$HOME/.config/solana/id.json" --no-passphrase --silent 73 | 74 | - name: Run SBF tests 75 | run: cargo test-sbf 76 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | **/*.rs.bk 6 | dist 7 | node_modules 8 | docker-target 9 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | 2 | .anchor 3 | .DS_Store 4 | target 5 | **/*.rs.bk 6 | node_modules 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "addin", 4 | "arrayref", 5 | "itertools", 6 | "Keypair", 7 | "lamports", 8 | "Metaplex", 9 | "Metas", 10 | "Pubkey", 11 | "solana" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /Anchor.toml: -------------------------------------------------------------------------------- 1 | anchor_version = "0.30.1" 2 | solana_version = "1.18.18" 3 | 4 | [features] 5 | seeds = false 6 | 7 | [programs.localnet] 8 | nft_voter = "GnftV5kLjd67tvHpNGyodwWveEKivz3ZWvvE3Z4xi2iw" 9 | gateway = "GgathUhdrCWRHowoRKACjgWhYHfxCEdBi5ViqYN6HVxk" 10 | quadratic = "quadCSapU8nTdLg73KHDnmdxKnJQsh7GUbu5tZfnRRr" 11 | solana-gateway = "gatem74V238djXdzWnJf94Wo1DcnuGkfijbf3AuBhfs" 12 | token-haver = "7gobfUihgoxA14RUnVaseoah89ggCgYAzgz1JoaPAXam" 13 | token_voter = "3JhBg9bSPcfWGFa3t8LH7ooVtrjm45yCkHpxYXMXstUM" 14 | bonk-plugin = "7yJT49ajgYyuhWYzQMzwEt9u9Zbbt7r8Ft2wq1bhhfyy" 15 | 16 | [registry] 17 | url = "https://anchor.projectserum.com" 18 | 19 | [provider] 20 | cluster = "localnet" 21 | wallet = "~/.config/solana/id.json" 22 | 23 | [scripts] 24 | test = "yarn run ts-mocha -p ./tsconfig.json -t 1000000 tests/**/*.ts" 25 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "programs/gateway", 4 | "programs/quadratic", 5 | "programs/realm-voter", 6 | "programs/nft-voter", 7 | "programs/bonk-plugin", 8 | "programs/token-haver", 9 | "programs/token-voter" 10 | ] 11 | resolver = "2" 12 | 13 | [profile.release] 14 | overflow-checks = true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2020 Solana Foundation. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # governance-program-library 2 | 3 | This repo still exists in archived form, but the maintained version has now relocated to: https://github.com/Mythic-Project/governance-program-library 4 | -------------------------------------------------------------------------------- /migrations/deploy.ts: -------------------------------------------------------------------------------- 1 | // Migrations are an early feature. Currently, they're nothing more than this 2 | // single deploy script that's invoked from the CLI, injecting a provider 3 | // configured from the workspace's Anchor.toml. 4 | 5 | const anchor = require("@coral-xyz/anchor"); 6 | 7 | module.exports = async function (provider) { 8 | // Configure client to use the provider. 9 | anchor.setProvider(provider); 10 | 11 | // Add your deploy script here. 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@solana/governance-program-library", 3 | "version": "0.18.0", 4 | "description": "Client for Governance Program Library which is a set of extensions for Solana's spl-governance program.", 5 | "author": "Solana Maintainers ", 6 | "license": "MIT", 7 | "main": "dist/index.js", 8 | "types": "dist/index.d.ts", 9 | "files": [ 10 | "dist/**/*" 11 | ], 12 | "repository": "https://github.com/solana-labs/governance-program-library", 13 | "scripts": { 14 | "build": "tsc", 15 | "clean": "rm -rf dist", 16 | "type-check": "tsc --pretty --noEmit", 17 | "format": "prettier --check .", 18 | "lint": "eslint . --ext ts --ext tsx --ext js --quiet" 19 | }, 20 | "devDependencies": { 21 | "@tsconfig/recommended": "^1.0.1", 22 | "@typescript-eslint/eslint-plugin": "^4.14.2", 23 | "@typescript-eslint/parser": "^4.14.2", 24 | "eslint": "^7.28.0", 25 | "eslint-config-prettier": "^7.2.0", 26 | "mocha": "^9.2.2", 27 | "prettier": "^2.0.5", 28 | "ts-mocha": "^9.0.2", 29 | "ts-node": "^9.1.1", 30 | "typedoc": "^0.22.5", 31 | "typescript": "^4.1.3" 32 | }, 33 | "publishConfig": { 34 | "access": "public" 35 | }, 36 | "prettier": { 37 | "singleQuote": true, 38 | "trailingComma": "all" 39 | }, 40 | "dependencies": { 41 | "@identity.com/solana-gateway-ts": "^0.12.0", 42 | "@solana/spl-governance": "^0.3.28", 43 | "@coral-xyz/anchor": "^0.30.1" 44 | } 45 | } -------------------------------------------------------------------------------- /programs/bonk-plugin/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "bonk-plugin" 3 | version = "0.1.0" 4 | description = "Created with Anchor" 5 | edition = "2021" 6 | 7 | [lib] 8 | crate-type = ["cdylib", "lib"] 9 | name = "gpl_bonk_plugin" 10 | 11 | [features] 12 | default = [] 13 | cpi = ["no-entrypoint"] 14 | no-entrypoint = [] 15 | no-idl = [] 16 | no-log-ix-name = [] 17 | idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] 18 | 19 | [dependencies] 20 | anchor-lang = { version = "0.30.1", features = ["init-if-needed"] } 21 | anchor-spl = "=0.30.1" 22 | spl-governance = { version = "4.0.0", features = ["no-entrypoint"] } 23 | spl-governance-tools = "0.1.4" 24 | solana-program = "1.18.18" 25 | itertools = "0.10.2" 26 | gpl-shared = { path = "../shared", features = ["no-entrypoint"] } 27 | spl-token = { version = "4.0.0", features = [ "no-entrypoint" ] } 28 | 29 | [dev-dependencies] 30 | borsh = "0.10.3" 31 | solana-sdk = "1.18.18" 32 | solana-program-test = "1.18.18" 33 | bytemuck = { version = "1.7", features = ["derive"] } 34 | -------------------------------------------------------------------------------- /programs/bonk-plugin/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /programs/bonk-plugin/src/error.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum BonkPluginError { 5 | #[msg("Invalid Realm Authority")] 6 | InvalidRealmAuthority, 7 | #[msg("The mint of the stake pool is different from the realm")] 8 | InvalidGoverningToken, 9 | #[msg("Invalid VoterWeightRecord Realm")] 10 | InvalidVoterWeightRecordRealm, 11 | #[msg("Invalid VoterWeightRecord Mint")] 12 | InvalidVoterWeightRecordMint, 13 | #[msg("Invalid Stake Pool")] 14 | InvalidStakePool, 15 | #[msg("Invalid TokenOwner for VoterWeightRecord")] 16 | InvalidTokenOwnerForVoterWeightRecord, 17 | #[msg("The owner of the receipt does not match")] 18 | VoterDoesNotOwnDepositReceipt, 19 | #[msg("The deposit receipt was already provided")] 20 | DuplicatedReceiptDetected, 21 | #[msg("The stake deposit receipt has already expired")] 22 | ExpiredStakeDepositReceipt, 23 | #[msg("The stake deposit receipt will expire before proposal")] 24 | InvalidStakeDuration, 25 | #[msg("The stake deposit receipts count does not match")] 26 | ReceiptsCountMismatch, 27 | #[msg("Proposal account is required for Cast Vote action")] 28 | ProposalAccountIsRequired, 29 | #[msg("Action target is different from the public key of the proposal")] 30 | ActionTargetMismatch, 31 | #[msg("Maximum deposits length reached")] 32 | MaximumDepositsReached, 33 | } 34 | -------------------------------------------------------------------------------- /programs/bonk-plugin/src/instructions/create_registrar.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use anchor_spl::token::Mint; 3 | use spl_governance::state::realm; 4 | 5 | use crate::{ 6 | error::BonkPluginError, state::*, utils::stake_pool::StakePool, SPL_TOKEN_STAKING_PROGRAM_ID, 7 | }; 8 | 9 | /// Creates Registrar storing Stake Pool details for Bonk 10 | /// This instruction should only be executed once per realm/governing_token_mint to create the account 11 | #[derive(Accounts)] 12 | #[instruction()] 13 | pub struct CreateRegistrar<'info> { 14 | #[account( 15 | init, 16 | seeds = [ 17 | b"registrar".as_ref(), 18 | realm.key().as_ref(), 19 | governing_token_mint.key().as_ref()], 20 | bump, 21 | payer = payer, 22 | space = 8 + Registrar::INIT_SPACE 23 | )] 24 | pub registrar: Account<'info, Registrar>, 25 | 26 | #[account(executable)] 27 | /// CHECK: Can be any instance of spl-governance and it's not known at the compilation time 28 | pub governance_program_id: UncheckedAccount<'info>, 29 | 30 | /// CHECK: The account data is not used 31 | pub previous_voter_weight_plugin_program_id: Option>, 32 | 33 | /// CHECK: Owned by spl-governance instance specified in governance_program_id 34 | #[account(owner = governance_program_id.key())] 35 | pub realm: UncheckedAccount<'info>, 36 | 37 | /// CHECK: Owned by SPL Staking Program 38 | #[account( 39 | owner = SPL_TOKEN_STAKING_PROGRAM_ID, 40 | )] 41 | pub stake_pool: AccountInfo<'info>, 42 | 43 | pub governing_token_mint: Account<'info, Mint>, 44 | pub realm_authority: Signer<'info>, 45 | #[account(mut)] 46 | pub payer: Signer<'info>, 47 | pub system_program: Program<'info, System>, 48 | } 49 | 50 | pub fn create_registrar_handler(ctx: Context) -> Result<()> { 51 | let registrar = &mut ctx.accounts.registrar; 52 | registrar.governance_program_id = ctx.accounts.governance_program_id.key(); 53 | registrar.realm = ctx.accounts.realm.key(); 54 | registrar.realm_authority = ctx.accounts.realm_authority.key(); 55 | registrar.governing_token_mint = ctx.accounts.governing_token_mint.key(); 56 | registrar.stake_pool = ctx.accounts.stake_pool.key(); 57 | 58 | if let Some(previous_voter_weight_plugin_program_info) = 59 | &ctx.accounts.previous_voter_weight_plugin_program_id 60 | { 61 | registrar.previous_voter_weight_plugin_program_id = 62 | Some(previous_voter_weight_plugin_program_info.key()); 63 | } 64 | 65 | // Verify that realm_authority is the expected authority of the Realm 66 | // and that the mint matches one of the realm mints too 67 | let realm = realm::get_realm_data_for_governing_token_mint( 68 | ®istrar.governance_program_id, 69 | &ctx.accounts.realm, 70 | ®istrar.governing_token_mint, 71 | )?; 72 | 73 | let stake_pool = StakePool::deserialize_checked(&ctx.accounts.stake_pool)?; 74 | 75 | require!( 76 | realm.authority.unwrap() == ctx.accounts.realm_authority.key(), 77 | BonkPluginError::InvalidRealmAuthority 78 | ); 79 | 80 | require!( 81 | stake_pool.mint == ctx.accounts.governing_token_mint.key(), 82 | BonkPluginError::InvalidGoverningToken 83 | ); 84 | 85 | Ok(()) 86 | } 87 | -------------------------------------------------------------------------------- /programs/bonk-plugin/src/instructions/create_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::state::*; 2 | use anchor_lang::prelude::*; 3 | 4 | /// Creates VoterWeightRecord used by spl-gov 5 | /// This instruction should only be executed once per realm/governing_token_mint/governing_token_owner 6 | /// to create the account 7 | #[derive(Accounts)] 8 | #[instruction(governing_token_owner: Pubkey)] 9 | pub struct CreateVoterWeightRecord<'info> { 10 | // The Registrar the VoterWeightRecord account belongs to 11 | pub registrar: Account<'info, Registrar>, 12 | 13 | #[account( 14 | init, 15 | seeds = [ 16 | b"voter-weight-record".as_ref(), 17 | registrar.realm.key().as_ref(), 18 | registrar.governing_token_mint.key().as_ref(), 19 | governing_token_owner.as_ref()], 20 | bump, 21 | payer = payer, 22 | space = VoterWeightRecord::get_space() 23 | )] 24 | pub voter_weight_record: Account<'info, VoterWeightRecord>, 25 | 26 | #[account( 27 | init, 28 | seeds = [ 29 | b"stake-deposit-record".as_ref(), 30 | voter_weight_record.key().as_ref(), 31 | ], 32 | bump, 33 | payer = payer, 34 | space = StakeDepositRecord::FIXED_LEN + 8 * 32 35 | )] 36 | pub stake_deposit_record: Account<'info, StakeDepositRecord>, 37 | 38 | #[account(mut)] 39 | pub payer: Signer<'info>, 40 | 41 | pub system_program: Program<'info, System>, 42 | } 43 | 44 | pub fn create_voter_weight_record_handler( 45 | ctx: Context, 46 | governing_token_owner: Pubkey, 47 | ) -> Result<()> { 48 | let voter_weight_record = &mut ctx.accounts.voter_weight_record; 49 | let stake_deposit_record = &mut ctx.accounts.stake_deposit_record; 50 | 51 | let registrar = &ctx.accounts.registrar; 52 | 53 | voter_weight_record.realm = registrar.realm.key(); 54 | voter_weight_record.governing_token_mint = registrar.governing_token_mint.key(); 55 | voter_weight_record.governing_token_owner = governing_token_owner; 56 | 57 | // Set expiry to expired 58 | voter_weight_record.voter_weight_expiry = Some(0); 59 | 60 | stake_deposit_record.weight_action_target = None; 61 | stake_deposit_record.weight_action = None; 62 | stake_deposit_record.deposits_len = 8; 63 | stake_deposit_record.bump = ctx.bumps.stake_deposit_record; 64 | stake_deposit_record.previous_voter_weight = 0; 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /programs/bonk-plugin/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub use create_registrar::*; 2 | mod create_registrar; 3 | 4 | pub use create_voter_weight_record::*; 5 | mod create_voter_weight_record; 6 | 7 | pub use update_voter_weight_record::*; 8 | mod update_voter_weight_record; 9 | -------------------------------------------------------------------------------- /programs/bonk-plugin/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::{prelude::*, solana_program::pubkey}; 2 | 3 | mod instructions; 4 | use instructions::*; 5 | use state::VoterWeightAction; 6 | 7 | pub mod error; 8 | pub mod state; 9 | pub mod utils; 10 | 11 | declare_id!("7yJT49ajgYyuhWYzQMzwEt9u9Zbbt7r8Ft2wq1bhhfyy"); 12 | 13 | #[constant] 14 | pub const SPL_TOKEN_STAKING_PROGRAM_ID: Pubkey = 15 | pubkey!("STAKEkKzbdeKkqzKpLkNQD3SUuLgshDKCD7U8duxAbB"); 16 | 17 | #[program] 18 | pub mod bonk_plugin { 19 | use super::*; 20 | 21 | pub fn create_registrar(ctx: Context) -> Result<()> { 22 | log_version(); 23 | create_registrar_handler(ctx) 24 | } 25 | 26 | pub fn create_voter_weight_record( 27 | ctx: Context, 28 | governing_token_owner: Pubkey, 29 | ) -> Result<()> { 30 | log_version(); 31 | create_voter_weight_record_handler(ctx, governing_token_owner) 32 | } 33 | 34 | pub fn update_voter_weight_record( 35 | ctx: Context, 36 | stake_receipts_count: u8, 37 | action_target: Pubkey, 38 | action: VoterWeightAction, 39 | ) -> Result<()> { 40 | log_version(); 41 | update_voter_weight_record_handler(ctx, stake_receipts_count, action_target, action) 42 | } 43 | } 44 | 45 | fn log_version() { 46 | msg!("VERSION:{:?}", env!("CARGO_PKG_VERSION")); 47 | } 48 | -------------------------------------------------------------------------------- /programs/bonk-plugin/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | pub use registrar::*; 2 | mod registrar; 3 | 4 | pub use voter_weight_record::*; 5 | mod voter_weight_record; 6 | 7 | pub use stake_deposit_record::*; 8 | mod stake_deposit_record; 9 | -------------------------------------------------------------------------------- /programs/bonk-plugin/src/state/stake_deposit_record.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | use super::VoterWeightAction; 4 | 5 | #[account] 6 | pub struct StakeDepositRecord { 7 | pub deposits: Vec, 8 | pub weight_action_target: Option, 9 | pub weight_action: Option, 10 | pub deposits_len: u8, 11 | pub bump: u8, 12 | pub previous_voter_weight: u64, 13 | } 14 | 15 | impl StakeDepositRecord { 16 | pub const FIXED_LEN: usize = 8 + 33 + 4 + 1 + 1 + 8 + 2; 17 | 18 | pub fn realloc_bytes( 19 | &self, 20 | new_receipts_len: u8, 21 | action_target: Pubkey, 22 | action: VoterWeightAction, 23 | ) -> usize { 24 | let new_len = self.new_deposit_len(new_receipts_len, action_target, action); 25 | StakeDepositRecord::FIXED_LEN + (new_len as usize) * 32 26 | } 27 | 28 | pub fn new_deposit_len( 29 | &self, 30 | new_receipts_len: u8, 31 | action_target: Pubkey, 32 | action: VoterWeightAction, 33 | ) -> u8 { 34 | let new_len = if Some(action_target) == self.weight_action_target 35 | && Some(action) == self.weight_action 36 | { 37 | let current_len = self.deposits.len() as u8; 38 | current_len.checked_add(new_receipts_len).unwrap() 39 | } else { 40 | new_receipts_len 41 | }; 42 | 43 | if new_len > self.deposits_len { 44 | new_len 45 | } else { 46 | self.deposits_len 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /programs/bonk-plugin/src/state/voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use gpl_shared::compose::VoterWeightRecordBase; 3 | use solana_program::pubkey::PUBKEY_BYTES; 4 | 5 | use crate::utils::anchor::DISCRIMINATOR_SIZE; 6 | 7 | /// VoterWeightAction enum as defined in spl-governance-addin-api 8 | /// It's redefined here for Anchor to export it to IDL 9 | #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy, PartialEq)] 10 | pub enum VoterWeightAction { 11 | /// Cast vote for a proposal. Target: Proposal 12 | CastVote, 13 | 14 | /// Comment a proposal. Target: Proposal 15 | CommentProposal, 16 | 17 | /// Create Governance within a realm. Target: Realm 18 | CreateGovernance, 19 | 20 | /// Create a proposal for a governance. Target: Governance 21 | CreateProposal, 22 | 23 | /// Signs off a proposal for a governance. Target: Proposal 24 | /// Note: SignOffProposal is not supported in the current version 25 | SignOffProposal, 26 | } 27 | 28 | /// VoterWeightRecord account as defined in spl-governance-addin-api 29 | /// It's redefined here without account_discriminator for Anchor to treat it as native account 30 | /// 31 | /// The account is used as an api interface to provide voting power to the governance program from external addin contracts 32 | #[account] 33 | #[derive(Debug, PartialEq)] 34 | pub struct VoterWeightRecord { 35 | pub realm: Pubkey, 36 | pub governing_token_mint: Pubkey, 37 | pub governing_token_owner: Pubkey, 38 | pub voter_weight: u64, 39 | pub voter_weight_expiry: Option, 40 | pub weight_action: Option, 41 | pub weight_action_target: Option, 42 | pub reserved: [u8; 8], 43 | } 44 | 45 | impl VoterWeightRecord { 46 | pub fn get_space() -> usize { 47 | DISCRIMINATOR_SIZE + PUBKEY_BYTES * 4 + 8 + 1 + 8 + 1 + 1 + 1 + 8 48 | } 49 | } 50 | 51 | impl<'a> VoterWeightRecordBase<'a> for VoterWeightRecord { 52 | fn get_governing_token_mint(&'a self) -> &'a Pubkey { 53 | &self.governing_token_mint 54 | } 55 | 56 | fn get_governing_token_owner(&'a self) -> &'a Pubkey { 57 | &self.governing_token_owner 58 | } 59 | } 60 | 61 | impl Default for VoterWeightRecord { 62 | fn default() -> Self { 63 | Self { 64 | realm: Default::default(), 65 | governing_token_mint: Default::default(), 66 | governing_token_owner: Default::default(), 67 | voter_weight: Default::default(), 68 | voter_weight_expiry: Some(0), 69 | weight_action: Some(VoterWeightAction::CastVote), 70 | weight_action_target: Some(Default::default()), 71 | reserved: Default::default(), 72 | } 73 | } 74 | } 75 | 76 | #[cfg(test)] 77 | mod test { 78 | 79 | use super::*; 80 | 81 | #[test] 82 | fn test_get_space() { 83 | // Arrange 84 | let expected_space = VoterWeightRecord::get_space(); 85 | 86 | // Act 87 | let actual_space = 88 | DISCRIMINATOR_SIZE + VoterWeightRecord::default().try_to_vec().unwrap().len(); 89 | 90 | // Assert 91 | assert_eq!(expected_space, actual_space); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /programs/bonk-plugin/src/utils/anchor.rs: -------------------------------------------------------------------------------- 1 | pub const DISCRIMINATOR_SIZE: usize = 8; 2 | -------------------------------------------------------------------------------- /programs/bonk-plugin/src/utils/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod anchor; 2 | pub mod stake_deposit_receipt; 3 | pub mod stake_pool; 4 | -------------------------------------------------------------------------------- /programs/bonk-plugin/src/utils/stake_deposit_receipt.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | use crate::SPL_TOKEN_STAKING_PROGRAM_ID; 4 | 5 | #[repr(C)] 6 | #[derive(AnchorDeserialize, Debug)] 7 | pub struct StakeDepositReceipt { 8 | pub discriminator: u64, 9 | /** Pubkey that owns the staked assets */ 10 | pub owner: Pubkey, 11 | /** Pubkey that paid for the deposit */ 12 | pub payer: Pubkey, 13 | /** StakePool the deposit is for */ 14 | pub stake_pool: Pubkey, 15 | /** Duration of the lockup period in seconds */ 16 | pub lockup_duration: u64, 17 | /** Timestamp in seconds of when the stake lockup began */ 18 | pub deposit_timestamp: i64, 19 | /** Amount of SPL token deposited */ 20 | pub deposit_amount: u64, 21 | /** Amount of stake weighted by lockup duration. */ 22 | pub effective_stake: u128, 23 | /// The amount per reward that has been claimed or perceived to be claimed. Indexes align with 24 | /// the StakedPool reward_pools property. 25 | pub claimed_amounts: [u128; 10], 26 | } 27 | 28 | impl StakeDepositReceipt { 29 | pub const ACCOUNT_DISCRIMINATOR: [u8; 8] = [210, 98, 254, 196, 151, 68, 235, 0]; 30 | 31 | pub fn deserialize_checked(stake_deposit_receipt_account_info: &AccountInfo) -> Result { 32 | if stake_deposit_receipt_account_info.owner != &SPL_TOKEN_STAKING_PROGRAM_ID { 33 | return Err(anchor_lang::error!( 34 | anchor_lang::error::ErrorCode::AccountOwnedByWrongProgram 35 | ) 36 | .with_account_name("StakeDepositReceipt")); 37 | } 38 | 39 | let stake_deposit_receipt_data = &stake_deposit_receipt_account_info.try_borrow_data()?; 40 | let data = &mut stake_deposit_receipt_data.as_ref(); 41 | 42 | let stake_deposit_receipt = Self::try_from_slice(data)?; 43 | 44 | if stake_deposit_receipt.discriminator.to_le_bytes() != Self::ACCOUNT_DISCRIMINATOR { 45 | return Err(anchor_lang::error!( 46 | anchor_lang::error::ErrorCode::AccountDiscriminatorMismatch 47 | ) 48 | .with_account_name("StakeDepositReceipt")); 49 | } 50 | 51 | Ok(stake_deposit_receipt) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /programs/bonk-plugin/tests/create_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::program_test::bonk_plugin_test::BonkPluginTest; 2 | use program_test::{spl_token_staking_test::SplTokenStakingCookie, tools::assert_ix_err}; 3 | use solana_program::instruction::InstructionError; 4 | use solana_program_test::*; 5 | use solana_sdk::transport::TransportError; 6 | 7 | mod program_test; 8 | 9 | #[tokio::test] 10 | async fn test_create_voter_weight_record() -> Result<(), TransportError> { 11 | // Arrange 12 | let mut bonk_plugin_test = BonkPluginTest::start_new().await; 13 | 14 | let realm_cookie = bonk_plugin_test.governance.with_realm().await?; 15 | 16 | // Act 17 | let mut spl_token_staking_cookie = SplTokenStakingCookie::new(bonk_plugin_test.bench.clone()); 18 | let stake_pool_pubkey = spl_token_staking_cookie 19 | .with_stake_pool(&realm_cookie.community_mint_cookie.address) 20 | .await?; 21 | let registrar_cookie = bonk_plugin_test 22 | .with_registrar(&realm_cookie, &stake_pool_pubkey) 23 | .await?; 24 | 25 | let voter_cookie = bonk_plugin_test.bench.with_wallet().await; 26 | 27 | // Act 28 | let voter_weight_record_cookie = bonk_plugin_test 29 | .with_voter_weight_record(®istrar_cookie, &voter_cookie) 30 | .await?; 31 | 32 | // Assert 33 | 34 | let voter_weight_record = bonk_plugin_test 35 | .get_voter_weight_record(&voter_weight_record_cookie.address) 36 | .await; 37 | 38 | assert_eq!(voter_weight_record_cookie.account, voter_weight_record); 39 | 40 | Ok(()) 41 | } 42 | 43 | #[tokio::test] 44 | async fn test_create_voter_weight_record_with_already_exists_error() -> Result<(), TransportError> { 45 | // Arrange 46 | let mut bonk_plugin_test = BonkPluginTest::start_new().await; 47 | 48 | let realm_cookie = bonk_plugin_test.governance.with_realm().await?; 49 | // Act 50 | let mut spl_token_staking_cookie = SplTokenStakingCookie::new(bonk_plugin_test.bench.clone()); 51 | let stake_pool_pubkey = spl_token_staking_cookie 52 | .with_stake_pool(&realm_cookie.community_mint_cookie.address) 53 | .await?; 54 | let registrar_cookie = bonk_plugin_test 55 | .with_registrar(&realm_cookie, &stake_pool_pubkey) 56 | .await?; 57 | 58 | let voter_cookie = bonk_plugin_test.bench.with_wallet().await; 59 | 60 | bonk_plugin_test 61 | .with_voter_weight_record(®istrar_cookie, &voter_cookie) 62 | .await?; 63 | 64 | bonk_plugin_test.bench.advance_clock().await; 65 | 66 | // Act 67 | let err = bonk_plugin_test 68 | .with_voter_weight_record(®istrar_cookie, &voter_cookie) 69 | .await 70 | .err() 71 | .unwrap(); 72 | 73 | // Assert 74 | 75 | // InstructionError::Custom(0) is returned for TransactionError::AccountInUse 76 | assert_ix_err(err, InstructionError::Custom(0)); 77 | 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /programs/bonk-plugin/tests/fixtures/spl_governance.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-labs/governance-program-library/c7a49fa2a663c9bdf1260827c0d9b15790ebf24b/programs/bonk-plugin/tests/fixtures/spl_governance.so -------------------------------------------------------------------------------- /programs/bonk-plugin/tests/fixtures/spl_token_staking.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-labs/governance-program-library/c7a49fa2a663c9bdf1260827c0d9b15790ebf24b/programs/bonk-plugin/tests/fixtures/spl_token_staking.so -------------------------------------------------------------------------------- /programs/bonk-plugin/tests/program_test/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod bonk_plugin_test; 2 | pub mod governance_test; 3 | pub mod program_test_bench; 4 | pub mod spl_token_staking_test; 5 | pub mod tools; 6 | -------------------------------------------------------------------------------- /programs/gateway/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gpl-civic-gateway" 3 | version = "0.1.1" 4 | description = "SPL Governance addin for Civic Pass (www.civic.com)" 5 | license = "Apache-2.0" 6 | edition = "2018" 7 | 8 | [lib] 9 | crate-type = ["cdylib", "lib"] 10 | name = "gpl_civic_gateway" 11 | 12 | [features] 13 | no-entrypoint = [] 14 | no-idl = [] 15 | no-log-ix-name = [] 16 | cpi = ["no-entrypoint"] 17 | default = [] 18 | idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] 19 | 20 | [dependencies] 21 | arrayref = "0.3.6" 22 | anchor-lang = { version = "0.30.1", features = ["init-if-needed"] } 23 | anchor-spl = "0.30.1" 24 | enum_dispatch = "0.3.8" 25 | itertools = "0.12.0" 26 | num = "0.4" 27 | num-derive = "0.4.1" 28 | num-traits = "0.2" 29 | solana-gateway = { version = "0.6.0", features = ["no-entrypoint"] } 30 | solana-program = "1.18.18" 31 | spl-governance = { version = "4.0", features = ["no-entrypoint"] } 32 | spl-governance-tools= "0.1.4" 33 | spl-governance-addin-api = "0.1.4" 34 | spl-token = { version = "4.0.0", features = [ "no-entrypoint" ] } 35 | 36 | [dev-dependencies] 37 | borsh = "0.10.3" 38 | solana-sdk = "1.18.18" 39 | solana-program-test = "1.18.18" 40 | spl-governance-addin-mock = "0.1.4" 41 | -------------------------------------------------------------------------------- /programs/gateway/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /programs/gateway/src/error.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum GatewayError { 5 | #[msg("Invalid realm authority")] 6 | InvalidRealmAuthority, 7 | 8 | #[msg("Invalid realm for the provided registrar")] 9 | InvalidRealmForRegistrar, 10 | 11 | #[msg("Invalid TokenOwnerRecord as input voter weight (expecting TokenOwnerRecord V1 or V2)")] 12 | InvalidPredecessorTokenOwnerRecord, 13 | 14 | #[msg("Invalid VoterWeightRecord as input voter weight (expecting VoterWeightRecord)")] 15 | InvalidPredecessorVoterWeightRecord, 16 | 17 | #[msg("Invalid VoterWeightRecord realm for input voter weight")] 18 | InvalidPredecessorVoterWeightRecordRealm, 19 | 20 | #[msg("Invalid VoterWeightRecord governance token mint for input voter weight")] 21 | InvalidPredecessorVoterWeightRecordGovTokenMint, 22 | 23 | #[msg("Invalid VoterWeightRecord governance token owner for input voter weight")] 24 | InvalidPredecessorVoterWeightRecordGovTokenOwner, 25 | 26 | #[msg("Invalid VoterWeightRecord realm")] 27 | InvalidVoterWeightRecordRealm, 28 | 29 | #[msg("Invalid VoterWeightRecord mint")] 30 | InvalidVoterWeightRecordMint, 31 | 32 | #[msg("Invalid gateway token")] 33 | InvalidGatewayToken, 34 | 35 | #[msg("Previous voter weight plugin required but not provided")] 36 | MissingPreviousVoterWeightPlugin, 37 | } 38 | -------------------------------------------------------------------------------- /programs/gateway/src/instructions/configure_registrar.rs: -------------------------------------------------------------------------------- 1 | use crate::error::GatewayError; 2 | use crate::state::*; 3 | use anchor_lang::prelude::*; 4 | use spl_governance::state::realm; 5 | 6 | /// Configures the Gateway Registrar, 7 | /// allowing the gatekeeper network or previous plugin to be updated 8 | #[derive(Accounts)] 9 | #[instruction(use_previous_voter_weight_plugin:bool)] 10 | pub struct ConfigureRegistrar<'info> { 11 | /// The Gateway Plugin Registrar to be updated 12 | #[account(mut)] 13 | pub registrar: Account<'info, Registrar>, 14 | 15 | /// An spl-governance Realm 16 | /// 17 | /// Realm is validated in the instruction: 18 | /// - Realm is owned by the governance_program_id 19 | /// - realm_authority is realm.authority 20 | /// 21 | /// CHECK: Owned by spl-governance instance specified in governance_program_id 22 | #[account( 23 | address = registrar.realm @ GatewayError::InvalidRealmForRegistrar, 24 | owner = registrar.governance_program_id.key() 25 | )] 26 | pub realm: UncheckedAccount<'info>, 27 | 28 | /// realm_authority must sign and match Realm.authority 29 | pub realm_authority: Signer<'info>, 30 | 31 | /// The new Identity.com Gateway gatekeeper network 32 | /// (See the registry struct docs for details). 33 | /// CHECK: This can be any public key. The gateway library checks that the provided 34 | /// Gateway Token belongs to this gatekeeper network, so passing a particular key here is 35 | /// essentially saying "We trust this gatekeeper network". 36 | pub gatekeeper_network: UncheckedAccount<'info>, 37 | } 38 | 39 | /// Configures a Registrar, updating the gatekeeperNetwork or the previous plugin program ID 40 | pub fn configure_registrar( 41 | ctx: Context, 42 | use_previous_voter_weight_plugin: bool, 43 | ) -> Result<()> { 44 | let registrar = &mut ctx.accounts.registrar; 45 | registrar.gatekeeper_network = ctx.accounts.gatekeeper_network.key(); 46 | 47 | let remaining_accounts = &ctx.remaining_accounts; 48 | 49 | // If the plugin has a previous plugin, it "inherits" the vote weight from a vote_weight_account owned 50 | // by the previous plugin. This chain is registered here. 51 | registrar.previous_voter_weight_plugin_program_id = use_previous_voter_weight_plugin 52 | .then(|| { 53 | remaining_accounts 54 | .first() 55 | .ok_or(GatewayError::MissingPreviousVoterWeightPlugin) 56 | .map(|account| account.key) 57 | }) 58 | .transpose()? 59 | .cloned(); 60 | 61 | // Verify that realm_authority is the expected authority of the Realm 62 | // and that the mint matches one of the realm mints too. 63 | let realm = realm::get_realm_data_for_governing_token_mint( 64 | ®istrar.governance_program_id, 65 | &ctx.accounts.realm, 66 | ®istrar.governing_token_mint, 67 | )?; 68 | require_eq!( 69 | realm.authority.unwrap(), 70 | ctx.accounts.realm_authority.key(), 71 | GatewayError::InvalidRealmAuthority 72 | ); 73 | 74 | Ok(()) 75 | } 76 | -------------------------------------------------------------------------------- /programs/gateway/src/instructions/create_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::state::*; 2 | use anchor_lang::prelude::*; 3 | 4 | /// Creates VoterWeightRecord used by spl-gov 5 | /// This instruction should only be executed once per realm/governing_token_mint/governing_token_owner 6 | /// to create the account 7 | #[derive(Accounts)] 8 | #[instruction(governing_token_owner: Pubkey)] 9 | pub struct CreateVoterWeightRecord<'info> { 10 | // The Registrar the VoterWeightRecord account belongs to 11 | pub registrar: Account<'info, Registrar>, 12 | 13 | #[account( 14 | init, 15 | seeds = [ b"voter-weight-record".as_ref(), 16 | registrar.realm.key().as_ref(), 17 | registrar.governing_token_mint.key().as_ref(), 18 | governing_token_owner.as_ref()], 19 | bump, 20 | payer = payer, 21 | space = VoterWeightRecord::get_space() 22 | )] 23 | pub voter_weight_record: Account<'info, VoterWeightRecord>, 24 | 25 | #[account(mut)] 26 | pub payer: Signer<'info>, 27 | 28 | pub system_program: Program<'info, System>, 29 | } 30 | 31 | pub fn create_voter_weight_record( 32 | ctx: Context, 33 | governing_token_owner: Pubkey, 34 | ) -> Result<()> { 35 | let voter_weight_record = &mut ctx.accounts.voter_weight_record; 36 | let registrar = &ctx.accounts.registrar; 37 | 38 | voter_weight_record.realm = registrar.realm.key(); 39 | voter_weight_record.governing_token_mint = registrar.governing_token_mint.key(); 40 | voter_weight_record.governing_token_owner = governing_token_owner; 41 | 42 | // Set expiry to expired 43 | voter_weight_record.voter_weight_expiry = Some(0); 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /programs/gateway/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub use create_registrar::*; 2 | mod create_registrar; 3 | 4 | pub use configure_registrar::*; 5 | mod configure_registrar; 6 | 7 | pub use create_voter_weight_record::*; 8 | mod create_voter_weight_record; 9 | 10 | pub use update_voter_weight_record::*; 11 | mod update_voter_weight_record; 12 | -------------------------------------------------------------------------------- /programs/gateway/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | pub mod error; 4 | 5 | mod instructions; 6 | use instructions::*; 7 | 8 | pub mod state; 9 | 10 | pub mod tools; 11 | 12 | declare_id!("GgathUhdrCWRHowoRKACjgWhYHfxCEdBi5ViqYN6HVxk"); 13 | 14 | #[program] 15 | pub mod gateway { 16 | use super::*; 17 | pub fn create_registrar( 18 | ctx: Context, 19 | use_previous_voter_weight_plugin: bool, 20 | ) -> Result<()> { 21 | log_version(); 22 | instructions::create_registrar(ctx, use_previous_voter_weight_plugin) 23 | } 24 | pub fn configure_registrar( 25 | ctx: Context, 26 | use_previous_voter_weight_plugin: bool, 27 | ) -> Result<()> { 28 | log_version(); 29 | instructions::configure_registrar(ctx, use_previous_voter_weight_plugin) 30 | } 31 | pub fn create_voter_weight_record( 32 | ctx: Context, 33 | governing_token_owner: Pubkey, 34 | ) -> Result<()> { 35 | log_version(); 36 | instructions::create_voter_weight_record(ctx, governing_token_owner) 37 | } 38 | pub fn update_voter_weight_record(ctx: Context) -> Result<()> { 39 | log_version(); 40 | instructions::update_voter_weight_record(ctx) 41 | } 42 | } 43 | 44 | fn log_version() { 45 | // TODO: Check if Anchor allows to log it before instruction is deserialized 46 | msg!("VERSION:{:?}", env!("CARGO_PKG_VERSION")); 47 | } 48 | -------------------------------------------------------------------------------- /programs/gateway/src/state/generic_voter_weight.rs: -------------------------------------------------------------------------------- 1 | use crate::state::VoterWeightAction; 2 | use enum_dispatch::enum_dispatch; 3 | use num_traits::FromPrimitive; 4 | use solana_program::pubkey::Pubkey; 5 | use spl_governance::state::token_owner_record::TokenOwnerRecordV2; 6 | 7 | /// A generic trait representing a voter weight, 8 | /// that can be passed as an input into the plugin 9 | #[enum_dispatch] 10 | pub trait GenericVoterWeight { 11 | fn get_governing_token_mint(&self) -> Pubkey; 12 | fn get_governing_token_owner(&self) -> Pubkey; 13 | fn get_realm(&self) -> Pubkey; 14 | fn get_voter_weight(&self) -> u64; 15 | fn get_weight_action(&self) -> Option; 16 | fn get_weight_action_target(&self) -> Option; 17 | fn get_voter_weight_expiry(&self) -> Option; 18 | } 19 | 20 | #[enum_dispatch(GenericVoterWeight)] 21 | pub enum GenericVoterWeightEnum { 22 | VoterWeightRecord(spl_governance_addin_api::voter_weight::VoterWeightRecord), 23 | TokenOwnerRecord(TokenOwnerRecordV2), 24 | } 25 | 26 | // the "official" on-chain voter weight record has a discriminator field 27 | // when a predecessor voter weight is provided, it uses this struct 28 | // We add the GenericVoterWeight trait here to hide this from the rest of the code. 29 | impl GenericVoterWeight for spl_governance_addin_api::voter_weight::VoterWeightRecord { 30 | fn get_governing_token_mint(&self) -> Pubkey { 31 | self.governing_token_mint 32 | } 33 | 34 | fn get_governing_token_owner(&self) -> Pubkey { 35 | self.governing_token_owner 36 | } 37 | 38 | fn get_realm(&self) -> Pubkey { 39 | self.realm 40 | } 41 | 42 | fn get_voter_weight(&self) -> u64 { 43 | self.voter_weight 44 | } 45 | 46 | // The GenericVoterWeight interface expects a crate-defined VoterWeightAction. 47 | // This is identical to spl_governance_addin_api::voter_weight::VoterWeightAction, but added here 48 | // so that Anchor will create the mapping correctly in the IDL. 49 | // This function converts the spl_governance_addin_api::voter_weight::VoterWeightAction to the 50 | // crate-defined VoterWeightAction by mapping the enum values by integer. 51 | // Note - it is imperative that the two enums stay in sync to avoid errors here. 52 | fn get_weight_action(&self) -> Option { 53 | self.weight_action 54 | .clone() 55 | .map(|x| FromPrimitive::from_u32(x as u32).unwrap()) 56 | } 57 | 58 | fn get_weight_action_target(&self) -> Option { 59 | self.weight_action_target 60 | } 61 | 62 | fn get_voter_weight_expiry(&self) -> Option { 63 | self.voter_weight_expiry 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /programs/gateway/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | pub use registrar::*; 2 | pub mod registrar; 3 | 4 | pub use voter_weight_record::*; 5 | pub mod voter_weight_record; 6 | 7 | pub use generic_voter_weight::*; 8 | pub mod generic_voter_weight; 9 | 10 | pub mod token_owner_record; 11 | -------------------------------------------------------------------------------- /programs/gateway/src/state/registrar.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | id, 3 | tools::anchor::{DISCRIMINATOR_SIZE, PUBKEY_SIZE}, 4 | }; 5 | use anchor_lang::prelude::*; 6 | 7 | /// Registrar which stores Civic Pass voting configuration for the given Realm 8 | #[account] 9 | #[derive(Debug, PartialEq)] 10 | pub struct Registrar { 11 | /// spl-governance program the Realm belongs to 12 | pub governance_program_id: Pubkey, 13 | 14 | /// Realm of the Registrar 15 | pub realm: Pubkey, 16 | 17 | /// Governing token mint the Registrar is for 18 | /// It can either be the Community or the Council mint of the Realm 19 | /// When the plugin is used the mint is only used as identity of the governing power (voting population) 20 | /// and the actual token of the mint is not used 21 | pub governing_token_mint: Pubkey, 22 | 23 | /// The Gatekeeper Network represents the "Pass Type" that a 24 | /// user must present. 25 | pub gatekeeper_network: Pubkey, 26 | 27 | /// If the plugin is one in a sequence, this is the previous plugin program ID 28 | /// If set, then update_voter_weight_record will expect a voter_weight_record owned by this program 29 | pub previous_voter_weight_plugin_program_id: Option, 30 | 31 | /// Reserved for future upgrades 32 | pub reserved: [u8; 128], 33 | } 34 | 35 | impl Registrar { 36 | pub fn get_space() -> usize { 37 | DISCRIMINATOR_SIZE + PUBKEY_SIZE * 4 + (PUBKEY_SIZE + 1) + 128 38 | } 39 | } 40 | 41 | /// Returns Registrar PDA seeds 42 | pub fn get_registrar_seeds<'a>( 43 | realm: &'a Pubkey, 44 | governing_token_mint: &'a Pubkey, 45 | ) -> [&'a [u8]; 3] { 46 | [b"registrar", realm.as_ref(), governing_token_mint.as_ref()] 47 | } 48 | 49 | /// Returns Registrar PDA address 50 | pub fn get_registrar_address(realm: &Pubkey, governing_token_mint: &Pubkey) -> Pubkey { 51 | Pubkey::find_program_address(&get_registrar_seeds(realm, governing_token_mint), &id()).0 52 | } 53 | 54 | #[cfg(test)] 55 | mod test { 56 | 57 | use super::*; 58 | 59 | #[test] 60 | fn test_get_space() { 61 | // Arrange 62 | let expected_space = Registrar::get_space(); 63 | 64 | let registrar = Registrar { 65 | governance_program_id: Pubkey::default(), 66 | previous_voter_weight_plugin_program_id: Pubkey::default().into(), 67 | realm: Pubkey::default(), 68 | governing_token_mint: Pubkey::default(), 69 | gatekeeper_network: Pubkey::default(), 70 | reserved: [0; 128], 71 | }; 72 | 73 | // Act 74 | let actual_space = DISCRIMINATOR_SIZE + registrar.try_to_vec().unwrap().len(); 75 | 76 | // Assert 77 | assert_eq!(expected_space, actual_space); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /programs/gateway/src/state/token_owner_record.rs: -------------------------------------------------------------------------------- 1 | use solana_program::pubkey::Pubkey; 2 | // Add the generic voter weight trait to TokenOwnerRecord structs 3 | // so that they can be used as input voter weights into the plugin 4 | use crate::state::generic_voter_weight::GenericVoterWeight; 5 | use crate::state::VoterWeightAction; 6 | use spl_governance::state::token_owner_record::TokenOwnerRecordV2; 7 | 8 | impl GenericVoterWeight for TokenOwnerRecordV2 { 9 | fn get_governing_token_mint(&self) -> Pubkey { 10 | self.governing_token_mint 11 | } 12 | 13 | fn get_governing_token_owner(&self) -> Pubkey { 14 | self.governing_token_owner 15 | } 16 | 17 | fn get_realm(&self) -> Pubkey { 18 | self.realm 19 | } 20 | 21 | fn get_voter_weight(&self) -> u64 { 22 | self.governing_token_deposit_amount 23 | } 24 | 25 | fn get_weight_action(&self) -> Option { 26 | None 27 | } 28 | 29 | fn get_weight_action_target(&self) -> Option { 30 | None 31 | } 32 | 33 | fn get_voter_weight_expiry(&self) -> Option { 34 | None 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /programs/gateway/src/tools/anchor.rs: -------------------------------------------------------------------------------- 1 | pub const DISCRIMINATOR_SIZE: usize = 8; 2 | pub const PUBKEY_SIZE: usize = 32; 3 | -------------------------------------------------------------------------------- /programs/gateway/src/tools/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod anchor; 2 | -------------------------------------------------------------------------------- /programs/gateway/tests/create_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use program_test::gateway_voter_test::GatewayVoterTest; 2 | use program_test::tools::assert_ix_err; 3 | use solana_program::instruction::InstructionError; 4 | use solana_program_test::*; 5 | use solana_sdk::transport::TransportError; 6 | 7 | mod program_test; 8 | 9 | #[tokio::test] 10 | async fn test_create_voter_weight_record() -> Result<(), TransportError> { 11 | // Arrange 12 | let mut gateway_voter_test = GatewayVoterTest::start_new().await; 13 | 14 | let realm_cookie = gateway_voter_test.governance.with_realm().await?; 15 | let gateway_cookie = gateway_voter_test.with_gateway().await?; 16 | 17 | let registrar_cookie = gateway_voter_test 18 | .with_registrar(&realm_cookie, &gateway_cookie, None) 19 | .await?; 20 | 21 | let voter_cookie = gateway_voter_test.bench.with_wallet().await; 22 | 23 | // Act 24 | let voter_weight_record_cookie = gateway_voter_test 25 | .with_voter_weight_record(®istrar_cookie, &voter_cookie) 26 | .await?; 27 | 28 | // Assert 29 | 30 | let voter_weight_record = gateway_voter_test 31 | .get_voter_weight_record(&voter_weight_record_cookie.address) 32 | .await; 33 | 34 | assert_eq!(voter_weight_record_cookie.account, voter_weight_record); 35 | 36 | Ok(()) 37 | } 38 | 39 | #[tokio::test] 40 | async fn test_create_voter_weight_record_with_already_exists_error() -> Result<(), TransportError> { 41 | // Arrange 42 | let mut gateway_voter_test = GatewayVoterTest::start_new().await; 43 | 44 | let realm_cookie = gateway_voter_test.governance.with_realm().await?; 45 | let gateway_cookie = gateway_voter_test.with_gateway().await?; 46 | 47 | let registrar_cookie = gateway_voter_test 48 | .with_registrar(&realm_cookie, &gateway_cookie, None) 49 | .await?; 50 | 51 | let voter_cookie = gateway_voter_test.bench.with_wallet().await; 52 | 53 | gateway_voter_test 54 | .with_voter_weight_record(®istrar_cookie, &voter_cookie) 55 | .await?; 56 | 57 | gateway_voter_test.bench.advance_clock().await; 58 | 59 | // Act 60 | let err = gateway_voter_test 61 | .with_voter_weight_record(®istrar_cookie, &voter_cookie) 62 | .await 63 | .err() 64 | .unwrap(); 65 | 66 | // Assert 67 | 68 | // InstructionError::Custom(0) is returned for TransactionError::AccountInUse 69 | assert_ix_err(err, InstructionError::Custom(0)); 70 | 71 | Ok(()) 72 | } 73 | -------------------------------------------------------------------------------- /programs/gateway/tests/fixtures/solana_gateway.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-labs/governance-program-library/c7a49fa2a663c9bdf1260827c0d9b15790ebf24b/programs/gateway/tests/fixtures/solana_gateway.so -------------------------------------------------------------------------------- /programs/gateway/tests/fixtures/spl_governance.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-labs/governance-program-library/c7a49fa2a663c9bdf1260827c0d9b15790ebf24b/programs/gateway/tests/fixtures/spl_governance.so -------------------------------------------------------------------------------- /programs/gateway/tests/fixtures/spl_governance_addin_mock.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-labs/governance-program-library/c7a49fa2a663c9bdf1260827c0d9b15790ebf24b/programs/gateway/tests/fixtures/spl_governance_addin_mock.so -------------------------------------------------------------------------------- /programs/gateway/tests/program_test/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod gateway_voter_test; 2 | pub mod governance_test; 3 | pub mod predecessor_plugin_test; 4 | pub mod program_test_bench; 5 | pub mod tools; 6 | -------------------------------------------------------------------------------- /programs/gateway/tests/program_test/predecessor_plugin_test.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | use std::sync::Arc; 3 | 4 | use anchor_lang::prelude::Pubkey; 5 | 6 | use gpl_civic_gateway::state::*; 7 | use solana_sdk::{signature::Keypair, signer::Signer, transport::TransportError}; 8 | use spl_governance_addin_mock::instruction::*; 9 | 10 | use crate::program_test::{ 11 | gateway_voter_test::VoterWeightRecordCookie, 12 | governance_test::RealmCookie, 13 | program_test_bench::{ProgramTestBench, WalletCookie}, 14 | }; 15 | use solana_program_test::{processor, ProgramTest}; 16 | 17 | pub struct PredecessorPluginTest { 18 | pub bench: Arc, 19 | } 20 | 21 | impl PredecessorPluginTest { 22 | pub fn program_id() -> Pubkey { 23 | Pubkey::from_str("GovAddinMock1111111111111111111111111111111").unwrap() 24 | } 25 | 26 | #[allow(dead_code)] 27 | pub fn add_program(program_test: &mut ProgramTest) { 28 | program_test.add_program( 29 | "spl_governance_addin_mock", 30 | Self::program_id(), 31 | processor!(spl_governance_addin_mock::processor::process_instruction), 32 | ); 33 | } 34 | 35 | #[allow(dead_code)] 36 | pub fn new(bench: Arc) -> Self { 37 | PredecessorPluginTest { bench } 38 | } 39 | 40 | #[allow(dead_code)] 41 | pub async fn with_voter_weight_record( 42 | &self, 43 | realm_cookie: &RealmCookie, 44 | voter_cookie: &WalletCookie, 45 | voter_weight: u64, 46 | ) -> Result { 47 | let governing_token_owner = voter_cookie.address; 48 | let voter_weight_record_account = Keypair::new(); 49 | 50 | let setup_voter_weight_record_ix = setup_voter_weight_record( 51 | &Self::program_id(), 52 | &realm_cookie.address, 53 | &realm_cookie.account.community_mint, 54 | &voter_cookie.address, 55 | &voter_weight_record_account.pubkey(), 56 | &self.bench.payer.pubkey(), 57 | voter_weight, 58 | Some(0), 59 | None, 60 | None, 61 | ); 62 | 63 | self.bench 64 | .process_transaction( 65 | &[setup_voter_weight_record_ix], 66 | Some(&[&voter_weight_record_account]), 67 | ) 68 | .await?; 69 | 70 | let account = VoterWeightRecord { 71 | realm: realm_cookie.address, 72 | governing_token_mint: realm_cookie.account.community_mint, 73 | governing_token_owner, 74 | voter_weight: 0, 75 | voter_weight_expiry: Some(0), 76 | weight_action: None, 77 | weight_action_target: None, 78 | reserved: [0; 8], 79 | }; 80 | 81 | Ok(VoterWeightRecordCookie { 82 | address: voter_weight_record_account.pubkey(), 83 | account, 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /programs/gateway/tests/program_test/tools.rs: -------------------------------------------------------------------------------- 1 | use crate::{ 2 | program_test::gateway_voter_test::VoterWeightRecordCookie, 3 | program_test::governance_test::TokenOwnerRecordCookie, 4 | }; 5 | use anchor_lang::prelude::ERROR_CODE_OFFSET; 6 | use gpl_civic_gateway::error::GatewayError; 7 | use itertools::Either; 8 | use solana_program::{instruction::InstructionError, pubkey::Pubkey}; 9 | use solana_program_test::BanksClientError; 10 | use solana_sdk::{signature::Keypair, transaction::TransactionError, transport::TransportError}; 11 | use spl_governance_tools::error::GovernanceToolsError; 12 | 13 | pub fn clone_keypair(source: &Keypair) -> Keypair { 14 | Keypair::from_bytes(&source.to_bytes()).unwrap() 15 | } 16 | 17 | /// NOP (No Operation) Override function 18 | #[allow(non_snake_case)] 19 | pub fn NopOverride(_: &mut T) {} 20 | 21 | #[allow(dead_code)] 22 | pub fn assert_gateway_err(banks_client_error: BanksClientError, gateway_error: GatewayError) { 23 | let tx_error = banks_client_error.unwrap(); 24 | 25 | match tx_error { 26 | TransactionError::InstructionError(_, instruction_error) => match instruction_error { 27 | InstructionError::Custom(e) => { 28 | assert_eq!(e, gateway_error as u32 + ERROR_CODE_OFFSET) 29 | } 30 | _ => panic!("{:?} Is not InstructionError::Custom()", instruction_error), 31 | }, 32 | _ => panic!("{:?} Is not InstructionError", tx_error), 33 | }; 34 | } 35 | 36 | #[allow(dead_code)] 37 | pub fn assert_gov_tools_err( 38 | banks_client_error: TransportError, 39 | gov_tools_error: GovernanceToolsError, 40 | ) { 41 | let tx_error = banks_client_error.unwrap(); 42 | 43 | match tx_error { 44 | TransactionError::InstructionError(_, instruction_error) => match instruction_error { 45 | InstructionError::Custom(e) => { 46 | assert_eq!(e, gov_tools_error as u32) 47 | } 48 | _ => panic!("{:?} Is not InstructionError::Custom()", instruction_error), 49 | }, 50 | _ => panic!("{:?} Is not InstructionError", tx_error), 51 | }; 52 | } 53 | 54 | #[allow(dead_code)] 55 | pub fn assert_anchor_err( 56 | banks_client_error: BanksClientError, 57 | anchor_error: anchor_lang::error::ErrorCode, 58 | ) { 59 | let tx_error = banks_client_error.unwrap(); 60 | 61 | match tx_error { 62 | TransactionError::InstructionError(_, instruction_error) => match instruction_error { 63 | InstructionError::Custom(e) => { 64 | assert_eq!(e, anchor_error as u32) 65 | } 66 | _ => panic!("{:?} Is not InstructionError::Custom()", instruction_error), 67 | }, 68 | _ => panic!("{:?} Is not InstructionError", tx_error), 69 | }; 70 | } 71 | 72 | #[allow(dead_code)] 73 | pub fn assert_ix_err(banks_client_error: BanksClientError, ix_error: InstructionError) { 74 | let tx_error = banks_client_error.unwrap(); 75 | 76 | match tx_error { 77 | TransactionError::InstructionError(_, instruction_error) => { 78 | assert_eq!(instruction_error, ix_error); 79 | } 80 | _ => panic!("{:?} Is not InstructionError", tx_error), 81 | }; 82 | } 83 | 84 | pub fn extract_voting_weight_address( 85 | account: &Either<&VoterWeightRecordCookie, &TokenOwnerRecordCookie>, 86 | ) -> Pubkey { 87 | account 88 | .map_left(|cookie| cookie.address) 89 | .map_right(|cookie| cookie.address) 90 | .into_inner() 91 | } 92 | -------------------------------------------------------------------------------- /programs/nft-voter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gpl-nft-voter" 3 | version = "0.2.2" 4 | description = "SPL Governance addin implementing NFT based governance" 5 | license = "Apache-2.0" 6 | edition = "2018" 7 | 8 | [lib] 9 | crate-type = ["cdylib", "lib"] 10 | name = "gpl_nft_voter" 11 | 12 | [features] 13 | no-entrypoint = [] 14 | no-idl = [] 15 | no-log-ix-name = [] 16 | cpi = ["no-entrypoint"] 17 | default = [] 18 | idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] 19 | 20 | [dependencies] 21 | arrayref = "0.3.6" 22 | anchor-lang = { version = "0.30.1", features = ["init-if-needed"] } 23 | anchor-spl = { version = "0.30.1", features = ["token"] } 24 | itertools = "0.10.2" 25 | mpl-token-metadata = "^4.1.2" 26 | solana-program = "1.18.18" 27 | spl-governance = { version = "4.0", features = ["no-entrypoint"] } 28 | spl-governance-tools = "0.1.4" 29 | spl-token = { version = "4.0", features = [ "no-entrypoint" ] } 30 | 31 | [dev-dependencies] 32 | borsh = "0.10.3" 33 | solana-sdk = "1.18.18" 34 | solana-program-test = "1.18.18" 35 | -------------------------------------------------------------------------------- /programs/nft-voter/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /programs/nft-voter/src/error.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum NftVoterError { 5 | #[msg("Invalid Realm Authority")] 6 | InvalidRealmAuthority, 7 | 8 | #[msg("Invalid Realm for Registrar")] 9 | InvalidRealmForRegistrar, 10 | 11 | #[msg("Invalid Collection Size")] 12 | InvalidCollectionSize, 13 | 14 | #[msg("Invalid MaxVoterWeightRecord Realm")] 15 | InvalidMaxVoterWeightRecordRealm, 16 | 17 | #[msg("Invalid MaxVoterWeightRecord Mint")] 18 | InvalidMaxVoterWeightRecordMint, 19 | 20 | #[msg("CastVote Is Not Allowed")] 21 | CastVoteIsNotAllowed, 22 | 23 | #[msg("Invalid VoterWeightRecord Realm")] 24 | InvalidVoterWeightRecordRealm, 25 | 26 | #[msg("Invalid VoterWeightRecord Mint")] 27 | InvalidVoterWeightRecordMint, 28 | 29 | #[msg("Invalid TokenOwner for VoterWeightRecord")] 30 | InvalidTokenOwnerForVoterWeightRecord, 31 | 32 | #[msg("Collection must be verified")] 33 | CollectionMustBeVerified, 34 | 35 | #[msg("Voter does not own NFT")] 36 | VoterDoesNotOwnNft, 37 | 38 | #[msg("Collection not found")] 39 | CollectionNotFound, 40 | 41 | #[msg("Missing Metadata collection")] 42 | MissingMetadataCollection, 43 | 44 | #[msg("Token Metadata doesn't match")] 45 | TokenMetadataDoesNotMatch, 46 | 47 | #[msg("Invalid account owner")] 48 | InvalidAccountOwner, 49 | 50 | #[msg("Invalid token metadata account")] 51 | InvalidTokenMetadataAccount, 52 | 53 | #[msg("Duplicated NFT detected")] 54 | DuplicatedNftDetected, 55 | 56 | #[msg("Invalid NFT amount")] 57 | InvalidNftAmount, 58 | 59 | #[msg("NFT already voted")] 60 | NftAlreadyVoted, 61 | 62 | #[msg("Invalid Proposal for NftVoteRecord")] 63 | InvalidProposalForNftVoteRecord, 64 | 65 | #[msg("Invalid TokenOwner for NftVoteRecord")] 66 | InvalidTokenOwnerForNftVoteRecord, 67 | 68 | #[msg("VoteRecord must be withdrawn")] 69 | VoteRecordMustBeWithdrawn, 70 | 71 | #[msg("Invalid VoteRecord for NftVoteRecord")] 72 | InvalidVoteRecordForNftVoteRecord, 73 | 74 | #[msg("VoterWeightRecord must be expired")] 75 | VoterWeightRecordMustBeExpired, 76 | } 77 | -------------------------------------------------------------------------------- /programs/nft-voter/src/instructions/create_max_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use anchor_spl::token::Mint; 3 | use spl_governance::state::realm; 4 | 5 | use crate::state::max_voter_weight_record::MaxVoterWeightRecord; 6 | 7 | /// Creates MaxVoterWeightRecord used by spl-gov 8 | /// This instruction should only be executed once per realm/governing_token_mint to create the account 9 | #[derive(Accounts)] 10 | pub struct CreateMaxVoterWeightRecord<'info> { 11 | #[account( 12 | init, 13 | seeds = [ b"max-voter-weight-record".as_ref(), 14 | realm.key().as_ref(), 15 | realm_governing_token_mint.key().as_ref()], 16 | bump, 17 | payer = payer, 18 | space = MaxVoterWeightRecord::get_space() 19 | )] 20 | pub max_voter_weight_record: Account<'info, MaxVoterWeightRecord>, 21 | 22 | /// The program id of the spl-governance program the realm belongs to 23 | /// CHECK: Can be any instance of spl-governance and it's not known at the compilation time 24 | #[account(executable)] 25 | pub governance_program_id: UncheckedAccount<'info>, 26 | 27 | #[account(owner = governance_program_id.key())] 28 | /// CHECK: Owned by spl-governance instance specified in governance_program_id 29 | pub realm: UncheckedAccount<'info>, 30 | 31 | /// Either the realm community mint or the council mint. 32 | pub realm_governing_token_mint: Account<'info, Mint>, 33 | 34 | #[account(mut)] 35 | pub payer: Signer<'info>, 36 | 37 | pub system_program: Program<'info, System>, 38 | } 39 | 40 | pub fn create_max_voter_weight_record(ctx: Context) -> Result<()> { 41 | // Deserialize the Realm to validate it 42 | let _realm = realm::get_realm_data_for_governing_token_mint( 43 | &ctx.accounts.governance_program_id.key(), 44 | &ctx.accounts.realm, 45 | &ctx.accounts.realm_governing_token_mint.key(), 46 | )?; 47 | 48 | let max_voter_weight_record = &mut ctx.accounts.max_voter_weight_record; 49 | 50 | max_voter_weight_record.realm = ctx.accounts.realm.key(); 51 | max_voter_weight_record.governing_token_mint = ctx.accounts.realm_governing_token_mint.key(); 52 | 53 | // Set expiry to expired 54 | max_voter_weight_record.max_voter_weight_expiry = Some(0); 55 | 56 | Ok(()) 57 | } 58 | -------------------------------------------------------------------------------- /programs/nft-voter/src/instructions/create_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::state::*; 2 | use anchor_lang::prelude::*; 3 | use anchor_spl::token::Mint; 4 | use spl_governance::state::realm; 5 | 6 | /// Creates VoterWeightRecord used by spl-gov 7 | /// This instruction should only be executed once per realm/governing_token_mint/governing_token_owner 8 | /// to create the account 9 | #[derive(Accounts)] 10 | #[instruction(governing_token_owner: Pubkey)] 11 | pub struct CreateVoterWeightRecord<'info> { 12 | #[account( 13 | init, 14 | seeds = [ b"voter-weight-record".as_ref(), 15 | realm.key().as_ref(), 16 | realm_governing_token_mint.key().as_ref(), 17 | governing_token_owner.as_ref()], 18 | bump, 19 | payer = payer, 20 | space = VoterWeightRecord::get_space() 21 | )] 22 | pub voter_weight_record: Account<'info, VoterWeightRecord>, 23 | 24 | /// The program id of the spl-governance program the realm belongs to 25 | /// CHECK: Can be any instance of spl-governance and it's not known at the compilation time 26 | #[account(executable)] 27 | pub governance_program_id: UncheckedAccount<'info>, 28 | 29 | /// CHECK: Owned by spl-governance instance specified in governance_program_id 30 | #[account(owner = governance_program_id.key())] 31 | pub realm: UncheckedAccount<'info>, 32 | 33 | /// Either the realm community mint or the council mint. 34 | pub realm_governing_token_mint: Account<'info, Mint>, 35 | 36 | #[account(mut)] 37 | pub payer: Signer<'info>, 38 | 39 | pub system_program: Program<'info, System>, 40 | } 41 | 42 | pub fn create_voter_weight_record( 43 | ctx: Context, 44 | governing_token_owner: Pubkey, 45 | ) -> Result<()> { 46 | // Deserialize the Realm to validate it 47 | let _realm = realm::get_realm_data_for_governing_token_mint( 48 | &ctx.accounts.governance_program_id.key(), 49 | &ctx.accounts.realm, 50 | &ctx.accounts.realm_governing_token_mint.key(), 51 | )?; 52 | 53 | let voter_weight_record = &mut ctx.accounts.voter_weight_record; 54 | 55 | voter_weight_record.realm = ctx.accounts.realm.key(); 56 | voter_weight_record.governing_token_mint = ctx.accounts.realm_governing_token_mint.key(); 57 | voter_weight_record.governing_token_owner = governing_token_owner; 58 | 59 | // Set expiry to expired 60 | voter_weight_record.voter_weight_expiry = Some(0); 61 | 62 | Ok(()) 63 | } 64 | -------------------------------------------------------------------------------- /programs/nft-voter/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub use configure_collection::*; 2 | mod configure_collection; 3 | 4 | pub use create_registrar::*; 5 | mod create_registrar; 6 | 7 | pub use create_voter_weight_record::*; 8 | mod create_voter_weight_record; 9 | 10 | pub use create_max_voter_weight_record::*; 11 | mod create_max_voter_weight_record; 12 | 13 | pub use update_voter_weight_record::*; 14 | mod update_voter_weight_record; 15 | 16 | pub use relinquish_nft_vote::*; 17 | mod relinquish_nft_vote; 18 | 19 | pub use cast_nft_vote::*; 20 | mod cast_nft_vote; 21 | -------------------------------------------------------------------------------- /programs/nft-voter/src/instructions/update_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::error::NftVoterError; 2 | use crate::state::*; 3 | use anchor_lang::prelude::*; 4 | use itertools::Itertools; 5 | 6 | /// Updates VoterWeightRecord to evaluate governance power for non voting use cases: CreateProposal, CreateGovernance etc... 7 | /// This instruction updates VoterWeightRecord which is valid for the current Slot and the given target action only 8 | /// and hance the instruction has to be executed inside the same transaction as the corresponding spl-gov instruction 9 | /// 10 | /// Note: UpdateVoterWeight is not cumulative the same way as CastNftVote and hence voter_weight for non voting scenarios 11 | /// can only be used with max 5 NFTs due to Solana transaction size limit 12 | /// It could be supported in future version by introducing bookkeeping accounts to track the NFTs 13 | /// which were already used to calculate the total weight 14 | #[derive(Accounts)] 15 | #[instruction(voter_weight_action:VoterWeightAction)] 16 | pub struct UpdateVoterWeightRecord<'info> { 17 | /// The NFT voting Registrar 18 | pub registrar: Account<'info, Registrar>, 19 | 20 | #[account( 21 | mut, 22 | constraint = voter_weight_record.realm == registrar.realm 23 | @ NftVoterError::InvalidVoterWeightRecordRealm, 24 | 25 | constraint = voter_weight_record.governing_token_mint == registrar.governing_token_mint 26 | @ NftVoterError::InvalidVoterWeightRecordMint, 27 | )] 28 | pub voter_weight_record: Account<'info, VoterWeightRecord>, 29 | } 30 | 31 | pub fn update_voter_weight_record( 32 | ctx: Context, 33 | voter_weight_action: VoterWeightAction, 34 | ) -> Result<()> { 35 | let registrar = &ctx.accounts.registrar; 36 | let governing_token_owner = &ctx.accounts.voter_weight_record.governing_token_owner; 37 | 38 | match voter_weight_action { 39 | // voter_weight for CastVote action can't be evaluated using this instruction 40 | VoterWeightAction::CastVote => return err!(NftVoterError::CastVoteIsNotAllowed), 41 | VoterWeightAction::CommentProposal 42 | | VoterWeightAction::CreateGovernance 43 | | VoterWeightAction::CreateProposal 44 | | VoterWeightAction::SignOffProposal => {} 45 | } 46 | 47 | let mut voter_weight = 0u64; 48 | 49 | // Ensure all nfts are unique 50 | let mut unique_nft_mints = vec![]; 51 | 52 | for (nft_info, nft_metadata_info) in ctx.remaining_accounts.iter().tuples() { 53 | let (nft_vote_weight, _) = resolve_nft_vote_weight_and_mint( 54 | registrar, 55 | governing_token_owner, 56 | nft_info, 57 | nft_metadata_info, 58 | &mut unique_nft_mints, 59 | )?; 60 | 61 | voter_weight = voter_weight.checked_add(nft_vote_weight).unwrap(); 62 | } 63 | 64 | let voter_weight_record = &mut ctx.accounts.voter_weight_record; 65 | 66 | voter_weight_record.voter_weight = voter_weight; 67 | 68 | // Record is only valid as of the current slot 69 | voter_weight_record.voter_weight_expiry = Some(Clock::get()?.slot); 70 | 71 | // Set the action to make it specific and prevent being used for voting 72 | voter_weight_record.weight_action = Some(voter_weight_action); 73 | voter_weight_record.weight_action_target = None; 74 | 75 | Ok(()) 76 | } 77 | -------------------------------------------------------------------------------- /programs/nft-voter/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | pub mod error; 4 | 5 | mod instructions; 6 | use instructions::*; 7 | 8 | pub mod state; 9 | 10 | pub mod tools; 11 | 12 | use crate::state::*; 13 | 14 | declare_id!("GnftV5kLjd67tvHpNGyodwWveEKivz3ZWvvE3Z4xi2iw"); 15 | 16 | #[program] 17 | pub mod nft_voter { 18 | 19 | use crate::state::VoterWeightAction; 20 | 21 | use super::*; 22 | pub fn create_registrar(ctx: Context, max_collections: u8) -> Result<()> { 23 | log_version(); 24 | instructions::create_registrar(ctx, max_collections) 25 | } 26 | pub fn create_voter_weight_record( 27 | ctx: Context, 28 | governing_token_owner: Pubkey, 29 | ) -> Result<()> { 30 | log_version(); 31 | instructions::create_voter_weight_record(ctx, governing_token_owner) 32 | } 33 | pub fn create_max_voter_weight_record(ctx: Context) -> Result<()> { 34 | log_version(); 35 | instructions::create_max_voter_weight_record(ctx) 36 | } 37 | pub fn update_voter_weight_record( 38 | ctx: Context, 39 | voter_weight_action: VoterWeightAction, 40 | ) -> Result<()> { 41 | log_version(); 42 | instructions::update_voter_weight_record(ctx, voter_weight_action) 43 | } 44 | pub fn relinquish_nft_vote(ctx: Context) -> Result<()> { 45 | log_version(); 46 | instructions::relinquish_nft_vote(ctx) 47 | } 48 | pub fn configure_collection( 49 | ctx: Context, 50 | weight: u64, 51 | size: u32, 52 | ) -> Result<()> { 53 | log_version(); 54 | instructions::configure_collection(ctx, weight, size) 55 | } 56 | 57 | pub fn cast_nft_vote<'info>( 58 | ctx: Context<'_, '_, '_, 'info, CastNftVote<'info>>, 59 | proposal: Pubkey, 60 | ) -> Result<()> { 61 | log_version(); 62 | instructions::cast_nft_vote(ctx, proposal) 63 | } 64 | } 65 | 66 | fn log_version() { 67 | // TODO: Check if Anchor allows to log it before instruction is deserialized 68 | msg!("VERSION:{:?}", env!("CARGO_PKG_VERSION")); 69 | } 70 | -------------------------------------------------------------------------------- /programs/nft-voter/src/state/collection_config.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | /// Configuration of an NFT collection used for governance power 4 | #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy, PartialEq, Default)] 5 | pub struct CollectionConfig { 6 | /// The NFT collection used for governance 7 | pub collection: Pubkey, 8 | 9 | /// The size of the NFT collection used to calculate max voter weight 10 | /// Note: At the moment the size is not captured on Metaplex accounts 11 | /// and it has to be manually updated on the Registrar 12 | pub size: u32, 13 | 14 | /// Governance power weight of the collection 15 | /// Each NFT in the collection has governance power = 1 * weight 16 | /// Note: The weight is scaled accordingly to the governing_token_mint decimals 17 | /// Ex: if the the mint has 2 decimal places then weight of 1 should be stored as 100 18 | pub weight: u64, 19 | 20 | /// Reserved for future upgrades 21 | pub reserved: [u8; 8], 22 | } 23 | 24 | impl CollectionConfig { 25 | pub fn get_max_weight(&self) -> u64 { 26 | (self.size as u64).checked_mul(self.weight).unwrap() 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /programs/nft-voter/src/state/idl_types.rs: -------------------------------------------------------------------------------- 1 | //! IDL only types which are required in IDL but not exported automatically by Anchor 2 | use anchor_lang::prelude::*; 3 | 4 | /// NftVoteRecord exported to IDL without account_discriminator 5 | /// TODO: Once we can support these accounts in Anchor via remaining_accounts then it should be possible to remove it 6 | #[account] 7 | pub struct NftVoteRecord { 8 | /// Proposal which was voted on 9 | pub proposal: Pubkey, 10 | 11 | /// The mint of the NFT which was used for the vote 12 | pub nft_mint: Pubkey, 13 | 14 | /// The voter who casted this vote 15 | /// It's a Realm member pubkey corresponding to TokenOwnerRecord.governing_token_owner 16 | pub governing_token_owner: Pubkey, 17 | } 18 | -------------------------------------------------------------------------------- /programs/nft-voter/src/state/max_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::id; 2 | use crate::tools::anchor::DISCRIMINATOR_SIZE; 3 | use anchor_lang::prelude::Pubkey; 4 | use anchor_lang::prelude::*; 5 | use solana_program::pubkey::PUBKEY_BYTES; 6 | 7 | /// MaxVoterWeightRecord account as defined in spl-governance-addin-api 8 | /// It's redefined here without account_discriminator for Anchor to treat it as native account 9 | /// 10 | /// The account is used as an api interface to provide max voting power to the governance program from external addin contracts 11 | #[account] 12 | #[derive(Debug, PartialEq)] 13 | pub struct MaxVoterWeightRecord { 14 | /// The Realm the MaxVoterWeightRecord belongs to 15 | pub realm: Pubkey, 16 | 17 | /// Governing Token Mint the MaxVoterWeightRecord is associated with 18 | /// Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only 19 | // The mint here is to link the record to either community or council mint of the realm 20 | pub governing_token_mint: Pubkey, 21 | 22 | /// Max voter weight 23 | /// The max voter weight provided by the addin for the given realm and governing_token_mint 24 | pub max_voter_weight: u64, 25 | 26 | /// The slot when the max voting weight expires 27 | /// It should be set to None if the weight never expires 28 | /// If the max vote weight decays with time, for example for time locked based weights, then the expiry must be set 29 | /// As a pattern Revise instruction to update the max weight should be invoked before governance instruction within the same transaction 30 | /// and the expiry set to the current slot to provide up to date weight 31 | pub max_voter_weight_expiry: Option, 32 | 33 | /// Reserved space for future versions 34 | pub reserved: [u8; 8], 35 | } 36 | 37 | impl Default for MaxVoterWeightRecord { 38 | fn default() -> Self { 39 | Self { 40 | realm: Default::default(), 41 | governing_token_mint: Default::default(), 42 | max_voter_weight: Default::default(), 43 | max_voter_weight_expiry: Some(0), 44 | reserved: Default::default(), 45 | } 46 | } 47 | } 48 | 49 | impl MaxVoterWeightRecord { 50 | pub fn get_space() -> usize { 51 | DISCRIMINATOR_SIZE + PUBKEY_BYTES * 2 + 8 + 1 + 8 + 8 52 | } 53 | } 54 | 55 | /// Returns MaxVoterWeightRecord PDA seeds 56 | pub fn get_max_voter_weight_record_seeds<'a>( 57 | realm: &'a Pubkey, 58 | governing_token_mint: &'a Pubkey, 59 | ) -> [&'a [u8]; 3] { 60 | [ 61 | b"max-voter-weight-record", 62 | realm.as_ref(), 63 | governing_token_mint.as_ref(), 64 | ] 65 | } 66 | 67 | /// Returns MaxVoterWeightRecord PDA address 68 | pub fn get_max_voter_weight_record_address( 69 | realm: &Pubkey, 70 | governing_token_mint: &Pubkey, 71 | ) -> Pubkey { 72 | Pubkey::find_program_address( 73 | &get_max_voter_weight_record_seeds(realm, governing_token_mint), 74 | &id(), 75 | ) 76 | .0 77 | } 78 | 79 | #[cfg(test)] 80 | mod test { 81 | 82 | use super::*; 83 | 84 | #[test] 85 | fn test_get_space() { 86 | // Arrange 87 | let expected_space = MaxVoterWeightRecord::get_space(); 88 | 89 | // Act 90 | let actual_space = 91 | DISCRIMINATOR_SIZE + MaxVoterWeightRecord::default().try_to_vec().unwrap().len(); 92 | 93 | // Assert 94 | assert_eq!(expected_space, actual_space); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /programs/nft-voter/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | pub use registrar::*; 2 | pub mod registrar; 3 | 4 | pub use collection_config::*; 5 | pub mod collection_config; 6 | 7 | pub use nft_vote_record::*; 8 | pub mod nft_vote_record; 9 | 10 | pub mod max_voter_weight_record; 11 | 12 | pub use voter_weight_record::*; 13 | pub mod voter_weight_record; 14 | 15 | pub mod idl_types; 16 | -------------------------------------------------------------------------------- /programs/nft-voter/src/state/nft_vote_record.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; 3 | use solana_program::program_pack::IsInitialized; 4 | 5 | use spl_governance_tools::account::{get_account_data, AccountMaxSize}; 6 | 7 | use crate::{error::NftVoterError, id}; 8 | 9 | /// Vote record indicating the given NFT voted on the Proposal 10 | /// The PDA of the record is ["nft-vote-record",proposal,nft_mint] 11 | /// It guarantees uniques and ensures the same NFT can't vote twice 12 | #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] 13 | pub struct NftVoteRecord { 14 | /// NftVoteRecord discriminator sha256("account:NftVoteRecord")[..8] 15 | /// Note: The discriminator is used explicitly because NftVoteRecords 16 | /// are created and consumed dynamically using remaining_accounts 17 | /// and Anchor doesn't really support this scenario without going through lots of hoops 18 | /// Once Anchor has better support for the scenario it shouldn't be necessary 19 | pub account_discriminator: [u8; 8], 20 | 21 | /// Proposal which was voted on 22 | pub proposal: Pubkey, 23 | 24 | /// The mint of the NFT which was used for the vote 25 | pub nft_mint: Pubkey, 26 | 27 | /// The voter who casted this vote 28 | /// It's a Realm member pubkey corresponding to TokenOwnerRecord.governing_token_owner 29 | pub governing_token_owner: Pubkey, 30 | 31 | /// Reserved for future upgrades 32 | pub reserved: [u8; 8], 33 | } 34 | 35 | impl NftVoteRecord { 36 | /// sha256("account:NftVoteRecord")[..8] 37 | pub const ACCOUNT_DISCRIMINATOR: [u8; 8] = [137, 6, 55, 139, 251, 126, 254, 99]; 38 | } 39 | 40 | impl AccountMaxSize for NftVoteRecord {} 41 | 42 | impl IsInitialized for NftVoteRecord { 43 | fn is_initialized(&self) -> bool { 44 | self.account_discriminator == NftVoteRecord::ACCOUNT_DISCRIMINATOR 45 | } 46 | } 47 | 48 | /// Returns NftVoteRecord PDA seeds 49 | pub fn get_nft_vote_record_seeds<'a>(proposal: &'a Pubkey, nft_mint: &'a Pubkey) -> [&'a [u8]; 3] { 50 | [b"nft-vote-record", proposal.as_ref(), nft_mint.as_ref()] 51 | } 52 | 53 | /// Returns NftVoteRecord PDA address 54 | pub fn get_nft_vote_record_address(proposal: &Pubkey, nft_mint: &Pubkey) -> Pubkey { 55 | Pubkey::find_program_address(&get_nft_vote_record_seeds(proposal, nft_mint), &id()).0 56 | } 57 | 58 | /// Deserializes account and checks owner program 59 | pub fn get_nft_vote_record_data(nft_vote_record_info: &AccountInfo) -> Result { 60 | Ok(get_account_data::( 61 | &id(), 62 | nft_vote_record_info, 63 | )?) 64 | } 65 | 66 | pub fn get_nft_vote_record_data_for_proposal_and_token_owner( 67 | nft_vote_record_info: &AccountInfo, 68 | proposal: &Pubkey, 69 | governing_token_owner: &Pubkey, 70 | ) -> Result { 71 | let nft_vote_record = get_nft_vote_record_data(nft_vote_record_info)?; 72 | 73 | require!( 74 | nft_vote_record.proposal == *proposal, 75 | NftVoterError::InvalidProposalForNftVoteRecord 76 | ); 77 | 78 | require!( 79 | nft_vote_record.governing_token_owner == *governing_token_owner, 80 | NftVoterError::InvalidTokenOwnerForNftVoteRecord 81 | ); 82 | 83 | Ok(nft_vote_record) 84 | } 85 | -------------------------------------------------------------------------------- /programs/nft-voter/src/tools/anchor.rs: -------------------------------------------------------------------------------- 1 | pub const DISCRIMINATOR_SIZE: usize = 8; 2 | -------------------------------------------------------------------------------- /programs/nft-voter/src/tools/governance.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::Pubkey; 2 | use spl_governance::state::{token_owner_record, vote_record}; 3 | 4 | pub fn get_vote_record_address( 5 | program_id: &Pubkey, 6 | realm: &Pubkey, 7 | governing_token_mint: &Pubkey, 8 | governing_token_owner: &Pubkey, 9 | proposal: &Pubkey, 10 | ) -> Pubkey { 11 | let token_owner_record_key = token_owner_record::get_token_owner_record_address( 12 | program_id, 13 | realm, 14 | governing_token_mint, 15 | governing_token_owner, 16 | ); 17 | 18 | vote_record::get_vote_record_address(program_id, proposal, &token_owner_record_key) 19 | } 20 | -------------------------------------------------------------------------------- /programs/nft-voter/src/tools/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod anchor; 2 | pub mod governance; 3 | pub mod spl_token; 4 | pub mod token_metadata; 5 | -------------------------------------------------------------------------------- /programs/nft-voter/src/tools/spl_token.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use arrayref::array_ref; 3 | use spl_governance::tools::spl_token::assert_is_valid_spl_token_account; 4 | 5 | /// Computationally cheap method to get amount from a token account 6 | /// It reads amount without deserializing full account data 7 | pub fn get_spl_token_amount(token_account_info: &AccountInfo) -> Result { 8 | assert_is_valid_spl_token_account(token_account_info)?; 9 | 10 | // TokeAccount layout: mint(32), owner(32), amount(8), ... 11 | let data = token_account_info.try_borrow_data()?; 12 | let amount_bytes = array_ref![data, 64, 8]; 13 | 14 | Ok(u64::from_le_bytes(*amount_bytes)) 15 | } 16 | -------------------------------------------------------------------------------- /programs/nft-voter/src/tools/token_metadata.rs: -------------------------------------------------------------------------------- 1 | use std::convert::TryFrom; 2 | 3 | use anchor_lang::prelude::*; 4 | use mpl_token_metadata::accounts::Metadata; 5 | 6 | use crate::error::NftVoterError; 7 | 8 | pub fn get_token_metadata(account_info: &AccountInfo) -> Result { 9 | if *account_info.owner != mpl_token_metadata::ID { 10 | return Err(NftVoterError::InvalidAccountOwner.into()); 11 | } 12 | 13 | let metadata = Metadata::try_from(account_info)?; 14 | 15 | // I'm not sure if this is needed but try_from_slice_checked in from_account_info 16 | // ignores Key::Uninitialized and hence checking for the exact Key match here 17 | if metadata.key != mpl_token_metadata::types::Key::MetadataV1 { 18 | return Err(NftVoterError::InvalidTokenMetadataAccount.into()); 19 | } 20 | 21 | Ok(metadata) 22 | } 23 | 24 | pub fn get_token_metadata_for_mint(account_info: &AccountInfo, mint: &Pubkey) -> Result { 25 | let token_metadata = get_token_metadata(account_info)?; 26 | 27 | if token_metadata.mint != *mint { 28 | return Err(NftVoterError::TokenMetadataDoesNotMatch.into()); 29 | } 30 | 31 | Ok(token_metadata) 32 | } 33 | -------------------------------------------------------------------------------- /programs/nft-voter/tests/fixtures/mpl_token_metadata.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-labs/governance-program-library/c7a49fa2a663c9bdf1260827c0d9b15790ebf24b/programs/nft-voter/tests/fixtures/mpl_token_metadata.so -------------------------------------------------------------------------------- /programs/nft-voter/tests/fixtures/spl_governance.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-labs/governance-program-library/c7a49fa2a663c9bdf1260827c0d9b15790ebf24b/programs/nft-voter/tests/fixtures/spl_governance.so -------------------------------------------------------------------------------- /programs/nft-voter/tests/program_test/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod governance_test; 2 | pub mod nft_voter_test; 3 | pub mod program_test_bench; 4 | pub mod token_metadata_test; 5 | pub mod tools; 6 | -------------------------------------------------------------------------------- /programs/quadratic/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gpl-quadratic" 3 | version = "0.1.1" 4 | description = "SPL Governance quadratic voting plugin" 5 | license = "Apache-2.0" 6 | edition = "2018" 7 | 8 | [lib] 9 | crate-type = ["cdylib", "lib"] 10 | name = "gpl_quadratic" 11 | 12 | [features] 13 | no-entrypoint = [] 14 | no-idl = [] 15 | no-log-ix-name = [] 16 | cpi = ["no-entrypoint"] 17 | default = [] 18 | idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] 19 | 20 | [dependencies] 21 | arrayref = "0.3.6" 22 | anchor-lang = { version = "0.30.1", features = ["init-if-needed"] } 23 | anchor-spl = "0.30.1" 24 | enum_dispatch = "0.3.8" 25 | gpl-shared = { path = "../shared", features = ["no-entrypoint"] } 26 | itertools = "0.12.0" 27 | num = "0.4" 28 | num-derive = "0.4.1" 29 | num-traits = "0.2" 30 | solana-program = "1.18.18" 31 | spl-governance = { version = "4.0", features = ["no-entrypoint"] } 32 | spl-governance-tools= "0.1.4" 33 | spl-governance-addin-api = "0.1.4" 34 | spl-token = { version = "4.0.0", features = [ "no-entrypoint" ] } 35 | 36 | [dev-dependencies] 37 | borsh = "0.10.3" 38 | solana-sdk = "1.18.18" 39 | solana-program-test = "1.18.18" 40 | spl-governance-addin-mock = "0.1.4" -------------------------------------------------------------------------------- /programs/quadratic/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /programs/quadratic/src/error.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum QuadraticError { 5 | #[msg("Invalid realm authority")] 6 | InvalidRealmAuthority, 7 | 8 | #[msg("Invalid realm for the provided registrar")] 9 | InvalidRealmForRegistrar, 10 | 11 | #[msg("Invalid TokenOwnerRecord as input voter weight (expecting TokenOwnerRecord V1 or V2)")] 12 | InvalidPredecessorTokenOwnerRecord, 13 | 14 | #[msg("Invalid VoterWeightRecord as input voter weight (expecting VoterWeightRecord)")] 15 | InvalidPredecessorVoterWeightRecord, 16 | 17 | #[msg("Invalid VoterWeightRecord realm for input voter weight")] 18 | InvalidPredecessorVoterWeightRecordRealm, 19 | 20 | #[msg("Invalid VoterWeightRecord governance token mint for input voter weight")] 21 | InvalidPredecessorVoterWeightRecordGovTokenMint, 22 | 23 | #[msg("Invalid VoterWeightRecord governance token owner for input voter weight")] 24 | InvalidPredecessorVoterWeightRecordGovTokenOwner, 25 | 26 | #[msg("Invalid VoterWeightRecord realm")] 27 | InvalidVoterWeightRecordRealm, 28 | 29 | #[msg("Invalid VoterWeightRecord mint")] 30 | InvalidVoterWeightRecordMint, 31 | 32 | #[msg("Previous voter weight plugin required but not provided")] 33 | MissingPreviousVoterWeightPlugin, 34 | } 35 | -------------------------------------------------------------------------------- /programs/quadratic/src/instructions/configure_registrar.rs: -------------------------------------------------------------------------------- 1 | use crate::error::QuadraticError; 2 | use crate::state::quadratic_coefficients::QuadraticCoefficients; 3 | use crate::state::*; 4 | use anchor_lang::prelude::*; 5 | use spl_governance::state::realm; 6 | 7 | /// Configures the quadratic Registrar, 8 | /// allowing the gatekeeper network or previous plugin to be updated 9 | #[derive(Accounts)] 10 | #[instruction(coefficients: QuadraticCoefficients, use_previous_voter_weight_plugin:bool)] 11 | pub struct ConfigureRegistrar<'info> { 12 | /// The quadratic Plugin Registrar to be updated 13 | #[account(mut)] 14 | pub registrar: Account<'info, Registrar>, 15 | 16 | /// An spl-governance Realm 17 | /// 18 | /// Realm is validated in the instruction: 19 | /// - Realm is owned by the governance_program_id 20 | /// - realm_authority is realm.authority 21 | /// 22 | /// CHECK: Owned by spl-governance instance specified in governance_program_id 23 | #[account( 24 | address = registrar.realm @ QuadraticError::InvalidRealmForRegistrar, 25 | owner = registrar.governance_program_id.key() 26 | )] 27 | pub realm: UncheckedAccount<'info>, 28 | 29 | /// realm_authority must sign and match Realm.authority 30 | pub realm_authority: Signer<'info>, 31 | } 32 | 33 | /// Configures a Registrar, setting a new previous voter weight plugin 34 | pub fn configure_registrar( 35 | ctx: Context, 36 | coefficients: QuadraticCoefficients, 37 | use_previous_voter_weight_plugin: bool, 38 | ) -> Result<()> { 39 | let registrar = &mut ctx.accounts.registrar; 40 | 41 | registrar.quadratic_coefficients = coefficients; 42 | 43 | let remaining_accounts = &ctx.remaining_accounts; 44 | 45 | // If the plugin has a previous plugin, it "inherits" the vote weight from a vote_weight_account owned 46 | // by the previous plugin. This chain is registered here. 47 | registrar.previous_voter_weight_plugin_program_id = use_previous_voter_weight_plugin 48 | .then(|| { 49 | remaining_accounts 50 | .first() 51 | .ok_or(QuadraticError::MissingPreviousVoterWeightPlugin) 52 | .map(|account| account.key) 53 | }) 54 | .transpose()? 55 | .cloned(); 56 | 57 | // Verify that realm_authority is the expected authority of the Realm 58 | // and that the mint matches one of the realm mints too. 59 | let realm = realm::get_realm_data_for_governing_token_mint( 60 | ®istrar.governance_program_id, 61 | &ctx.accounts.realm, 62 | ®istrar.governing_token_mint, 63 | )?; 64 | require_eq!( 65 | realm.authority.unwrap(), 66 | ctx.accounts.realm_authority.key(), 67 | QuadraticError::InvalidRealmAuthority 68 | ); 69 | 70 | Ok(()) 71 | } 72 | -------------------------------------------------------------------------------- /programs/quadratic/src/instructions/create_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::state::*; 2 | use anchor_lang::prelude::*; 3 | 4 | /// Creates VoterWeightRecord used by spl-gov 5 | /// This instruction should only be executed once per realm/governing_token_mint/governing_token_owner 6 | /// to create the account 7 | #[derive(Accounts)] 8 | #[instruction(governing_token_owner: Pubkey)] 9 | pub struct CreateVoterWeightRecord<'info> { 10 | // The Registrar the VoterWeightRecord account belongs to 11 | pub registrar: Account<'info, Registrar>, 12 | 13 | #[account( 14 | init, 15 | seeds = [ b"voter-weight-record".as_ref(), 16 | registrar.realm.key().as_ref(), 17 | registrar.governing_token_mint.key().as_ref(), 18 | governing_token_owner.as_ref()], 19 | bump, 20 | payer = payer, 21 | space = VoterWeightRecord::get_space() 22 | )] 23 | pub voter_weight_record: Account<'info, VoterWeightRecord>, 24 | 25 | #[account(mut)] 26 | pub payer: Signer<'info>, 27 | 28 | pub system_program: Program<'info, System>, 29 | } 30 | 31 | pub fn create_voter_weight_record( 32 | ctx: Context, 33 | governing_token_owner: Pubkey, 34 | ) -> Result<()> { 35 | let voter_weight_record = &mut ctx.accounts.voter_weight_record; 36 | let registrar = &ctx.accounts.registrar; 37 | 38 | voter_weight_record.realm = registrar.realm.key(); 39 | voter_weight_record.governing_token_mint = registrar.governing_token_mint.key(); 40 | voter_weight_record.governing_token_owner = governing_token_owner; 41 | 42 | // Set expiry to expired 43 | voter_weight_record.voter_weight_expiry = Some(0); 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /programs/quadratic/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub use create_registrar::*; 2 | mod create_registrar; 3 | 4 | pub use configure_registrar::*; 5 | mod configure_registrar; 6 | 7 | pub use create_voter_weight_record::*; 8 | mod create_voter_weight_record; 9 | 10 | pub use update_voter_weight_record::*; 11 | mod update_voter_weight_record; 12 | -------------------------------------------------------------------------------- /programs/quadratic/src/instructions/update_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::error::QuadraticError; 2 | use crate::state::*; 3 | use crate::util::convert_vote; 4 | use anchor_lang::prelude::*; 5 | use gpl_shared::compose::{resolve_input_voter_weight, VoterWeightRecordBase}; 6 | use gpl_shared::generic_voter_weight::GenericVoterWeight; 7 | use std::cmp::max; 8 | 9 | impl<'a> VoterWeightRecordBase<'a> for VoterWeightRecord { 10 | fn get_governing_token_mint(&'a self) -> &'a Pubkey { 11 | &self.governing_token_mint 12 | } 13 | 14 | fn get_governing_token_owner(&'a self) -> &'a Pubkey { 15 | &self.governing_token_owner 16 | } 17 | } 18 | 19 | /// Updates VoterWeightRecord to evaluate governance power for non voting use cases: CreateProposal, CreateGovernance etc... 20 | /// This instruction updates VoterWeightRecord which is valid for the current Slot and the given target action only 21 | /// and hence the instruction has to be executed inside the same transaction as the corresponding spl-gov instruction 22 | #[derive(Accounts)] 23 | #[instruction()] 24 | pub struct UpdateVoterWeightRecord<'info> { 25 | /// The quadratic plugin Registrar 26 | pub registrar: Account<'info, Registrar>, 27 | 28 | /// An account that is either of type TokenOwnerRecordV2 or VoterWeightRecord 29 | /// depending on whether the registrar includes a predecessor or not 30 | /// CHECK: Checked in the code depending on the registrar 31 | #[account()] 32 | pub input_voter_weight: UncheckedAccount<'info>, 33 | 34 | #[account( 35 | mut, 36 | constraint = voter_weight_record.realm == registrar.realm 37 | @ QuadraticError::InvalidVoterWeightRecordRealm, 38 | 39 | constraint = voter_weight_record.governing_token_mint == registrar.governing_token_mint 40 | @ QuadraticError::InvalidVoterWeightRecordMint, 41 | )] 42 | pub voter_weight_record: Account<'info, VoterWeightRecord>, 43 | } 44 | 45 | /// Adapts the weight of from the predecessor 46 | pub fn update_voter_weight_record(ctx: Context) -> Result<()> { 47 | let voter_weight_record = &mut ctx.accounts.voter_weight_record; 48 | 49 | let input_voter_weight_account = ctx.accounts.input_voter_weight.to_account_info(); 50 | 51 | let clone_record = voter_weight_record.clone(); 52 | let input_voter_weight_record = resolve_input_voter_weight( 53 | &input_voter_weight_account, 54 | &clone_record, 55 | &ctx.accounts.registrar, 56 | )?; 57 | 58 | let coefficients = &ctx.accounts.registrar.quadratic_coefficients; 59 | 60 | let output_voter_weight = 61 | convert_vote(input_voter_weight_record.get_voter_weight(), coefficients) as u64; 62 | msg!( 63 | "input weight: {}. output weight {}. coefficients: {:?}", 64 | input_voter_weight_record.get_voter_weight(), 65 | output_voter_weight, 66 | coefficients 67 | ); 68 | voter_weight_record.voter_weight = output_voter_weight; 69 | 70 | // If the input voter weight record has an expiry, use the max between that and the current slot 71 | // Otherwise use the current slot 72 | let current_slot = Clock::get()?.slot; 73 | voter_weight_record.voter_weight_expiry = 74 | input_voter_weight_record.get_voter_weight_expiry().map_or( 75 | Some(current_slot), // no previous expiry, use current slot 76 | |previous_expiry| Some(max(previous_expiry, current_slot)), 77 | ); 78 | 79 | Ok(()) 80 | } 81 | -------------------------------------------------------------------------------- /programs/quadratic/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | pub mod error; 4 | 5 | mod instructions; 6 | use instructions::*; 7 | 8 | pub mod state; 9 | mod util; 10 | 11 | use state::*; 12 | 13 | declare_id!("quadCSapU8nTdLg73KHDnmdxKnJQsh7GUbu5tZfnRRr"); 14 | 15 | #[program] 16 | pub mod quadratic { 17 | use super::*; 18 | pub fn create_registrar( 19 | ctx: Context, 20 | coefficients: QuadraticCoefficients, 21 | use_previous_voter_weight_plugin: bool, 22 | ) -> Result<()> { 23 | log_version(); 24 | instructions::create_registrar(ctx, coefficients, use_previous_voter_weight_plugin) 25 | } 26 | pub fn configure_registrar( 27 | ctx: Context, 28 | coefficients: QuadraticCoefficients, 29 | use_previous_voter_weight_plugin: bool, 30 | ) -> Result<()> { 31 | log_version(); 32 | instructions::configure_registrar(ctx, coefficients, use_previous_voter_weight_plugin) 33 | } 34 | pub fn create_voter_weight_record( 35 | ctx: Context, 36 | governing_token_owner: Pubkey, 37 | ) -> Result<()> { 38 | log_version(); 39 | instructions::create_voter_weight_record(ctx, governing_token_owner) 40 | } 41 | pub fn update_voter_weight_record(ctx: Context) -> Result<()> { 42 | log_version(); 43 | instructions::update_voter_weight_record(ctx) 44 | } 45 | } 46 | 47 | fn log_version() { 48 | // TODO: Check if Anchor allows to log it before instruction is deserialized 49 | msg!("VERSION:{:?}", env!("CARGO_PKG_VERSION")); 50 | } 51 | -------------------------------------------------------------------------------- /programs/quadratic/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | pub use registrar::*; 2 | pub mod registrar; 3 | 4 | pub use voter_weight_record::*; 5 | pub mod voter_weight_record; 6 | 7 | pub use quadratic_coefficients::*; 8 | pub mod quadratic_coefficients; 9 | -------------------------------------------------------------------------------- /programs/quadratic/src/state/quadratic_coefficients.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy, PartialEq)] 3 | pub struct QuadraticCoefficients { 4 | pub a: f64, 5 | pub b: f64, 6 | pub c: f64, 7 | } 8 | impl Default for QuadraticCoefficients { 9 | fn default() -> Self { 10 | QuadraticCoefficients { 11 | a: 1.0, 12 | b: 0.0, 13 | c: 0.0, 14 | } 15 | } 16 | } 17 | impl QuadraticCoefficients { 18 | pub const SPACE: usize = 8 + 8 + 8; 19 | } 20 | -------------------------------------------------------------------------------- /programs/quadratic/src/util/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::state::QuadraticCoefficients; 2 | 3 | pub fn convert_vote(input_voter_weight: u64, coefficients: &QuadraticCoefficients) -> f64 { 4 | let input_voter_weight = input_voter_weight as f64; 5 | let a = coefficients.a; 6 | let b = coefficients.b; 7 | let c = coefficients.c; 8 | 9 | a * input_voter_weight.powf(0.5) + b * input_voter_weight + c 10 | } 11 | -------------------------------------------------------------------------------- /programs/quadratic/tests/create_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use gpl_quadratic::state::QuadraticCoefficients; 2 | use program_test::quadratic_voter_test::QuadraticVoterTest; 3 | use program_test::tools::assert_ix_err; 4 | use solana_program::instruction::InstructionError; 5 | use solana_program_test::*; 6 | use solana_sdk::transport::TransportError; 7 | 8 | mod program_test; 9 | 10 | #[tokio::test] 11 | async fn test_create_voter_weight_record() -> Result<(), TransportError> { 12 | // Arrange 13 | let mut quadratic_voter_test = QuadraticVoterTest::start_new().await; 14 | 15 | let realm_cookie = quadratic_voter_test.governance.with_realm().await?; 16 | 17 | let registrar_cookie = quadratic_voter_test 18 | .with_registrar(&realm_cookie, &QuadraticCoefficients::default(), None) 19 | .await?; 20 | 21 | let voter_cookie = quadratic_voter_test.bench.with_wallet().await; 22 | 23 | // Act 24 | let voter_weight_record_cookie = quadratic_voter_test 25 | .with_voter_weight_record(®istrar_cookie, &voter_cookie) 26 | .await?; 27 | 28 | // Assert 29 | 30 | let voter_weight_record = quadratic_voter_test 31 | .get_voter_weight_record(&voter_weight_record_cookie.address) 32 | .await; 33 | 34 | assert_eq!(voter_weight_record_cookie.account, voter_weight_record); 35 | 36 | Ok(()) 37 | } 38 | 39 | #[tokio::test] 40 | async fn test_create_voter_weight_record_with_already_exists_error() -> Result<(), TransportError> { 41 | // Arrange 42 | let mut quadratic_voter_test = QuadraticVoterTest::start_new().await; 43 | 44 | let realm_cookie = quadratic_voter_test.governance.with_realm().await?; 45 | 46 | let registrar_cookie = quadratic_voter_test 47 | .with_registrar(&realm_cookie, &QuadraticCoefficients::default(), None) 48 | .await?; 49 | 50 | let voter_cookie = quadratic_voter_test.bench.with_wallet().await; 51 | 52 | quadratic_voter_test 53 | .with_voter_weight_record(®istrar_cookie, &voter_cookie) 54 | .await?; 55 | 56 | quadratic_voter_test.bench.advance_clock().await; 57 | 58 | // Act 59 | let err = quadratic_voter_test 60 | .with_voter_weight_record(®istrar_cookie, &voter_cookie) 61 | .await 62 | .err() 63 | .unwrap(); 64 | 65 | // Assert 66 | 67 | // InstructionError::Custom(0) is returned for TransactionError::AccountInUse 68 | assert_ix_err(err, InstructionError::Custom(0)); 69 | 70 | Ok(()) 71 | } 72 | -------------------------------------------------------------------------------- /programs/quadratic/tests/fixtures/spl_governance.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-labs/governance-program-library/c7a49fa2a663c9bdf1260827c0d9b15790ebf24b/programs/quadratic/tests/fixtures/spl_governance.so -------------------------------------------------------------------------------- /programs/quadratic/tests/fixtures/spl_governance_addin_mock.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-labs/governance-program-library/c7a49fa2a663c9bdf1260827c0d9b15790ebf24b/programs/quadratic/tests/fixtures/spl_governance_addin_mock.so -------------------------------------------------------------------------------- /programs/quadratic/tests/program_test/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod governance_test; 2 | pub mod predecessor_plugin_test; 3 | pub mod program_test_bench; 4 | pub mod quadratic_voter_test; 5 | pub mod tools; 6 | -------------------------------------------------------------------------------- /programs/quadratic/tests/program_test/predecessor_plugin_test.rs: -------------------------------------------------------------------------------- 1 | use std::str::FromStr; 2 | use std::sync::Arc; 3 | 4 | use anchor_lang::prelude::Pubkey; 5 | 6 | use gpl_quadratic::state::*; 7 | use solana_sdk::{signature::Keypair, signer::Signer, transport::TransportError}; 8 | use spl_governance_addin_mock::instruction::*; 9 | 10 | use crate::program_test::{ 11 | governance_test::RealmCookie, 12 | program_test_bench::{ProgramTestBench, WalletCookie}, 13 | quadratic_voter_test::VoterWeightRecordCookie, 14 | }; 15 | use gpl_quadratic::state::VoterWeightRecord; 16 | use solana_program_test::{BanksClientError, ProgramTest}; 17 | 18 | pub struct PredecessorPluginTest { 19 | pub bench: Arc, 20 | } 21 | 22 | impl PredecessorPluginTest { 23 | pub fn program_id() -> Pubkey { 24 | Pubkey::from_str("GovAddinMock1111111111111111111111111111111").unwrap() 25 | } 26 | 27 | #[allow(dead_code)] 28 | pub fn add_program(program_test: &mut ProgramTest) { 29 | program_test.add_program("spl_governance_addin_mock", Self::program_id(), None); 30 | } 31 | 32 | #[allow(dead_code)] 33 | pub fn new(bench: Arc) -> Self { 34 | PredecessorPluginTest { bench } 35 | } 36 | 37 | #[allow(dead_code)] 38 | pub async fn with_voter_weight_record( 39 | &self, 40 | realm_cookie: &RealmCookie, 41 | voter_cookie: &WalletCookie, 42 | voter_weight: u64, 43 | ) -> Result { 44 | let governing_token_owner = voter_cookie.address; 45 | let voter_weight_record_account = Keypair::new(); 46 | 47 | let setup_voter_weight_record_ix = setup_voter_weight_record( 48 | &Self::program_id(), 49 | &realm_cookie.address, 50 | &realm_cookie.account.community_mint, 51 | &voter_cookie.address, 52 | &voter_weight_record_account.pubkey(), 53 | &self.bench.payer.pubkey(), 54 | voter_weight, 55 | Some(0), 56 | None, 57 | None, 58 | ); 59 | 60 | self.bench 61 | .process_transaction( 62 | &[setup_voter_weight_record_ix], 63 | Some(&[&voter_weight_record_account]), 64 | ) 65 | .await?; 66 | 67 | let account = VoterWeightRecord { 68 | realm: realm_cookie.address, 69 | governing_token_mint: realm_cookie.account.community_mint, 70 | governing_token_owner, 71 | voter_weight: 0, 72 | voter_weight_expiry: Some(0), 73 | weight_action: None, 74 | weight_action_target: None, 75 | reserved: [0; 8], 76 | }; 77 | 78 | Ok(VoterWeightRecordCookie { 79 | address: voter_weight_record_account.pubkey(), 80 | account, 81 | }) 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /programs/realm-voter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gpl-realm-voter" 3 | version = "0.0.1" 4 | description = "SPL Governance plugin granting governance power through Realms membership" 5 | license = "Apache-2.0" 6 | edition = "2018" 7 | 8 | [lib] 9 | crate-type = ["cdylib", "lib"] 10 | name = "gpl_realm_voter" 11 | 12 | [features] 13 | no-entrypoint = [] 14 | no-idl = [] 15 | no-log-ix-name = [] 16 | cpi = ["no-entrypoint"] 17 | default = [] 18 | idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] 19 | 20 | [dependencies] 21 | arrayref = "0.3.6" 22 | anchor-lang = { version = "0.30.1" } 23 | anchor-spl = "0.30.1" 24 | solana-program = "1.18.18" 25 | spl-governance = { version = "4.0", features = ["no-entrypoint"] } 26 | spl-governance-tools = "0.1.4" 27 | spl-token = { version = "4.0.0", features = [ "no-entrypoint" ] } 28 | 29 | [dev-dependencies] 30 | borsh = "0.10.3" 31 | solana-sdk = "1.18.18" 32 | solana-program-test = "1.18.18" 33 | -------------------------------------------------------------------------------- /programs/realm-voter/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /programs/realm-voter/src/error.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum RealmVoterError { 5 | #[msg("Invalid Realm Authority")] 6 | InvalidRealmAuthority, 7 | 8 | #[msg("Invalid Realm for Registrar")] 9 | InvalidRealmForRegistrar, 10 | 11 | #[msg("Invalid VoterWeightRecord Realm")] 12 | InvalidVoterWeightRecordRealm, 13 | 14 | #[msg("Invalid VoterWeightRecord Mint")] 15 | InvalidVoterWeightRecordMint, 16 | 17 | #[msg("TokenOwnerRecord from own realm is not allowed")] 18 | TokenOwnerRecordFromOwnRealmNotAllowed, 19 | 20 | #[msg("Governance program not configured")] 21 | GovernanceProgramNotConfigured, 22 | 23 | #[msg("Governing TokenOwner must match")] 24 | GoverningTokenOwnerMustMatch, 25 | } 26 | -------------------------------------------------------------------------------- /programs/realm-voter/src/instructions/configure_voter_weights.rs: -------------------------------------------------------------------------------- 1 | use crate::error::RealmVoterError; 2 | use crate::state::max_voter_weight_record::MaxVoterWeightRecord; 3 | use crate::state::*; 4 | use anchor_lang::prelude::*; 5 | use spl_governance::state::realm; 6 | 7 | /// Configures realm_member_voter_weight and max_voter_weight for Registrar 8 | /// It also sets MaxVoterWeightRecord.max_voter_weight to the provided value 9 | /// MaxVoterWeightRecord.max_voter_weight is static and can only be set using this instruction and hence it never expires 10 | #[derive(Accounts)] 11 | pub struct ConfigureVoterWeights<'info> { 12 | /// The Registrar for the given realm and governing_token_mint 13 | #[account(mut)] 14 | pub registrar: Account<'info, Registrar>, 15 | 16 | #[account( 17 | address = registrar.realm @ RealmVoterError::InvalidRealmForRegistrar, 18 | owner = registrar.governance_program_id 19 | )] 20 | /// CHECK: Owned by spl-governance instance specified in registrar.governance_program_id 21 | pub realm: UncheckedAccount<'info>, 22 | 23 | /// Authority of the Realm must sign and match realm.authority 24 | pub realm_authority: Signer<'info>, 25 | 26 | /// MaxVoterWeightRecord for the given registrar.realm and registrar.governing_token_mint 27 | #[account( 28 | mut, 29 | constraint = max_voter_weight_record.realm == registrar.realm 30 | @ RealmVoterError::InvalidVoterWeightRecordRealm, 31 | 32 | constraint = max_voter_weight_record.governing_token_mint == registrar.governing_token_mint 33 | @ RealmVoterError::InvalidVoterWeightRecordMint, 34 | )] 35 | pub max_voter_weight_record: Account<'info, MaxVoterWeightRecord>, 36 | } 37 | 38 | pub fn configure_voter_weights( 39 | ctx: Context, 40 | realm_member_voter_weight: u64, 41 | max_voter_weight: u64, 42 | ) -> Result<()> { 43 | let registrar = &mut ctx.accounts.registrar; 44 | // Note: max_voter_weight is stored on Registrar for consistency sake to have the registrar as the source of truth for configuration 45 | // And MaxVoterWeightRecord.max_voter_weight is used as data exchange account 46 | registrar.realm_member_voter_weight = realm_member_voter_weight; 47 | registrar.max_voter_weight = max_voter_weight; 48 | 49 | let realm = realm::get_realm_data_for_governing_token_mint( 50 | ®istrar.governance_program_id, 51 | &ctx.accounts.realm, 52 | ®istrar.governing_token_mint, 53 | )?; 54 | 55 | require_eq!( 56 | realm.authority.unwrap(), 57 | ctx.accounts.realm_authority.key(), 58 | RealmVoterError::InvalidRealmAuthority 59 | ); 60 | 61 | let voter_weight_record = &mut ctx.accounts.max_voter_weight_record; 62 | voter_weight_record.max_voter_weight = max_voter_weight; 63 | 64 | // max_voter_weight can only be updated using this instruction and it never expires 65 | voter_weight_record.max_voter_weight_expiry = None; 66 | 67 | Ok(()) 68 | } 69 | -------------------------------------------------------------------------------- /programs/realm-voter/src/instructions/create_max_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | use crate::state::{max_voter_weight_record::MaxVoterWeightRecord, Registrar}; 4 | 5 | /// Creates MaxVoterWeightRecord used by spl-governance 6 | /// This instruction should only be executed once per realm/governing_token_mint to create the account 7 | #[derive(Accounts)] 8 | pub struct CreateMaxVoterWeightRecord<'info> { 9 | // The Registrar the MaxVoterWeightRecord account belongs to 10 | pub registrar: Account<'info, Registrar>, 11 | 12 | #[account( 13 | init, 14 | seeds = [ b"max-voter-weight-record".as_ref(), 15 | registrar.realm.key().as_ref(), 16 | registrar.governing_token_mint.key().as_ref()], 17 | bump, 18 | payer = payer, 19 | space = MaxVoterWeightRecord::get_space() 20 | )] 21 | pub max_voter_weight_record: Account<'info, MaxVoterWeightRecord>, 22 | 23 | #[account(mut)] 24 | pub payer: Signer<'info>, 25 | 26 | pub system_program: Program<'info, System>, 27 | } 28 | 29 | pub fn create_max_voter_weight_record(ctx: Context) -> Result<()> { 30 | let max_voter_weight_record = &mut ctx.accounts.max_voter_weight_record; 31 | let registrar = &ctx.accounts.registrar; 32 | 33 | max_voter_weight_record.realm = registrar.realm; 34 | max_voter_weight_record.governing_token_mint = registrar.governing_token_mint; 35 | 36 | // Set expiry to expired 37 | max_voter_weight_record.max_voter_weight_expiry = Some(0); 38 | 39 | Ok(()) 40 | } 41 | -------------------------------------------------------------------------------- /programs/realm-voter/src/instructions/create_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::state::*; 2 | use anchor_lang::prelude::*; 3 | 4 | /// Creates VoterWeightRecord used by spl-gov 5 | /// This instruction should only be executed once per realm/governing_token_mint/governing_token_owner 6 | /// to create the account 7 | #[derive(Accounts)] 8 | #[instruction(governing_token_owner: Pubkey)] 9 | pub struct CreateVoterWeightRecord<'info> { 10 | // The Registrar the VoterWeightRecord account belongs to 11 | pub registrar: Account<'info, Registrar>, 12 | 13 | #[account( 14 | init, 15 | seeds = [ b"voter-weight-record".as_ref(), 16 | registrar.realm.key().as_ref(), 17 | registrar.governing_token_mint.key().as_ref(), 18 | governing_token_owner.as_ref()], 19 | bump, 20 | payer = payer, 21 | space = VoterWeightRecord::get_space() 22 | )] 23 | pub voter_weight_record: Account<'info, VoterWeightRecord>, 24 | 25 | #[account(mut)] 26 | pub payer: Signer<'info>, 27 | 28 | pub system_program: Program<'info, System>, 29 | } 30 | 31 | pub fn create_voter_weight_record( 32 | ctx: Context, 33 | governing_token_owner: Pubkey, 34 | ) -> Result<()> { 35 | let voter_weight_record = &mut ctx.accounts.voter_weight_record; 36 | let registrar = &ctx.accounts.registrar; 37 | 38 | voter_weight_record.realm = registrar.realm.key(); 39 | voter_weight_record.governing_token_mint = registrar.governing_token_mint.key(); 40 | voter_weight_record.governing_token_owner = governing_token_owner; 41 | 42 | // Set expiry to expired 43 | voter_weight_record.voter_weight_expiry = Some(0); 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /programs/realm-voter/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub use configure_governance_program::*; 2 | mod configure_governance_program; 3 | 4 | pub use create_registrar::*; 5 | mod create_registrar; 6 | 7 | pub use create_voter_weight_record::*; 8 | mod create_voter_weight_record; 9 | 10 | pub use create_max_voter_weight_record::*; 11 | mod create_max_voter_weight_record; 12 | 13 | pub use update_voter_weight_record::*; 14 | mod update_voter_weight_record; 15 | 16 | pub use configure_voter_weights::*; 17 | mod configure_voter_weights; 18 | -------------------------------------------------------------------------------- /programs/realm-voter/src/instructions/update_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::error::RealmVoterError; 2 | use crate::state::*; 3 | use anchor_lang::prelude::*; 4 | use spl_governance::state::token_owner_record; 5 | 6 | /// Updates VoterWeightRecord based on Realm DAO membership 7 | /// The membership is evaluated via a valid TokenOwnerRecord which must belong to one of the configured spl-governance instances 8 | /// 9 | /// This instruction sets VoterWeightRecord.voter_weight which is valid for the current slot only 10 | /// and must be executed inside the same transaction as the corresponding spl-gov instruction 11 | #[derive(Accounts)] 12 | pub struct UpdateVoterWeightRecord<'info> { 13 | /// The RealmVoter voting Registrar 14 | pub registrar: Account<'info, Registrar>, 15 | 16 | #[account( 17 | mut, 18 | constraint = voter_weight_record.realm == registrar.realm 19 | @ RealmVoterError::InvalidVoterWeightRecordRealm, 20 | 21 | constraint = voter_weight_record.governing_token_mint == registrar.governing_token_mint 22 | @ RealmVoterError::InvalidVoterWeightRecordMint, 23 | )] 24 | pub voter_weight_record: Account<'info, VoterWeightRecord>, 25 | 26 | /// TokenOwnerRecord for any of the configured spl-governance instances 27 | /// CHECK: Owned by any of the spl-governance instances specified in registrar.governance_program_configs 28 | pub token_owner_record: UncheckedAccount<'info>, 29 | } 30 | 31 | pub fn update_voter_weight_record(ctx: Context) -> Result<()> { 32 | let registrar = &ctx.accounts.registrar; 33 | let voter_weight_record = &mut ctx.accounts.voter_weight_record; 34 | 35 | let governance_program_id = ctx.accounts.token_owner_record.owner; 36 | 37 | // Note: We only verify a valid TokenOwnerRecord account exists for one of the configured spl-governance instances 38 | // The existence of the account proofs the governing_token_owner has interacted with spl-governance Realm at least once in the past 39 | if !registrar 40 | .governance_program_configs 41 | .iter() 42 | .any(|cc| cc.program_id == governance_program_id.key()) 43 | { 44 | return err!(RealmVoterError::GovernanceProgramNotConfigured); 45 | }; 46 | 47 | let token_owner_record = token_owner_record::get_token_owner_record_data( 48 | governance_program_id, 49 | &ctx.accounts.token_owner_record, 50 | )?; 51 | 52 | // Ensure VoterWeightRecord and TokenOwnerRecord are for the same governing_token_owner 53 | require_eq!( 54 | token_owner_record.governing_token_owner, 55 | voter_weight_record.governing_token_owner, 56 | RealmVoterError::GoverningTokenOwnerMustMatch 57 | ); 58 | 59 | // Membership of the Realm the plugin is configured for is not allowed as a source of governance power 60 | require_neq!( 61 | token_owner_record.realm, 62 | registrar.realm, 63 | RealmVoterError::TokenOwnerRecordFromOwnRealmNotAllowed 64 | ); 65 | 66 | // Setup voter_weight 67 | voter_weight_record.voter_weight = registrar.realm_member_voter_weight; 68 | 69 | // Record is only valid as of the current slot 70 | voter_weight_record.voter_weight_expiry = Some(Clock::get()?.slot); 71 | 72 | // Set action and target to None to indicate the weight is valid for any action and target 73 | voter_weight_record.weight_action = None; 74 | voter_weight_record.weight_action_target = None; 75 | 76 | Ok(()) 77 | } 78 | -------------------------------------------------------------------------------- /programs/realm-voter/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | pub mod error; 4 | 5 | mod instructions; 6 | use instructions::*; 7 | 8 | pub mod state; 9 | 10 | pub mod tools; 11 | use crate::state::CollectionItemChangeType; 12 | 13 | declare_id!("GRmVtfLq2BPeWs5EDoQoZc787VYkhdkA11k63QM1Xemz"); 14 | 15 | #[program] 16 | pub mod realm_voter { 17 | 18 | use super::*; 19 | 20 | pub fn create_registrar( 21 | ctx: Context, 22 | max_governance_programs: u8, 23 | ) -> Result<()> { 24 | log_version(); 25 | instructions::create_registrar(ctx, max_governance_programs) 26 | } 27 | pub fn create_voter_weight_record( 28 | ctx: Context, 29 | governing_token_owner: Pubkey, 30 | ) -> Result<()> { 31 | log_version(); 32 | instructions::create_voter_weight_record(ctx, governing_token_owner) 33 | } 34 | pub fn create_max_voter_weight_record(ctx: Context) -> Result<()> { 35 | log_version(); 36 | instructions::create_max_voter_weight_record(ctx) 37 | } 38 | pub fn update_voter_weight_record(ctx: Context) -> Result<()> { 39 | log_version(); 40 | instructions::update_voter_weight_record(ctx) 41 | } 42 | pub fn configure_voter_weights( 43 | ctx: Context, 44 | realm_member_voter_weight: u64, 45 | max_voter_weight: u64, 46 | ) -> Result<()> { 47 | log_version(); 48 | instructions::configure_voter_weights(ctx, realm_member_voter_weight, max_voter_weight) 49 | } 50 | 51 | pub fn configure_governance_program( 52 | ctx: Context, 53 | change_type: CollectionItemChangeType, 54 | ) -> Result<()> { 55 | log_version(); 56 | instructions::configure_governance_program(ctx, change_type) 57 | } 58 | } 59 | 60 | fn log_version() { 61 | // TODO: Check if Anchor allows to log it before instruction is deserialized 62 | msg!("VERSION:{:?}", env!("CARGO_PKG_VERSION")); 63 | } 64 | -------------------------------------------------------------------------------- /programs/realm-voter/src/state/governance_program_config.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | /// Configuration of an spl-governance instance used to grant governance power 4 | #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy, PartialEq, Default)] 5 | pub struct GovernanceProgramConfig { 6 | /// The program id of the configured spl-governance instance 7 | pub program_id: Pubkey, 8 | 9 | /// Reserved for future upgrades 10 | pub reserved: [u8; 8], 11 | } 12 | -------------------------------------------------------------------------------- /programs/realm-voter/src/state/max_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::id; 2 | use crate::tools::anchor::DISCRIMINATOR_SIZE; 3 | use anchor_lang::prelude::Pubkey; 4 | use anchor_lang::prelude::*; 5 | use solana_program::pubkey::PUBKEY_BYTES; 6 | 7 | /// MaxVoterWeightRecord account as defined in spl-governance-addin-api 8 | /// It's redefined here without account_discriminator for Anchor to treat it as native account 9 | /// 10 | /// The account is used as an api interface to provide max voting power to the governance program from external addin contracts 11 | #[account] 12 | #[derive(Debug, PartialEq)] 13 | pub struct MaxVoterWeightRecord { 14 | /// The Realm the MaxVoterWeightRecord belongs to 15 | pub realm: Pubkey, 16 | 17 | /// Governing Token Mint the MaxVoterWeightRecord is associated with 18 | /// Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only 19 | // The mint here is to link the record to either community or council mint of the realm 20 | pub governing_token_mint: Pubkey, 21 | 22 | /// Max voter weight 23 | /// The max voter weight provided by the addin for the given realm and governing_token_mint 24 | pub max_voter_weight: u64, 25 | 26 | /// The slot when the max voting weight expires 27 | /// It should be set to None if the weight never expires 28 | /// If the max vote weight decays with time, for example for time locked based weights, then the expiry must be set 29 | /// As a pattern Revise instruction to update the max weight should be invoked before governance instruction within the same transaction 30 | /// and the expiry set to the current slot to provide up to date weight 31 | pub max_voter_weight_expiry: Option, 32 | 33 | /// Reserved space for future versions 34 | pub reserved: [u8; 8], 35 | } 36 | 37 | impl Default for MaxVoterWeightRecord { 38 | fn default() -> Self { 39 | Self { 40 | realm: Default::default(), 41 | governing_token_mint: Default::default(), 42 | max_voter_weight: Default::default(), 43 | max_voter_weight_expiry: Some(0), 44 | reserved: Default::default(), 45 | } 46 | } 47 | } 48 | 49 | impl MaxVoterWeightRecord { 50 | pub fn get_space() -> usize { 51 | DISCRIMINATOR_SIZE + PUBKEY_BYTES * 2 + 8 + 1 + 8 + 8 52 | } 53 | } 54 | 55 | /// Returns MaxVoterWeightRecord PDA seeds 56 | pub fn get_max_voter_weight_record_seeds<'a>( 57 | realm: &'a Pubkey, 58 | governing_token_mint: &'a Pubkey, 59 | ) -> [&'a [u8]; 3] { 60 | [ 61 | b"max-voter-weight-record", 62 | realm.as_ref(), 63 | governing_token_mint.as_ref(), 64 | ] 65 | } 66 | 67 | /// Returns MaxVoterWeightRecord PDA address 68 | pub fn get_max_voter_weight_record_address( 69 | realm: &Pubkey, 70 | governing_token_mint: &Pubkey, 71 | ) -> Pubkey { 72 | Pubkey::find_program_address( 73 | &get_max_voter_weight_record_seeds(realm, governing_token_mint), 74 | &id(), 75 | ) 76 | .0 77 | } 78 | 79 | #[cfg(test)] 80 | mod test { 81 | 82 | use super::*; 83 | 84 | #[test] 85 | fn test_get_space() { 86 | // Arrange 87 | let expected_space = MaxVoterWeightRecord::get_space(); 88 | 89 | // Act 90 | let actual_space = 91 | DISCRIMINATOR_SIZE + MaxVoterWeightRecord::default().try_to_vec().unwrap().len(); 92 | 93 | // Assert 94 | assert_eq!(expected_space, actual_space); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /programs/realm-voter/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | pub use registrar::*; 2 | pub mod registrar; 3 | 4 | pub use governance_program_config::*; 5 | pub mod governance_program_config; 6 | 7 | pub mod max_voter_weight_record; 8 | 9 | pub use voter_weight_record::*; 10 | pub mod voter_weight_record; 11 | -------------------------------------------------------------------------------- /programs/realm-voter/src/tools/anchor.rs: -------------------------------------------------------------------------------- 1 | pub const DISCRIMINATOR_SIZE: usize = 8; 2 | -------------------------------------------------------------------------------- /programs/realm-voter/src/tools/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod anchor; 2 | -------------------------------------------------------------------------------- /programs/realm-voter/tests/create_max_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::program_test::realm_voter_test::RealmVoterTest; 2 | use program_test::tools::assert_ix_err; 3 | use solana_program::instruction::InstructionError; 4 | use solana_program_test::*; 5 | use solana_sdk::transport::TransportError; 6 | 7 | mod program_test; 8 | 9 | #[tokio::test] 10 | async fn test_create_max_voter_weight_record() -> Result<(), TransportError> { 11 | // Arrange 12 | let mut realm_voter_test = RealmVoterTest::start_new().await; 13 | 14 | let realm_cookie = realm_voter_test.governance.with_realm().await?; 15 | 16 | let registrar_cookie = realm_voter_test.with_registrar(&realm_cookie).await?; 17 | 18 | // Act 19 | let max_voter_weight_record_cookie = realm_voter_test 20 | .with_max_voter_weight_record(®istrar_cookie) 21 | .await?; 22 | 23 | // Assert 24 | 25 | let max_voter_weight_record = realm_voter_test 26 | .get_max_voter_weight_record(&max_voter_weight_record_cookie.address) 27 | .await; 28 | 29 | assert_eq!( 30 | max_voter_weight_record_cookie.account, 31 | max_voter_weight_record 32 | ); 33 | 34 | Ok(()) 35 | } 36 | 37 | #[tokio::test] 38 | async fn test_create_max_voter_weight_record_with_already_exists_error( 39 | ) -> Result<(), TransportError> { 40 | // Arrange 41 | let mut realm_voter_test = RealmVoterTest::start_new().await; 42 | 43 | let realm_cookie = realm_voter_test.governance.with_realm().await?; 44 | 45 | let registrar_cookie = realm_voter_test.with_registrar(&realm_cookie).await?; 46 | 47 | realm_voter_test 48 | .with_max_voter_weight_record(®istrar_cookie) 49 | .await?; 50 | 51 | realm_voter_test.bench.advance_clock().await; 52 | 53 | // Act 54 | let err = realm_voter_test 55 | .with_max_voter_weight_record(®istrar_cookie) 56 | .await 57 | .err() 58 | .unwrap(); 59 | 60 | // Assert 61 | 62 | // InstructionError::Custom(0) is returned for TransactionError::AccountInUse 63 | assert_ix_err(err, InstructionError::Custom(0)); 64 | 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /programs/realm-voter/tests/create_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::program_test::realm_voter_test::RealmVoterTest; 2 | use program_test::tools::assert_ix_err; 3 | use solana_program::instruction::InstructionError; 4 | use solana_program_test::*; 5 | use solana_sdk::transport::TransportError; 6 | 7 | mod program_test; 8 | 9 | #[tokio::test] 10 | async fn test_create_voter_weight_record() -> Result<(), TransportError> { 11 | // Arrange 12 | let mut realm_voter_test = RealmVoterTest::start_new().await; 13 | 14 | let realm_cookie = realm_voter_test.governance.with_realm().await?; 15 | 16 | let registrar_cookie = realm_voter_test.with_registrar(&realm_cookie).await?; 17 | 18 | let voter_cookie = realm_voter_test.bench.with_wallet().await; 19 | 20 | // Act 21 | let voter_weight_record_cookie = realm_voter_test 22 | .with_voter_weight_record(®istrar_cookie, &voter_cookie) 23 | .await?; 24 | 25 | // Assert 26 | 27 | let voter_weight_record = realm_voter_test 28 | .get_voter_weight_record(&voter_weight_record_cookie.address) 29 | .await; 30 | 31 | assert_eq!(voter_weight_record_cookie.account, voter_weight_record); 32 | 33 | Ok(()) 34 | } 35 | 36 | #[tokio::test] 37 | async fn test_create_voter_weight_record_with_already_exists_error() -> Result<(), TransportError> { 38 | // Arrange 39 | let mut realm_voter_test = RealmVoterTest::start_new().await; 40 | 41 | let realm_cookie = realm_voter_test.governance.with_realm().await?; 42 | 43 | let registrar_cookie = realm_voter_test.with_registrar(&realm_cookie).await?; 44 | 45 | let voter_cookie = realm_voter_test.bench.with_wallet().await; 46 | 47 | realm_voter_test 48 | .with_voter_weight_record(®istrar_cookie, &voter_cookie) 49 | .await?; 50 | 51 | realm_voter_test.bench.advance_clock().await; 52 | 53 | // Act 54 | let err = realm_voter_test 55 | .with_voter_weight_record(®istrar_cookie, &voter_cookie) 56 | .await 57 | .err() 58 | .unwrap(); 59 | 60 | // Assert 61 | 62 | // InstructionError::Custom(0) is returned for TransactionError::AccountInUse 63 | assert_ix_err(err, InstructionError::Custom(0)); 64 | 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /programs/realm-voter/tests/fixtures/spl_governance.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-labs/governance-program-library/c7a49fa2a663c9bdf1260827c0d9b15790ebf24b/programs/realm-voter/tests/fixtures/spl_governance.so -------------------------------------------------------------------------------- /programs/realm-voter/tests/program_test/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod governance_test; 2 | pub mod program_test_bench; 3 | pub mod realm_voter_test; 4 | pub mod tools; 5 | -------------------------------------------------------------------------------- /programs/realm-voter/tests/program_test/tools.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::ERROR_CODE_OFFSET; 2 | use gpl_realm_voter::error::RealmVoterError; 3 | use solana_program::instruction::InstructionError; 4 | use solana_program_test::BanksClientError; 5 | use solana_sdk::{signature::Keypair, transaction::TransactionError, transport::TransportError}; 6 | use spl_governance_tools::error::GovernanceToolsError; 7 | 8 | pub fn clone_keypair(source: &Keypair) -> Keypair { 9 | Keypair::from_bytes(&source.to_bytes()).unwrap() 10 | } 11 | 12 | /// NOP (No Operation) Override function 13 | #[allow(non_snake_case)] 14 | pub fn NopOverride(_: &mut T) {} 15 | 16 | #[allow(dead_code)] 17 | pub fn assert_realm_voter_err( 18 | banks_client_error: BanksClientError, 19 | realm_voter_error: RealmVoterError, 20 | ) { 21 | let tx_error = banks_client_error.unwrap(); 22 | 23 | match tx_error { 24 | TransactionError::InstructionError(_, instruction_error) => match instruction_error { 25 | InstructionError::Custom(e) => { 26 | assert_eq!(e, realm_voter_error as u32 + ERROR_CODE_OFFSET) 27 | } 28 | _ => panic!("{:?} Is not InstructionError::Custom()", instruction_error), 29 | }, 30 | _ => panic!("{:?} Is not InstructionError", tx_error), 31 | }; 32 | } 33 | 34 | #[allow(dead_code)] 35 | pub fn assert_gov_tools_err( 36 | banks_client_error: TransportError, 37 | gov_tools_error: GovernanceToolsError, 38 | ) { 39 | let tx_error = banks_client_error.unwrap(); 40 | 41 | match tx_error { 42 | TransactionError::InstructionError(_, instruction_error) => match instruction_error { 43 | InstructionError::Custom(e) => { 44 | assert_eq!(e, gov_tools_error as u32) 45 | } 46 | _ => panic!("{:?} Is not InstructionError::Custom()", instruction_error), 47 | }, 48 | _ => panic!("{:?} Is not InstructionError", tx_error), 49 | }; 50 | } 51 | 52 | #[allow(dead_code)] 53 | pub fn assert_anchor_err( 54 | banks_client_error: BanksClientError, 55 | anchor_error: anchor_lang::error::ErrorCode, 56 | ) { 57 | let tx_error = banks_client_error.unwrap(); 58 | 59 | match tx_error { 60 | TransactionError::InstructionError(_, instruction_error) => match instruction_error { 61 | InstructionError::Custom(e) => { 62 | assert_eq!(e, anchor_error as u32) 63 | } 64 | _ => panic!("{:?} Is not InstructionError::Custom()", instruction_error), 65 | }, 66 | _ => panic!("{:?} Is not InstructionError", tx_error), 67 | }; 68 | } 69 | 70 | #[allow(dead_code)] 71 | pub fn assert_ix_err(banks_client_error: BanksClientError, ix_error: InstructionError) { 72 | let tx_error = banks_client_error.unwrap(); 73 | 74 | match tx_error { 75 | TransactionError::InstructionError(_, instruction_error) => { 76 | assert_eq!(instruction_error, ix_error); 77 | } 78 | _ => panic!("{:?} Is not InstructionError", tx_error), 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /programs/shared/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gpl-shared" 3 | version = "0.1.1" 4 | description = "SPL Governance plugin shared code" 5 | license = "Apache-2.0" 6 | edition = "2018" 7 | 8 | [lib] 9 | crate-type = ["cdylib", "lib"] 10 | name = "gpl_shared" 11 | 12 | [features] 13 | no-entrypoint = [] 14 | no-idl = [] 15 | no-log-ix-name = [] 16 | cpi = ["no-entrypoint"] 17 | default = [] 18 | 19 | [dependencies] 20 | anchor-lang = { version = "0.30.1", features = ["init-if-needed"] } 21 | enum_dispatch = "0.3.8" 22 | num-derive = "0.4.1" 23 | num-traits = "0.2" 24 | spl-governance = { version = "4.0", features = ["no-entrypoint"] } 25 | spl-governance-tools = "0.1.4" 26 | spl-governance-addin-api = "0.1.4" 27 | spl-token = { version = "4.0.0", features = [ "no-entrypoint" ] } 28 | 29 | [dev-dependencies] -------------------------------------------------------------------------------- /programs/shared/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /programs/shared/src/anchor.rs: -------------------------------------------------------------------------------- 1 | pub const DISCRIMINATOR_SIZE: usize = 8; 2 | pub const PUBKEY_SIZE: usize = 32; 3 | -------------------------------------------------------------------------------- /programs/shared/src/error.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum VoterWeightError { 5 | #[msg("Invalid TokenOwnerRecord as input voter weight (expecting TokenOwnerRecord V1 or V2)")] 6 | InvalidPredecessorTokenOwnerRecord, 7 | 8 | #[msg("Invalid VoterWeightRecord as input voter weight (expecting VoterWeightRecord)")] 9 | InvalidPredecessorVoterWeightRecord, 10 | 11 | #[msg("Invalid VoterWeightRecord realm for input voter weight")] 12 | InvalidPredecessorVoterWeightRecordRealm, 13 | 14 | #[msg("Invalid VoterWeightRecord governance token mint for input voter weight")] 15 | InvalidPredecessorVoterWeightRecordGovTokenMint, 16 | 17 | #[msg("Invalid VoterWeightRecord governance token owner for input voter weight")] 18 | InvalidPredecessorVoterWeightRecordGovTokenOwner, 19 | 20 | #[msg("Invalid VoterWeightRecord realm")] 21 | InvalidVoterWeightRecordRealm, 22 | 23 | #[msg("Invalid VoterWeightRecord mint")] 24 | InvalidVoterWeightRecordMint, 25 | 26 | #[msg("Previous voter weight plugin required but not provided")] 27 | MissingPreviousVoterWeightPlugin, 28 | } 29 | -------------------------------------------------------------------------------- /programs/shared/src/generic_max_voter_weight.rs: -------------------------------------------------------------------------------- 1 | use crate::mint::MintMaxVoterWeight; 2 | use anchor_lang::prelude::*; 3 | use enum_dispatch::enum_dispatch; 4 | use spl_governance_addin_api::max_voter_weight::MaxVoterWeightRecord; 5 | 6 | /// A generic trait representing a max voter weight, 7 | /// that can be passed as an input into the plugin 8 | #[enum_dispatch] 9 | pub trait GenericMaxVoterWeight { 10 | fn get_governing_token_mint(&self) -> Pubkey; 11 | // fn get_governing_token_owner(&self) -> Pubkey; 12 | // fn get_realm(&self) -> Pubkey; 13 | fn get_max_voter_weight(&self) -> u64; 14 | // fn get_weight_action(&self) -> Option; 15 | // fn get_weight_action_target(&self) -> Option; 16 | fn get_max_voter_weight_expiry(&self) -> Option; 17 | } 18 | 19 | #[enum_dispatch(GenericMaxVoterWeight)] 20 | pub enum GenericMaxVoterWeightEnum { 21 | MaxVoterWeightRecord(MaxVoterWeightRecord), 22 | Mint(MintMaxVoterWeight), 23 | } 24 | 25 | // the "official" on-chain max voter weight record has a discriminator field 26 | // when a predecessor voter weight is provided, it uses this struct 27 | // We add the GenericMaxVoterWeight trait here to hide this from the rest of the code. 28 | impl GenericMaxVoterWeight for MaxVoterWeightRecord { 29 | fn get_governing_token_mint(&self) -> Pubkey { 30 | self.governing_token_mint 31 | } 32 | // 33 | // fn get_governing_token_owner(&self) -> Pubkey { 34 | // self.governing_token_owner 35 | // } 36 | // 37 | // fn get_realm(&self) -> Pubkey { 38 | // self.realm 39 | // } 40 | 41 | fn get_max_voter_weight(&self) -> u64 { 42 | self.max_voter_weight 43 | } 44 | 45 | // The GenericVoterWeight interface expects a crate-defined VoterWeightAction. 46 | // This is identical to spl_governance_addin_api::voter_weight::VoterWeightAction, but added here 47 | // so that Anchor will create the mapping correctly in the IDL. 48 | // This function converts the spl_governance_addin_api::voter_weight::VoterWeightAction to the 49 | // crate-defined VoterWeightAction by mapping the enum values by integer. 50 | // Note - it is imperative that the two enums stay in sync to avoid errors here. 51 | // fn get_weight_action(&self) -> Option { 52 | // self.weight_action.clone() 53 | // .clone() 54 | // .map(|x| FromPrimitive::from_u32(x as u32).unwrap()) 55 | // } 56 | 57 | // fn get_weight_action_target(&self) -> Option { 58 | // self.weight_action_target 59 | // } 60 | // 61 | fn get_max_voter_weight_expiry(&self) -> Option { 62 | self.max_voter_weight_expiry 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /programs/shared/src/generic_voter_weight.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | use enum_dispatch::enum_dispatch; 3 | use spl_governance::state::token_owner_record::TokenOwnerRecordV2; 4 | use spl_governance_addin_api::voter_weight::{VoterWeightAction, VoterWeightRecord}; 5 | 6 | /// A generic trait representing a voter weight, 7 | /// that can be passed as an input into the plugin 8 | #[enum_dispatch] 9 | pub trait GenericVoterWeight { 10 | fn get_governing_token_mint(&self) -> Pubkey; 11 | fn get_governing_token_owner(&self) -> Pubkey; 12 | fn get_realm(&self) -> Pubkey; 13 | fn get_voter_weight(&self) -> u64; 14 | fn get_weight_action(&self) -> Option; 15 | fn get_weight_action_target(&self) -> Option; 16 | fn get_voter_weight_expiry(&self) -> Option; 17 | } 18 | 19 | #[enum_dispatch(GenericVoterWeight)] 20 | pub enum GenericVoterWeightEnum { 21 | VoterWeightRecord(VoterWeightRecord), 22 | TokenOwnerRecord(TokenOwnerRecordV2), 23 | } 24 | 25 | // the "official" on-chain voter weight record has a discriminator field 26 | // when a predecessor voter weight is provided, it uses this struct 27 | // We add the GenericVoterWeight trait here to hide this from the rest of the code. 28 | impl GenericVoterWeight for VoterWeightRecord { 29 | fn get_governing_token_mint(&self) -> Pubkey { 30 | self.governing_token_mint 31 | } 32 | 33 | fn get_governing_token_owner(&self) -> Pubkey { 34 | self.governing_token_owner 35 | } 36 | 37 | fn get_realm(&self) -> Pubkey { 38 | self.realm 39 | } 40 | 41 | fn get_voter_weight(&self) -> u64 { 42 | self.voter_weight 43 | } 44 | 45 | // The GenericVoterWeight interface expects a crate-defined VoterWeightAction. 46 | // This is identical to spl_governance_addin_api::voter_weight::VoterWeightAction, but added here 47 | // so that Anchor will create the mapping correctly in the IDL. 48 | // This function converts the spl_governance_addin_api::voter_weight::VoterWeightAction to the 49 | // crate-defined VoterWeightAction by mapping the enum values by integer. 50 | // Note - it is imperative that the two enums stay in sync to avoid errors here. 51 | fn get_weight_action(&self) -> Option { 52 | self.weight_action.clone() 53 | // .clone() 54 | // .map(|x| FromPrimitive::from_u32(x as u32).unwrap()) 55 | } 56 | 57 | fn get_weight_action_target(&self) -> Option { 58 | self.weight_action_target 59 | } 60 | 61 | fn get_voter_weight_expiry(&self) -> Option { 62 | self.voter_weight_expiry 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /programs/shared/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate core; 2 | 3 | pub mod anchor; 4 | pub mod compose; 5 | pub mod error; 6 | pub mod generic_max_voter_weight; 7 | pub mod generic_voter_weight; 8 | pub mod mint; 9 | pub mod token_owner_record; 10 | -------------------------------------------------------------------------------- /programs/shared/src/mint.rs: -------------------------------------------------------------------------------- 1 | // Add the generic max voter weight trait to SPL-Token mint structs 2 | use crate::generic_max_voter_weight::GenericMaxVoterWeight; 3 | use anchor_lang::prelude::Pubkey; 4 | use spl_token::state::Mint; 5 | 6 | pub struct MintMaxVoterWeight { 7 | pub mint: Mint, 8 | pub key: Pubkey, 9 | } 10 | 11 | impl GenericMaxVoterWeight for MintMaxVoterWeight { 12 | fn get_governing_token_mint(&self) -> Pubkey { 13 | self.key 14 | } 15 | 16 | /// By default, the max voter weight is equal to the total supply of governance tokens 17 | fn get_max_voter_weight(&self) -> u64 { 18 | self.mint.supply 19 | } 20 | 21 | // when using a governing token - the max voter weight has no expiry 22 | fn get_max_voter_weight_expiry(&self) -> Option { 23 | None 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /programs/shared/src/token_owner_record.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::Pubkey; 2 | // Add the generic voter weight trait to TokenOwnerRecord structs 3 | use crate::generic_voter_weight::GenericVoterWeight; 4 | use spl_governance::state::token_owner_record::TokenOwnerRecordV2; 5 | use spl_governance_addin_api::voter_weight::VoterWeightAction; 6 | 7 | impl GenericVoterWeight for TokenOwnerRecordV2 { 8 | fn get_governing_token_mint(&self) -> Pubkey { 9 | self.governing_token_mint 10 | } 11 | 12 | fn get_governing_token_owner(&self) -> Pubkey { 13 | self.governing_token_owner 14 | } 15 | 16 | fn get_realm(&self) -> Pubkey { 17 | self.realm 18 | } 19 | 20 | fn get_voter_weight(&self) -> u64 { 21 | self.governing_token_deposit_amount 22 | } 23 | 24 | fn get_weight_action(&self) -> Option { 25 | None 26 | } 27 | 28 | fn get_weight_action_target(&self) -> Option { 29 | None 30 | } 31 | 32 | fn get_voter_weight_expiry(&self) -> Option { 33 | None 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /programs/token-haver/.tests/create_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::program_test::token_haver_test::RealmVoterTest; 2 | use program_test::tools::assert_ix_err; 3 | use solana_program::instruction::InstructionError; 4 | use solana_program_test::*; 5 | use solana_sdk::transport::TransportError; 6 | 7 | mod program_test; 8 | 9 | #[tokio::test] 10 | async fn test_create_voter_weight_record() -> Result<(), TransportError> { 11 | // Arrange 12 | let mut realm_voter_test = RealmVoterTest::start_new().await; 13 | 14 | let realm_cookie = realm_voter_test.governance.with_realm().await?; 15 | 16 | let registrar_cookie = realm_voter_test.with_registrar(&realm_cookie).await?; 17 | 18 | let voter_cookie = realm_voter_test.bench.with_wallet().await; 19 | 20 | // Act 21 | let voter_weight_record_cookie = realm_voter_test 22 | .with_voter_weight_record(®istrar_cookie, &voter_cookie) 23 | .await?; 24 | 25 | // Assert 26 | 27 | let voter_weight_record = realm_voter_test 28 | .get_voter_weight_record(&voter_weight_record_cookie.address) 29 | .await; 30 | 31 | assert_eq!(voter_weight_record_cookie.account, voter_weight_record); 32 | 33 | Ok(()) 34 | } 35 | 36 | #[tokio::test] 37 | async fn test_create_voter_weight_record_with_already_exists_error() -> Result<(), TransportError> { 38 | // Arrange 39 | let mut realm_voter_test = RealmVoterTest::start_new().await; 40 | 41 | let realm_cookie = realm_voter_test.governance.with_realm().await?; 42 | 43 | let registrar_cookie = realm_voter_test.with_registrar(&realm_cookie).await?; 44 | 45 | let voter_cookie = realm_voter_test.bench.with_wallet().await; 46 | 47 | realm_voter_test 48 | .with_voter_weight_record(®istrar_cookie, &voter_cookie) 49 | .await?; 50 | 51 | realm_voter_test.bench.advance_clock().await; 52 | 53 | // Act 54 | let err = realm_voter_test 55 | .with_voter_weight_record(®istrar_cookie, &voter_cookie) 56 | .await 57 | .err() 58 | .unwrap(); 59 | 60 | // Assert 61 | 62 | // InstructionError::Custom(0) is returned for TransactionError::AccountInUse 63 | assert_ix_err(err, InstructionError::Custom(0)); 64 | 65 | Ok(()) 66 | } 67 | -------------------------------------------------------------------------------- /programs/token-haver/.tests/fixtures/spl_governance.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-labs/governance-program-library/c7a49fa2a663c9bdf1260827c0d9b15790ebf24b/programs/token-haver/.tests/fixtures/spl_governance.so -------------------------------------------------------------------------------- /programs/token-haver/.tests/program_test/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod governance_test; 2 | pub mod program_test_bench; 3 | pub mod token_haver_test; 4 | pub mod tools; 5 | -------------------------------------------------------------------------------- /programs/token-haver/.tests/program_test/tools.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::ERROR_CODE_OFFSET; 2 | use gpl_token_haver::error::TokenHaverError; 3 | use solana_program::instruction::InstructionError; 4 | use solana_program_test::BanksClientError; 5 | use solana_sdk::{signature::Keypair, transaction::TransactionError, transport::TransportError}; 6 | use spl_governance_tools::error::GovernanceToolsError; 7 | 8 | pub fn clone_keypair(source: &Keypair) -> Keypair { 9 | Keypair::from_bytes(&source.to_bytes()).unwrap() 10 | } 11 | 12 | /// NOP (No Operation) Override function 13 | #[allow(non_snake_case)] 14 | pub fn NopOverride(_: &mut T) {} 15 | 16 | #[allow(dead_code)] 17 | pub fn assert_realm_voter_err( 18 | banks_client_error: BanksClientError, 19 | realm_voter_error: TokenHaverError, 20 | ) { 21 | let tx_error = banks_client_error.unwrap(); 22 | 23 | match tx_error { 24 | TransactionError::InstructionError(_, instruction_error) => match instruction_error { 25 | InstructionError::Custom(e) => { 26 | assert_eq!(e, realm_voter_error as u32 + ERROR_CODE_OFFSET) 27 | } 28 | _ => panic!("{:?} Is not InstructionError::Custom()", instruction_error), 29 | }, 30 | _ => panic!("{:?} Is not InstructionError", tx_error), 31 | }; 32 | } 33 | 34 | #[allow(dead_code)] 35 | pub fn assert_gov_tools_err( 36 | banks_client_error: TransportError, 37 | gov_tools_error: GovernanceToolsError, 38 | ) { 39 | let tx_error = banks_client_error.unwrap(); 40 | 41 | match tx_error { 42 | TransactionError::InstructionError(_, instruction_error) => match instruction_error { 43 | InstructionError::Custom(e) => { 44 | assert_eq!(e, gov_tools_error as u32) 45 | } 46 | _ => panic!("{:?} Is not InstructionError::Custom()", instruction_error), 47 | }, 48 | _ => panic!("{:?} Is not InstructionError", tx_error), 49 | }; 50 | } 51 | 52 | #[allow(dead_code)] 53 | pub fn assert_anchor_err( 54 | banks_client_error: BanksClientError, 55 | anchor_error: anchor_lang::error::ErrorCode, 56 | ) { 57 | let tx_error = banks_client_error.unwrap(); 58 | 59 | match tx_error { 60 | TransactionError::InstructionError(_, instruction_error) => match instruction_error { 61 | InstructionError::Custom(e) => { 62 | assert_eq!(e, anchor_error as u32) 63 | } 64 | _ => panic!("{:?} Is not InstructionError::Custom()", instruction_error), 65 | }, 66 | _ => panic!("{:?} Is not InstructionError", tx_error), 67 | }; 68 | } 69 | 70 | #[allow(dead_code)] 71 | pub fn assert_ix_err(banks_client_error: BanksClientError, ix_error: InstructionError) { 72 | let tx_error = banks_client_error.unwrap(); 73 | 74 | match tx_error { 75 | TransactionError::InstructionError(_, instruction_error) => { 76 | assert_eq!(instruction_error, ix_error); 77 | } 78 | _ => panic!("{:?} Is not InstructionError", tx_error), 79 | }; 80 | } 81 | -------------------------------------------------------------------------------- /programs/token-haver/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gpl-token-haver" 3 | version = "0.0.2" 4 | description = "SPL Governance plugin granting governance power based on the nonzero presence of locked tokens" 5 | license = "Apache-2.0" 6 | edition = "2018" 7 | 8 | [lib] 9 | crate-type = ["cdylib", "lib"] 10 | name = "gpl_token_haver" 11 | 12 | [features] 13 | no-entrypoint = [] 14 | no-idl = [] 15 | no-log-ix-name = [] 16 | cpi = ["no-entrypoint"] 17 | default = [] 18 | idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] 19 | 20 | [dependencies] 21 | arrayref = "0.3.6" 22 | anchor-lang = { version = "0.30.1" } 23 | anchor-spl = "0.30.1" 24 | solana-program = "1.18.18" 25 | spl-governance = { version = "4.0.0", features = ["no-entrypoint"] } 26 | spl-governance-tools= "0.1.4" 27 | spl-token = { version = "4.0.0", features = [ "no-entrypoint" ] } 28 | 29 | # tests are incomplete 30 | # [dev-dependencies] 31 | # borsh = "0.10.3" 32 | # solana-sdk = "1.18.18" 33 | # solana-program-test = "1.18.18" -------------------------------------------------------------------------------- /programs/token-haver/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /programs/token-haver/readme.md: -------------------------------------------------------------------------------- 1 | This plugin checks for the presence of nonzero tokens from certain mints in the user's wallet. 2 | 3 | ### You would use this if: 4 | 5 | - You want voting power to be based on having an _indefinitely locked_ token, but not proportional to the amount of the token 6 | - You don't want to use a Membership (for UX reasons) 7 | -------------------------------------------------------------------------------- /programs/token-haver/src/error.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum TokenHaverError { 5 | #[msg("Invalid Realm Authority")] 6 | InvalidRealmAuthority, 7 | 8 | #[msg("Invalid Realm for Registrar")] 9 | InvalidRealmForRegistrar, 10 | 11 | #[msg("Invalid VoterWeightRecord Realm")] 12 | InvalidVoterWeightRecordRealm, 13 | 14 | #[msg("Invalid VoterWeightRecord Mint")] 15 | InvalidVoterWeightRecordMint, 16 | 17 | #[msg("Governing TokenOwner must match")] 18 | GoverningTokenOwnerMustMatch, 19 | 20 | #[msg("All token accounts must be owned by the governing token owner")] 21 | TokenAccountWrongOwner, 22 | 23 | #[msg("All token accounts' mints must be included in the registrar")] 24 | TokenAccountWrongMint, 25 | 26 | #[msg("All token accounts must be locked")] 27 | TokenAccountNotLocked, 28 | 29 | #[msg("All token accounts' mints must be unique")] 30 | TokenAccountDuplicateMint, 31 | } 32 | -------------------------------------------------------------------------------- /programs/token-haver/src/instructions/configure_mints.rs: -------------------------------------------------------------------------------- 1 | use crate::error::TokenHaverError; 2 | use crate::state::*; 3 | use anchor_lang::system_program::Transfer; 4 | use anchor_lang::{prelude::*, system_program}; 5 | use spl_governance::state::realm; 6 | 7 | /// Configures mints for Registrar 8 | #[derive(Accounts)] 9 | #[instruction(mints: Vec)] 10 | pub struct ConfigureMints<'info> { 11 | /// The Registrar for the given realm and governing_token_mint 12 | #[account(mut)] 13 | pub registrar: Account<'info, Registrar>, 14 | 15 | #[account( 16 | address = registrar.realm @ TokenHaverError::InvalidRealmForRegistrar, 17 | owner = registrar.governance_program_id 18 | )] 19 | /// CHECK: Owned by spl-governance instance specified in registrar.governance_program_id 20 | pub realm: UncheckedAccount<'info>, 21 | 22 | // will pay in the event of a resize 23 | pub payer: Signer<'info>, 24 | 25 | /// Authority of the Realm must sign and match realm.authority 26 | pub realm_authority: Signer<'info>, 27 | 28 | pub system_program: Program<'info, System>, 29 | } 30 | 31 | pub fn configure_mints(ctx: Context, mints: Vec) -> Result<()> { 32 | let new_size = Registrar::get_space(mints.len() as u8); 33 | 34 | let rent = Rent::get()?; 35 | let new_minimum_balance = rent.minimum_balance(new_size); 36 | 37 | let lamports_diff = 38 | new_minimum_balance.saturating_sub(ctx.accounts.registrar.to_account_info().lamports()); 39 | 40 | // if lamports_diff is positive, we need to fund the account 41 | if lamports_diff > 0 { 42 | // Create a CPI context for the transfer 43 | let cpi_accounts = Transfer { 44 | from: ctx.accounts.payer.to_account_info().clone(), 45 | to: ctx.accounts.registrar.to_account_info().clone(), 46 | }; 47 | 48 | let cpi_program = ctx.accounts.system_program.to_account_info(); 49 | let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts); 50 | 51 | // Perform the transfer 52 | system_program::transfer(cpi_ctx, lamports_diff)?; 53 | } 54 | 55 | let registrar = &mut ctx.accounts.registrar; 56 | registrar.to_account_info().realloc(new_size, false)?; 57 | 58 | registrar.mints = mints; 59 | 60 | let realm = realm::get_realm_data_for_governing_token_mint( 61 | ®istrar.governance_program_id, 62 | &ctx.accounts.realm, 63 | ®istrar.governing_token_mint, 64 | )?; 65 | 66 | require_eq!( 67 | realm.authority.unwrap(), 68 | ctx.accounts.realm_authority.key(), 69 | TokenHaverError::InvalidRealmAuthority 70 | ); 71 | 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /programs/token-haver/src/instructions/create_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::state::*; 2 | use anchor_lang::prelude::*; 3 | 4 | /// Creates VoterWeightRecord used by spl-gov 5 | /// This instruction should only be executed once per realm/governing_token_mint/governing_token_owner 6 | /// to create the account 7 | #[derive(Accounts)] 8 | #[instruction(governing_token_owner: Pubkey)] 9 | pub struct CreateVoterWeightRecord<'info> { 10 | // The Registrar the VoterWeightRecord account belongs to 11 | pub registrar: Account<'info, Registrar>, 12 | 13 | #[account( 14 | init, 15 | seeds = [ b"voter-weight-record".as_ref(), 16 | registrar.realm.key().as_ref(), 17 | registrar.governing_token_mint.key().as_ref(), 18 | governing_token_owner.as_ref()], 19 | bump, 20 | payer = payer, 21 | space = VoterWeightRecord::get_space() 22 | )] 23 | pub voter_weight_record: Account<'info, VoterWeightRecord>, 24 | 25 | #[account(mut)] 26 | pub payer: Signer<'info>, 27 | 28 | pub system_program: Program<'info, System>, 29 | } 30 | 31 | pub fn create_voter_weight_record( 32 | ctx: Context, 33 | governing_token_owner: Pubkey, 34 | ) -> Result<()> { 35 | let voter_weight_record = &mut ctx.accounts.voter_weight_record; 36 | let registrar = &ctx.accounts.registrar; 37 | 38 | voter_weight_record.realm = registrar.realm.key(); 39 | voter_weight_record.governing_token_mint = registrar.governing_token_mint.key(); 40 | voter_weight_record.governing_token_owner = governing_token_owner; 41 | 42 | // Set expiry to expired 43 | voter_weight_record.voter_weight_expiry = Some(0); 44 | 45 | Ok(()) 46 | } 47 | -------------------------------------------------------------------------------- /programs/token-haver/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub use create_registrar::*; 2 | mod create_registrar; 3 | 4 | pub use create_voter_weight_record::*; 5 | mod create_voter_weight_record; 6 | 7 | pub use update_voter_weight_record::*; 8 | mod update_voter_weight_record; 9 | 10 | pub use configure_mints::*; 11 | mod configure_mints; 12 | -------------------------------------------------------------------------------- /programs/token-haver/src/instructions/update_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::error::TokenHaverError; 2 | use crate::state::*; 3 | use anchor_lang::prelude::*; 4 | use anchor_spl::token::TokenAccount; 5 | 6 | /// Updates VoterWeightRecord based on Realm DAO membership 7 | /// The membership is evaluated via a valid TokenOwnerRecord which must belong to one of the configured spl-governance instances 8 | /// 9 | /// This instruction sets VoterWeightRecord.voter_weight which is valid for the current slot only 10 | /// and must be executed inside the same transaction as the corresponding spl-gov instruction 11 | #[derive(Accounts)] 12 | pub struct UpdateVoterWeightRecord<'info> { 13 | /// The RealmVoter voting Registrar 14 | pub registrar: Account<'info, Registrar>, 15 | 16 | #[account( 17 | mut, 18 | constraint = voter_weight_record.realm == registrar.realm 19 | @ TokenHaverError::InvalidVoterWeightRecordRealm, 20 | 21 | constraint = voter_weight_record.governing_token_mint == registrar.governing_token_mint 22 | @ TokenHaverError::InvalidVoterWeightRecordMint, 23 | )] 24 | pub voter_weight_record: Account<'info, VoterWeightRecord>, 25 | } 26 | 27 | pub fn update_voter_weight_record<'info>( 28 | ctx: Context<'_, '_, 'info, 'info, UpdateVoterWeightRecord<'info>>, 29 | ) -> Result<()> { 30 | let registrar = &ctx.accounts.registrar; 31 | let voter_weight_record = &mut ctx.accounts.voter_weight_record; 32 | 33 | let nonzero_token_accounts: Vec> = ctx 34 | .remaining_accounts 35 | .iter() 36 | .map(|account| Account::::try_from(account).unwrap()) 37 | .filter(|account| account.amount > 0) // filter out zero balance accounts 38 | .collect(); 39 | 40 | for account in nonzero_token_accounts.iter() { 41 | // Throw an error if a token account's owner doesnt match token_owner_record.governing_token_owner 42 | require_eq!( 43 | account.owner, 44 | voter_weight_record.governing_token_owner, 45 | TokenHaverError::TokenAccountWrongOwner 46 | ); 47 | 48 | // Throw an error if a token account's mint is not unique amount the accounts 49 | require!( 50 | nonzero_token_accounts 51 | .iter() 52 | .filter(|a| a.mint == account.mint) 53 | .count() 54 | == 1, 55 | TokenHaverError::TokenAccountDuplicateMint 56 | ); 57 | 58 | // Throw an error if a token account's mint isn't in registrar.mints 59 | require!( 60 | registrar.mints.contains(&account.mint), 61 | TokenHaverError::TokenAccountWrongMint 62 | ); 63 | // Throw an error if a token account is not frozen 64 | require!(account.is_frozen(), TokenHaverError::TokenAccountNotLocked); 65 | } 66 | 67 | // Setup voter_weight 68 | voter_weight_record.voter_weight = (nonzero_token_accounts.len() as u64) * 1_000_000; 69 | 70 | // Record is only valid as of the current slot 71 | voter_weight_record.voter_weight_expiry = Some(Clock::get()?.slot); 72 | Ok(()) 73 | } 74 | -------------------------------------------------------------------------------- /programs/token-haver/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | pub mod error; 4 | 5 | mod instructions; 6 | use instructions::*; 7 | 8 | pub mod state; 9 | 10 | pub mod tools; 11 | 12 | declare_id!("7gobfUihgoxA14RUnVaseoah89ggCgYAzgz1JoaPAXam"); 13 | 14 | #[program] 15 | pub mod token_haver { 16 | 17 | use super::*; 18 | 19 | pub fn create_registrar(ctx: Context, mints: Vec) -> Result<()> { 20 | log_version(); 21 | instructions::create_registrar(ctx, mints) 22 | } 23 | pub fn create_voter_weight_record( 24 | ctx: Context, 25 | governing_token_owner: Pubkey, 26 | ) -> Result<()> { 27 | log_version(); 28 | instructions::create_voter_weight_record(ctx, governing_token_owner) 29 | } 30 | pub fn update_voter_weight_record<'info>( 31 | ctx: Context<'_, '_, 'info, 'info, UpdateVoterWeightRecord<'info>>, 32 | ) -> Result<()> { 33 | log_version(); 34 | instructions::update_voter_weight_record(ctx) 35 | } 36 | pub fn configure_mints(ctx: Context, mints: Vec) -> Result<()> { 37 | log_version(); 38 | instructions::configure_mints(ctx, mints) 39 | } 40 | } 41 | 42 | fn log_version() { 43 | // TODO: Check if Anchor allows to log it before instruction is deserialized 44 | msg!("VERSION:{:?}", env!("CARGO_PKG_VERSION")); 45 | } 46 | -------------------------------------------------------------------------------- /programs/token-haver/src/state/max_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::id; 2 | use crate::tools::anchor::DISCRIMINATOR_SIZE; 3 | use anchor_lang::prelude::Pubkey; 4 | use anchor_lang::prelude::*; 5 | use solana_program::pubkey::PUBKEY_BYTES; 6 | 7 | /// MaxVoterWeightRecord account as defined in spl-governance-addin-api 8 | /// It's redefined here without account_discriminator for Anchor to treat it as native account 9 | /// 10 | /// The account is used as an api interface to provide max voting power to the governance program from external addin contracts 11 | #[account] 12 | #[derive(Debug, PartialEq)] 13 | pub struct MaxVoterWeightRecord { 14 | /// The Realm the MaxVoterWeightRecord belongs to 15 | pub realm: Pubkey, 16 | 17 | /// Governing Token Mint the MaxVoterWeightRecord is associated with 18 | /// Note: The addin can take deposits of any tokens and is not restricted to the community or council tokens only 19 | // The mint here is to link the record to either community or council mint of the realm 20 | pub governing_token_mint: Pubkey, 21 | 22 | /// Max voter weight 23 | /// The max voter weight provided by the addin for the given realm and governing_token_mint 24 | pub max_voter_weight: u64, 25 | 26 | /// The slot when the max voting weight expires 27 | /// It should be set to None if the weight never expires 28 | /// If the max vote weight decays with time, for example for time locked based weights, then the expiry must be set 29 | /// As a pattern Revise instruction to update the max weight should be invoked before governance instruction within the same transaction 30 | /// and the expiry set to the current slot to provide up to date weight 31 | pub max_voter_weight_expiry: Option, 32 | 33 | /// Reserved space for future versions 34 | pub reserved: [u8; 8], 35 | } 36 | 37 | impl Default for MaxVoterWeightRecord { 38 | fn default() -> Self { 39 | Self { 40 | realm: Default::default(), 41 | governing_token_mint: Default::default(), 42 | max_voter_weight: Default::default(), 43 | max_voter_weight_expiry: Some(0), 44 | reserved: Default::default(), 45 | } 46 | } 47 | } 48 | 49 | impl MaxVoterWeightRecord { 50 | pub fn get_space() -> usize { 51 | DISCRIMINATOR_SIZE + PUBKEY_BYTES * 2 + 8 + 1 + 8 + 8 52 | } 53 | } 54 | 55 | /// Returns MaxVoterWeightRecord PDA seeds 56 | pub fn get_max_voter_weight_record_seeds<'a>( 57 | realm: &'a Pubkey, 58 | governing_token_mint: &'a Pubkey, 59 | ) -> [&'a [u8]; 3] { 60 | [ 61 | b"max-voter-weight-record", 62 | realm.as_ref(), 63 | governing_token_mint.as_ref(), 64 | ] 65 | } 66 | 67 | /// Returns MaxVoterWeightRecord PDA address 68 | pub fn get_max_voter_weight_record_address( 69 | realm: &Pubkey, 70 | governing_token_mint: &Pubkey, 71 | ) -> Pubkey { 72 | Pubkey::find_program_address( 73 | &get_max_voter_weight_record_seeds(realm, governing_token_mint), 74 | &id(), 75 | ) 76 | .0 77 | } 78 | 79 | #[cfg(test)] 80 | mod test { 81 | 82 | use super::*; 83 | 84 | #[test] 85 | fn test_get_space() { 86 | // Arrange 87 | let expected_space = MaxVoterWeightRecord::get_space(); 88 | 89 | // Act 90 | let actual_space = 91 | DISCRIMINATOR_SIZE + MaxVoterWeightRecord::default().try_to_vec().unwrap().len(); 92 | 93 | // Assert 94 | assert_eq!(expected_space, actual_space); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /programs/token-haver/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | pub use registrar::*; 2 | pub mod registrar; 3 | 4 | pub mod max_voter_weight_record; 5 | 6 | pub use voter_weight_record::*; 7 | pub mod voter_weight_record; 8 | -------------------------------------------------------------------------------- /programs/token-haver/src/state/registrar.rs: -------------------------------------------------------------------------------- 1 | use crate::{id, tools::anchor::DISCRIMINATOR_SIZE}; 2 | use anchor_lang::prelude::*; 3 | use borsh::{BorshDeserialize, BorshSchema, BorshSerialize}; 4 | use solana_program::pubkey::PUBKEY_BYTES; 5 | 6 | /// Enum defining collection item change type 7 | #[derive(Clone, Debug, PartialEq, BorshDeserialize, BorshSerialize, BorshSchema)] 8 | pub enum CollectionItemChangeType { 9 | /// Update item in the collection if it already exists and Insert the item if it doesn't 10 | Upsert, 11 | /// Remove item from the collection 12 | Remove, 13 | } 14 | 15 | /// Registrar which stores spl-governance configurations for the given Realm 16 | #[account] 17 | #[derive(Debug, PartialEq)] 18 | pub struct Registrar { 19 | /// spl-governance program the Realm belongs to 20 | pub governance_program_id: Pubkey, 21 | 22 | /// Realm of the Registrar 23 | pub realm: Pubkey, 24 | 25 | /// Governing token mint the Registrar is for 26 | /// It can either be the Community or the Council mint of the Realm 27 | /// When the plugin is enabled the mint is only used as the identity of the governing power (voting population) 28 | /// and the actual token of the mint is not used 29 | pub governing_token_mint: Pubkey, 30 | 31 | pub mints: Vec, 32 | } 33 | 34 | impl Registrar { 35 | pub fn get_space(max_mints: u8) -> usize { 36 | DISCRIMINATOR_SIZE + PUBKEY_BYTES * 3 + 4 + max_mints as usize * PUBKEY_BYTES 37 | } 38 | } 39 | 40 | /// Returns Registrar PDA seeds 41 | pub fn get_registrar_seeds<'a>( 42 | realm: &'a Pubkey, 43 | governing_token_mint: &'a Pubkey, 44 | ) -> [&'a [u8]; 3] { 45 | [b"registrar", realm.as_ref(), governing_token_mint.as_ref()] 46 | } 47 | 48 | /// Returns Registrar PDA address 49 | pub fn get_registrar_address(realm: &Pubkey, governing_token_mint: &Pubkey) -> Pubkey { 50 | Pubkey::find_program_address(&get_registrar_seeds(realm, governing_token_mint), &id()).0 51 | } 52 | 53 | #[cfg(test)] 54 | mod test { 55 | 56 | use super::*; 57 | 58 | #[test] 59 | fn test_get_space() { 60 | // Arrange 61 | let expected_space = Registrar::get_space(3); 62 | 63 | let registrar = Registrar { 64 | governance_program_id: Pubkey::default(), 65 | realm: Pubkey::default(), 66 | governing_token_mint: Pubkey::default(), 67 | mints: vec![Pubkey::default(), Pubkey::default(), Pubkey::default()], 68 | }; 69 | 70 | // Act 71 | let actual_space = DISCRIMINATOR_SIZE + registrar.try_to_vec().unwrap().len(); 72 | 73 | // Assert 74 | assert_eq!(expected_space, actual_space); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /programs/token-haver/src/tools/anchor.rs: -------------------------------------------------------------------------------- 1 | pub const DISCRIMINATOR_SIZE: usize = 8; 2 | -------------------------------------------------------------------------------- /programs/token-haver/src/tools/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod anchor; 2 | -------------------------------------------------------------------------------- /programs/token-voter/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gpl-token-voter" 3 | version = "0.0.1" 4 | description = "SPL Governance plugin implementing token based governance power" 5 | license = "Apache-2.0" 6 | edition = "2018" 7 | 8 | [lib] 9 | crate-type = ["cdylib", "lib"] 10 | name = "gpl_token_voter" 11 | 12 | [features] 13 | no-entrypoint = [] 14 | no-idl = [] 15 | no-log-ix-name = [] 16 | cpi = ["no-entrypoint"] 17 | default = [] 18 | idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] 19 | 20 | [dependencies] 21 | anchor-lang = { version = "0.30.1", features = ["init-if-needed"] } 22 | anchor-spl = "0.30.1" 23 | arrayref = "0.3.7" 24 | solana-program = "1.18.18" 25 | spl-governance = { version = "4.0.0", features = ["no-entrypoint"] } 26 | spl-tlv-account-resolution = { version = "0.6.3" } 27 | spl-transfer-hook-interface = { version = "0.6.3" } 28 | spl-governance-tools = "0.1.4" 29 | spl-token = { version = "4.0.0", features = [ "no-entrypoint" ] } 30 | spl-token-2022 = { version = "3.0.4", features = [ "no-entrypoint" ] } 31 | ahash = "=0.8.7" 32 | static_assertions = "1.1" 33 | spl-governance-addin-api = "0.1.4" 34 | 35 | 36 | [dev-dependencies] 37 | borsh = "0.10.3" 38 | spl-associated-token-account = { version = "^3.0.2", features = ["no-entrypoint"] } 39 | spl-transfer-hook-example = { version = "0.6.0", features = ["no-entrypoint"] } 40 | solana-sdk = "1.18.18" 41 | solana-program-test = "1.18.18" 42 | log = "0.4.14" 43 | env_logger = "0.9.0" 44 | spl-token-client = "0.10.0" 45 | -------------------------------------------------------------------------------- /programs/token-voter/Xargo.toml: -------------------------------------------------------------------------------- 1 | [target.bpfel-unknown-unknown.dependencies.std] 2 | features = [] 3 | -------------------------------------------------------------------------------- /programs/token-voter/src/error.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | #[error_code] 4 | pub enum TokenVoterError { 5 | #[msg("Invalid Realm Authority")] 6 | InvalidRealmAuthority, 7 | 8 | #[msg("Invalid Realm for Registrar")] 9 | InvalidRealmForRegistrar, 10 | 11 | #[msg("Invalid MaxVoterWeightRecord Realm")] 12 | InvalidMaxVoterWeightRecordRealm, 13 | 14 | #[msg("Invalid MaxVoterWeightRecord Mint")] 15 | InvalidMaxVoterWeightRecordMint, 16 | 17 | #[msg("Invalid VoterWeightRecord Realm")] 18 | InvalidVoterWeightRecordRealm, 19 | 20 | #[msg("Invalid VoterWeightRecord Mint")] 21 | InvalidVoterWeightRecordMint, 22 | 23 | #[msg("Invalid TokenOwner for VoterWeightRecord")] 24 | InvalidTokenOwnerForVoterWeightRecord, 25 | 26 | #[msg("Mathematical Overflow")] 27 | Overflow, 28 | 29 | /// Invalid Token account owner 30 | #[msg("Invalid Token account owner")] 31 | SplTokenAccountWithInvalidOwner, 32 | 33 | /// Invalid Mint account owner 34 | #[msg("Invalid Mint account owner")] 35 | SplTokenMintWithInvalidOwner, 36 | 37 | /// Token Account doesn't exist 38 | #[msg("Token Account doesn't exist")] 39 | SplTokenAccountDoesNotExist, 40 | 41 | /// Token account data is invalid 42 | #[msg("Token account data is invalid")] 43 | SplTokenInvalidTokenAccountData, 44 | 45 | /// Token mint account data is invalid 46 | #[msg("Token mint account data is invalid")] 47 | SplTokenInvalidMintAccountData, 48 | 49 | /// Token Mint is not initialized 50 | #[msg("Token Mint account is not initialized")] 51 | SplTokenMintNotInitialized, 52 | 53 | /// Token Mint account doesn't exist 54 | #[msg("Token Mint account doesn't exist")] 55 | SplTokenMintDoesNotExist, 56 | 57 | /// Account data is empty or invalid 58 | #[msg("Account Data is empty or invalid")] 59 | InvalidAccountData, 60 | 61 | /// Math Overflow in VoterWeight 62 | #[msg("Math Overflow in VoterWeight")] 63 | VoterWeightOverflow, 64 | 65 | #[msg("Mint Not Found in Mint Configs")] 66 | MintNotFound, 67 | 68 | #[msg("Governing TokenOwner must match")] 69 | GoverningTokenOwnerMustMatch, 70 | 71 | #[msg("Invalid Token Owner Records")] 72 | InvalidTokenOwnerRecord, 73 | 74 | #[msg("Index is out of Deposit Entry bounds")] 75 | OutOfBoundsDepositEntryIndex, 76 | 77 | #[msg("No Cpi Allowed")] 78 | ForbiddenCpi, 79 | 80 | #[msg("Voting Tokens are not withdrawn")] 81 | VotingTokenNonZero, 82 | 83 | #[msg("Vault Tokens are not withdrawn")] 84 | VaultTokenNonZero, 85 | 86 | #[msg("Invalid Voter Token Authority")] 87 | InvalidAuthority, 88 | 89 | /// Token Amount Overflow 90 | #[msg("Math Overflow in Token Amount")] 91 | TokenAmountOverflow, 92 | 93 | /// Withdrawal in the same slot. 94 | #[msg("Cannot Withdraw in the same slot")] 95 | CannotWithdraw, 96 | 97 | #[msg("Resizing Max Mints cannot be smaller than Configure Mint Configs")] 98 | InvalidResizeMaxMints, 99 | 100 | #[msg("Mint Index mismatch!")] 101 | MintIndexMismatch, 102 | 103 | #[msg("Inactive Deposit Index!")] 104 | DepositIndexInactive, 105 | } 106 | -------------------------------------------------------------------------------- /programs/token-voter/src/instructions/close_voter.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{ 3 | error::*, 4 | state::*, 5 | tools::spl_token::{get_spl_token_amount, get_spl_token_owner}, 6 | }, 7 | anchor_lang::prelude::*, 8 | anchor_spl::{ 9 | token_2022::{self, CloseAccount}, 10 | token_interface::TokenInterface, 11 | }, 12 | }; 13 | 14 | // Remaining accounts must be all the token accounts owned by voter account they want to close, 15 | // they should be writable so that they can be closed and sol rented 16 | // would be transfered to sol_destination 17 | #[derive(Accounts)] 18 | pub struct CloseVoter<'info> { 19 | pub registrar: Box>, 20 | 21 | #[account( 22 | mut, 23 | seeds = [voter.registrar.as_ref(), b"voter".as_ref(), voter_authority.key().as_ref()], 24 | bump = voter.voter_bump, 25 | close = sol_destination 26 | )] 27 | pub voter: Box>, 28 | 29 | #[account( 30 | mut, 31 | seeds = [registrar.key().as_ref(), b"voter-weight-record".as_ref(), voter_authority.key().as_ref()], 32 | bump, 33 | close = sol_destination 34 | )] 35 | pub voter_weight_record: Box>, 36 | 37 | pub voter_authority: Signer<'info>, 38 | 39 | /// CHECK: Destination may be any address. 40 | #[account(mut)] 41 | pub sol_destination: UncheckedAccount<'info>, 42 | 43 | pub token_program: Interface<'info, TokenInterface>, 44 | } 45 | 46 | /// Closes the voter account (Optionally, also token vaults, as part of remaining_accounts), 47 | /// allowing one to retrieve rent exemption SOL. 48 | /// Only accounts with no remaining deposits can be closed. 49 | /// 50 | /// Tokens must be withdrawn first to be able to close voter and close the token accounts. 51 | pub fn close_voter<'info>(ctx: Context<'_, '_, '_, 'info, CloseVoter<'info>>) -> Result<()> { 52 | let voter = &ctx.accounts.voter; 53 | let voter_authority = &ctx.accounts.voter_authority; 54 | let amount = voter.deposits.iter().fold(0u64, |sum, d| { 55 | sum.checked_add(d.amount_deposited_native).unwrap() 56 | }); 57 | require_eq!(amount, 0, TokenVoterError::VotingTokenNonZero); 58 | let voter_authority_key = voter_authority.key(); 59 | let voter_seeds = voter_seeds!(voter, voter_authority_key); 60 | 61 | for token_account in ctx.remaining_accounts { 62 | let token_account_clone = &token_account.clone(); 63 | let token_owner = get_spl_token_owner(token_account_clone)?; 64 | let token_amount = get_spl_token_amount(token_account_clone)?; 65 | 66 | require_keys_eq!( 67 | token_owner, 68 | ctx.accounts.voter.key(), 69 | TokenVoterError::InvalidAuthority 70 | ); 71 | require_eq!(token_amount, 0, TokenVoterError::VaultTokenNonZero); 72 | 73 | let cpi_accounts = CloseAccount { 74 | account: token_account.to_account_info(), 75 | destination: ctx.accounts.sol_destination.to_account_info(), 76 | authority: ctx.accounts.voter.to_account_info(), 77 | }; 78 | 79 | let cpi_program = ctx.accounts.token_program.to_account_info(); 80 | token_2022::close_account(CpiContext::new_with_signer( 81 | cpi_program, 82 | cpi_accounts, 83 | &[voter_seeds], 84 | ))?; 85 | 86 | token_account.exit(ctx.program_id)?; 87 | } 88 | 89 | Ok(()) 90 | } 91 | -------------------------------------------------------------------------------- /programs/token-voter/src/instructions/create_max_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::state::*, anchor_lang::prelude::*, anchor_spl::token_interface::Mint, 3 | spl_governance::state::realm, 4 | }; 5 | 6 | /// Creates MaxVoterWeightRecord used by spl-governance 7 | /// This instruction should only be executed once per realm/governing_token_mint to create the account 8 | #[derive(Accounts)] 9 | pub struct CreateMaxVoterWeightRecord<'info> { 10 | // The Registrar the MaxVoterWeightRecord account belongs to 11 | pub registrar: Account<'info, Registrar>, 12 | 13 | #[account( 14 | init, 15 | seeds = [ b"max-voter-weight-record".as_ref(), 16 | registrar.realm.key().as_ref(), 17 | registrar.governing_token_mint.key().as_ref()], 18 | bump, 19 | payer = payer, 20 | space = MaxVoterWeightRecord::get_space() 21 | )] 22 | pub max_voter_weight_record: Account<'info, MaxVoterWeightRecord>, 23 | 24 | /// The program id of the spl-governance program the realm belongs to 25 | /// CHECK: Can be any instance of spl-governance and it's not known at the compilation time 26 | #[account(executable)] 27 | pub governance_program_id: UncheckedAccount<'info>, 28 | 29 | #[account(owner = governance_program_id.key())] 30 | /// CHECK: Owned by spl-governance instance specified in governance_program_id 31 | pub realm: UncheckedAccount<'info>, 32 | 33 | /// Either the realm community mint or the council mint. 34 | pub realm_governing_token_mint: InterfaceAccount<'info, Mint>, 35 | 36 | #[account(mut)] 37 | pub payer: Signer<'info>, 38 | 39 | pub system_program: Program<'info, System>, 40 | } 41 | 42 | pub fn create_max_voter_weight_record(ctx: Context) -> Result<()> { 43 | // Deserialize the Realm to validate it 44 | let _realm = realm::get_realm_data_for_governing_token_mint( 45 | &ctx.accounts.governance_program_id.key(), 46 | &ctx.accounts.realm, 47 | &ctx.accounts.realm_governing_token_mint.key(), 48 | )?; 49 | 50 | let max_voter_weight_record = &mut ctx.accounts.max_voter_weight_record; 51 | let registrar = &ctx.accounts.registrar; 52 | max_voter_weight_record.account_discriminator = 53 | spl_governance_addin_api::max_voter_weight::MaxVoterWeightRecord::ACCOUNT_DISCRIMINATOR; 54 | max_voter_weight_record.realm = registrar.realm; 55 | max_voter_weight_record.governing_token_mint = registrar.governing_token_mint; 56 | 57 | // Set expiry to expired 58 | max_voter_weight_record.max_voter_weight_expiry = Some(0); 59 | 60 | Ok(()) 61 | } 62 | -------------------------------------------------------------------------------- /programs/token-voter/src/instructions/create_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{error::*, state::*}, 3 | anchor_lang::prelude::*, 4 | anchor_lang::solana_program::sysvar::instructions as tx_instructions, 5 | }; 6 | 7 | /// Creates VoterWeightRecord used by spl-gov 8 | /// This instruction should only be executed once per realm/governing_token_mint/governing_token_owner 9 | /// to create the account 10 | #[derive(Accounts)] 11 | pub struct CreateVoterWeightRecord<'info> { 12 | // The Registrar the VoterWeightRecord account belongs to 13 | pub registrar: Box>, 14 | 15 | #[account( 16 | init, 17 | seeds = [registrar.key().as_ref(), b"voter".as_ref(), voter_authority.key().as_ref()], 18 | bump, 19 | payer = voter_authority, 20 | space = Voter::get_space(registrar.max_mints), 21 | )] 22 | pub voter: Box>, 23 | 24 | #[account( 25 | init, 26 | seeds = [registrar.key().as_ref(), b"voter-weight-record".as_ref(), voter_authority.key().as_ref()], 27 | bump, 28 | payer = voter_authority, 29 | space = VoterWeightRecord::get_space() 30 | )] 31 | pub voter_weight_record: Box>, 32 | 33 | #[account(mut)] 34 | pub voter_authority: Signer<'info>, 35 | 36 | pub system_program: Program<'info, System>, 37 | 38 | /// CHECK: Address constraint is set 39 | #[account(address = tx_instructions::ID)] 40 | pub instructions: UncheckedAccount<'info>, 41 | } 42 | 43 | pub fn create_voter_weight_record(ctx: Context) -> Result<()> { 44 | // Forbid creating voter accounts from CPI. The goal is to make automation 45 | // impossible that weakens some of the limitations intentionally imposed on 46 | // tokens. 47 | { 48 | let ixns = ctx.accounts.instructions.to_account_info(); 49 | let current_index = tx_instructions::load_current_index_checked(&ixns)? as usize; 50 | let current_ixn = tx_instructions::load_instruction_at_checked(current_index, &ixns)?; 51 | require_keys_eq!( 52 | current_ixn.program_id, 53 | *ctx.program_id, 54 | TokenVoterError::ForbiddenCpi 55 | ); 56 | } 57 | 58 | let voter = &mut ctx.accounts.voter; 59 | let voter_authority = &ctx.accounts.voter_authority; 60 | let registrar = &ctx.accounts.registrar; 61 | 62 | voter.voter_bump = ctx.bumps.voter; 63 | voter.voter_weight_record_bump = ctx.bumps.voter_weight_record; 64 | voter.voter_authority = voter_authority.key(); 65 | voter.registrar = registrar.key(); 66 | voter.deposits = DepositEntry::init_deposits(registrar.max_mints as usize); 67 | 68 | let voter_weight_record = &mut ctx.accounts.voter_weight_record; 69 | 70 | voter_weight_record.account_discriminator = 71 | spl_governance_addin_api::voter_weight::VoterWeightRecord::ACCOUNT_DISCRIMINATOR; 72 | voter_weight_record.realm = registrar.realm.key(); 73 | voter_weight_record.governing_token_mint = registrar.governing_token_mint.key(); 74 | voter_weight_record.governing_token_owner = voter_authority.key(); 75 | 76 | // Set expiry to expired 77 | voter_weight_record.voter_weight_expiry = Some(0); 78 | Ok(()) 79 | } 80 | -------------------------------------------------------------------------------- /programs/token-voter/src/instructions/mod.rs: -------------------------------------------------------------------------------- 1 | pub use configure_mint_config::*; 2 | mod configure_mint_config; 3 | 4 | pub use create_registrar::*; 5 | mod create_registrar; 6 | 7 | pub use resize_registrar::*; 8 | mod resize_registrar; 9 | 10 | pub use create_voter_weight_record::*; 11 | mod create_voter_weight_record; 12 | 13 | pub use create_max_voter_weight_record::*; 14 | mod create_max_voter_weight_record; 15 | 16 | pub use close_voter::*; 17 | mod close_voter; 18 | 19 | pub use withdraw::*; 20 | mod withdraw; 21 | 22 | pub use deposit::*; 23 | mod deposit; 24 | -------------------------------------------------------------------------------- /programs/token-voter/src/instructions/resize_registrar.rs: -------------------------------------------------------------------------------- 1 | use { 2 | crate::{error::*, state::*}, 3 | anchor_lang::prelude::*, 4 | anchor_spl::token_interface::Mint, 5 | spl_governance::state::realm, 6 | }; 7 | 8 | /// Resizes Registrar storing Realm Voter configuration for spl-governance Realm 9 | /// This instruction can only be ran if the max_mint is higher than currently used voting_mint_configs length 10 | #[derive(Accounts)] 11 | #[instruction(max_mints: u8)] 12 | pub struct ResizeRegistrar<'info> { 13 | /// The Realm Voter Registrar 14 | /// There can only be a single registrar per governance Realm and governing mint of the Realm 15 | #[account( 16 | mut, 17 | seeds = [b"registrar".as_ref(), realm.key().as_ref(), governing_token_mint.key().as_ref()], 18 | bump, 19 | realloc = Registrar::get_space(max_mints), 20 | realloc::payer = payer, 21 | realloc::zero = false, 22 | )] 23 | pub registrar: Account<'info, Registrar>, 24 | 25 | /// The program id of the spl-governance program the realm belongs to 26 | /// CHECK: Can be any instance of spl-governance and it's not known at the compilation time 27 | #[account(executable)] 28 | pub governance_program_id: UncheckedAccount<'info>, 29 | 30 | /// An spl-governance Realm 31 | /// 32 | /// Realm is validated in the instruction: 33 | /// - Realm is owned by the governance_program_id 34 | /// - governing_token_mint must be the community or council mint 35 | /// - realm_authority is realm.authority 36 | /// 37 | /// CHECK: Owned by spl-governance instance specified in governance_program_id 38 | #[account(owner = governance_program_id.key())] 39 | pub realm: UncheckedAccount<'info>, 40 | 41 | /// Either the realm community mint or the council mint. 42 | /// It must match Realm.community_mint or Realm.config.council_mint 43 | /// 44 | /// Note: Once the Realm voter plugin is enabled the governing_token_mint is used only as identity 45 | /// for the voting population and the tokens of that are no longer used 46 | pub governing_token_mint: InterfaceAccount<'info, Mint>, 47 | 48 | /// realm_authority must sign and match Realm.authority 49 | pub realm_authority: Signer<'info>, 50 | 51 | #[account(mut)] 52 | pub payer: Signer<'info>, 53 | 54 | pub system_program: Program<'info, System>, 55 | } 56 | 57 | /// Resizes a Registrar which stores Realms voter configuration for the given Realm 58 | /// 59 | /// max_mints is used to allocate account size for the maximum number of configured mint instances 60 | pub fn resize_registrar(ctx: Context, max_mints: u8) -> Result<()> { 61 | let registrar = &mut ctx.accounts.registrar; 62 | 63 | require_gt!( 64 | max_mints as usize, 65 | registrar.voting_mint_configs.len(), 66 | TokenVoterError::InvalidResizeMaxMints 67 | ); 68 | 69 | registrar.max_mints = max_mints; 70 | 71 | // Verify that realm_authority is the expected authority of the Realm 72 | // and that the mint matches one of the realm mints too 73 | let realm = realm::get_realm_data_for_governing_token_mint( 74 | ®istrar.governance_program_id, 75 | &ctx.accounts.realm, 76 | ®istrar.governing_token_mint, 77 | )?; 78 | 79 | require_eq!( 80 | realm.authority.unwrap(), 81 | ctx.accounts.realm_authority.key(), 82 | TokenVoterError::InvalidRealmAuthority 83 | ); 84 | 85 | Ok(()) 86 | } 87 | -------------------------------------------------------------------------------- /programs/token-voter/src/lib.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::*; 2 | 3 | pub mod error; 4 | 5 | mod instructions; 6 | use instructions::*; 7 | 8 | pub mod state; 9 | 10 | mod governance; 11 | pub mod tools; 12 | 13 | #[macro_use] 14 | extern crate static_assertions; 15 | 16 | declare_id!("3JhBg9bSPcfWGFa3t8LH7ooVtrjm45yCkHpxYXMXstUM"); 17 | 18 | #[program] 19 | pub mod token_voter { 20 | use super::*; 21 | 22 | pub fn create_registrar(ctx: Context, max_mints: u8) -> Result<()> { 23 | log_version(); 24 | instructions::create_registrar(ctx, max_mints) 25 | } 26 | 27 | pub fn resize_registrar(ctx: Context, max_mints: u8) -> Result<()> { 28 | log_version(); 29 | instructions::resize_registrar(ctx, max_mints) 30 | } 31 | 32 | pub fn create_voter_weight_record(ctx: Context) -> Result<()> { 33 | log_version(); 34 | instructions::create_voter_weight_record(ctx) 35 | } 36 | 37 | pub fn create_max_voter_weight_record(ctx: Context) -> Result<()> { 38 | log_version(); 39 | instructions::create_max_voter_weight_record(ctx) 40 | } 41 | 42 | pub fn configure_mint_config( 43 | ctx: Context, 44 | digit_shift: i8, 45 | ) -> Result<()> { 46 | log_version(); 47 | instructions::configure_mint_config(ctx, digit_shift) 48 | } 49 | 50 | pub fn deposit<'info>( 51 | ctx: Context<'_, '_, '_, 'info, Deposit<'info>>, 52 | deposit_entry_index: u8, 53 | amount: u64, 54 | ) -> Result<()> { 55 | log_version(); 56 | instructions::deposit(ctx, deposit_entry_index, amount) 57 | } 58 | 59 | pub fn withdraw<'info>( 60 | ctx: Context<'_, '_, '_, 'info, Withdraw<'info>>, 61 | deposit_entry_index: u8, 62 | amount: u64, 63 | ) -> Result<()> { 64 | log_version(); 65 | instructions::withdraw(ctx, deposit_entry_index, amount) 66 | } 67 | 68 | pub fn close_voter<'info>(ctx: Context<'_, '_, '_, 'info, CloseVoter<'info>>) -> Result<()> { 69 | log_version(); 70 | instructions::close_voter(ctx) 71 | } 72 | } 73 | 74 | fn log_version() { 75 | msg!("VERSION:{:?}", env!("CARGO_PKG_VERSION")); 76 | } 77 | -------------------------------------------------------------------------------- /programs/token-voter/src/state/deposit_entry.rs: -------------------------------------------------------------------------------- 1 | use {crate::state::VotingMintConfig, anchor_lang::prelude::*}; 2 | 3 | /// Bookkeeping for a single deposit for a given mint. 4 | #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy, PartialEq)] 5 | pub struct DepositEntry { 6 | /// Amount in deposited, in native currency. 7 | /// Withdraws directly reduce this amount. 8 | /// 9 | /// This directly tracks the total amount added by the user. They may 10 | /// never withdraw more than this amount. 11 | pub amount_deposited_native: u64, 12 | 13 | /// Points to the VotingMintConfig this deposit uses. 14 | pub voting_mint_config_idx: u8, 15 | 16 | /// Deposit slot hash. 17 | /// saves deposit slot hash so that depositor cannot withdraw at the same slot. 18 | pub deposit_slot_hash: u64, 19 | 20 | // True if the deposit entry is being used. 21 | pub is_used: bool, 22 | 23 | /// Reserved for future upgrades 24 | pub reserved: [u8; 38], 25 | } 26 | 27 | const_assert!(std::mem::size_of::() == 8 + 1 + 8 + 1 + 38); 28 | const_assert!(std::mem::size_of::() % 8 == 0); 29 | 30 | impl DepositEntry { 31 | /// Creates a new DepositEntry with default values 32 | pub fn new() -> Self { 33 | Self { 34 | amount_deposited_native: 0, 35 | voting_mint_config_idx: 0, 36 | deposit_slot_hash: 0, 37 | is_used: false, 38 | reserved: [0; 38], 39 | } 40 | } 41 | /// Initializes a vector of DepositEntry with a given length 42 | pub fn init_deposits(length: usize) -> Vec { 43 | vec![Self::new(); length] 44 | } 45 | 46 | /// Voting Power Caclulation 47 | /// Returns the voting power for the deposit. 48 | pub fn voting_power(&self, mint_config: &VotingMintConfig) -> Result { 49 | let vote_weight = mint_config.digit_shift_native(self.amount_deposited_native)?; 50 | 51 | Ok(vote_weight) 52 | } 53 | } 54 | 55 | impl Default for DepositEntry { 56 | fn default() -> Self { 57 | Self::new() 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /programs/token-voter/src/state/mod.rs: -------------------------------------------------------------------------------- 1 | pub use registrar::*; 2 | pub mod registrar; 3 | 4 | pub use voting_mint_config::*; 5 | pub mod voting_mint_config; 6 | 7 | pub use deposit_entry::*; 8 | pub mod deposit_entry; 9 | 10 | pub use voter::*; 11 | pub mod voter; 12 | -------------------------------------------------------------------------------- /programs/token-voter/src/state/voting_mint_config.rs: -------------------------------------------------------------------------------- 1 | use crate::error::*; 2 | use anchor_lang::prelude::*; 3 | use std::convert::TryFrom; 4 | 5 | /// Exchange rate for an asset that can be used to mint voting rights. 6 | /// 7 | /// See documentation of configure_voting_mint for details on how 8 | /// native token amounts convert to vote weight. 9 | #[derive(AnchorSerialize, AnchorDeserialize, Debug, Clone, Copy, PartialEq)] 10 | pub struct VotingMintConfig { 11 | /// Mint for this entry. 12 | pub mint: Pubkey, 13 | 14 | /// Number of digits to shift native amounts, applying a 10^digit_shift factor. 15 | pub digit_shift: i8, 16 | 17 | // The mint_supply is used to calculate the vote weight 18 | pub mint_supply: u64, 19 | 20 | // Empty bytes for future upgrades. 21 | pub reserved1: [u8; 55], 22 | } 23 | 24 | const_assert!(std::mem::size_of::() == 32 + 1 + 8 + 55); 25 | const_assert!(std::mem::size_of::() % 8 == 0); 26 | 27 | impl VotingMintConfig { 28 | /// Converts an amount in this voting mints's native currency 29 | /// to the base vote weight 30 | /// by applying the digit_shift factor. 31 | pub fn compute_digit_shift_native(digit_shift: i8, amount_native: u64) -> Result { 32 | let compute = || -> Option { 33 | let val = if digit_shift < 0 { 34 | (amount_native as u128).checked_div(10u128.pow((-digit_shift) as u32))? 35 | } else { 36 | (amount_native as u128).checked_mul(10u128.pow(digit_shift as u32))? 37 | }; 38 | u64::try_from(val).ok() 39 | }; 40 | compute().ok_or_else(|| error!(TokenVoterError::VoterWeightOverflow)) 41 | } 42 | 43 | /// Same function as above but with self 44 | pub fn digit_shift_native(&self, amount_native: u64) -> Result { 45 | Self::compute_digit_shift_native(self.digit_shift, amount_native) 46 | } 47 | 48 | /// Whether this voting mint is configured. 49 | pub fn in_use(&self) -> bool { 50 | self.mint != Pubkey::default() 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /programs/token-voter/src/tools/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod spl_token; 2 | -------------------------------------------------------------------------------- /programs/token-voter/tests/create_max_voter_weight_record.rs: -------------------------------------------------------------------------------- 1 | use crate::program_test::token_voter_test::TokenVoterTest; 2 | use program_test::tools::assert_ix_err; 3 | use solana_program::instruction::InstructionError; 4 | use solana_program_test::*; 5 | use solana_sdk::transport::TransportError; 6 | 7 | mod program_test; 8 | 9 | #[tokio::test] 10 | async fn test_create_max_voter_weight_record_with_token_extensions() -> Result<(), TransportError> { 11 | // Arrange 12 | let mut token_voter_test = TokenVoterTest::start_new_token_extensions(None).await; 13 | let realm_cookie = token_voter_test.governance.with_realm().await?; 14 | 15 | let registrar_cookie = token_voter_test.with_registrar(&realm_cookie).await?; 16 | 17 | // Act 18 | let max_voter_weight_record_cookie = token_voter_test 19 | .with_max_voter_weight_record(®istrar_cookie) 20 | .await?; 21 | 22 | // Assert 23 | 24 | let max_voter_weight_record = token_voter_test 25 | .get_max_voter_weight_record(&max_voter_weight_record_cookie.address) 26 | .await; 27 | 28 | assert_eq!( 29 | max_voter_weight_record_cookie.account.max_voter_weight, 30 | max_voter_weight_record.max_voter_weight 31 | ); 32 | assert_eq!( 33 | max_voter_weight_record_cookie.account.realm, 34 | max_voter_weight_record.realm 35 | ); 36 | 37 | Ok(()) 38 | } 39 | 40 | #[tokio::test] 41 | async fn test_create_max_voter_weight_record() -> Result<(), TransportError> { 42 | // Arrange 43 | let mut token_voter_test = TokenVoterTest::start_new().await; 44 | 45 | let realm_cookie = token_voter_test.governance.with_realm().await?; 46 | 47 | let registrar_cookie = token_voter_test.with_registrar(&realm_cookie).await?; 48 | 49 | // Act 50 | let max_voter_weight_record_cookie = token_voter_test 51 | .with_max_voter_weight_record(®istrar_cookie) 52 | .await?; 53 | 54 | // Assert 55 | 56 | let max_voter_weight_record = token_voter_test 57 | .get_max_voter_weight_record(&max_voter_weight_record_cookie.address) 58 | .await; 59 | 60 | assert_eq!( 61 | max_voter_weight_record_cookie.account.max_voter_weight, 62 | max_voter_weight_record.max_voter_weight 63 | ); 64 | assert_eq!( 65 | max_voter_weight_record_cookie.account.realm, 66 | max_voter_weight_record.realm 67 | ); 68 | 69 | Ok(()) 70 | } 71 | 72 | #[tokio::test] 73 | async fn test_create_max_voter_weight_record_with_already_exists_error( 74 | ) -> Result<(), TransportError> { 75 | // Arrange 76 | let mut token_voter_test = TokenVoterTest::start_new().await; 77 | 78 | let realm_cookie = token_voter_test.governance.with_realm().await?; 79 | 80 | let registrar_cookie = token_voter_test.with_registrar(&realm_cookie).await?; 81 | 82 | token_voter_test 83 | .with_max_voter_weight_record(®istrar_cookie) 84 | .await?; 85 | 86 | token_voter_test.bench.advance_clock().await; 87 | 88 | // Act 89 | let err = token_voter_test 90 | .with_max_voter_weight_record(®istrar_cookie) 91 | .await 92 | .err() 93 | .unwrap(); 94 | 95 | // Assert 96 | 97 | // InstructionError::Custom(0) is returned for TransactionError::AccountInUse 98 | assert_ix_err(err, InstructionError::Custom(0)); 99 | 100 | Ok(()) 101 | } 102 | -------------------------------------------------------------------------------- /programs/token-voter/tests/fixtures/spl_governance.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-labs/governance-program-library/c7a49fa2a663c9bdf1260827c0d9b15790ebf24b/programs/token-voter/tests/fixtures/spl_governance.so -------------------------------------------------------------------------------- /programs/token-voter/tests/fixtures/spl_transfer_hook_example.so: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/solana-labs/governance-program-library/c7a49fa2a663c9bdf1260827c0d9b15790ebf24b/programs/token-voter/tests/fixtures/spl_transfer_hook_example.so -------------------------------------------------------------------------------- /programs/token-voter/tests/program_test/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod governance_test; 2 | pub mod program_test_bench; 3 | pub mod token_voter_test; 4 | pub mod tools; 5 | use log::*; 6 | use std::{sync::Arc, sync::RwLock}; 7 | 8 | #[derive(Default, Clone)] 9 | pub struct ProgramOutput { 10 | pub logs: Vec, 11 | pub data: Vec, 12 | } 13 | struct LoggerWrapper { 14 | inner: env_logger::Logger, 15 | output: Arc>, 16 | } 17 | 18 | impl Log for LoggerWrapper { 19 | fn enabled(&self, metadata: &log::Metadata) -> bool { 20 | self.inner.enabled(metadata) 21 | } 22 | 23 | fn log(&self, record: &log::Record) { 24 | if record 25 | .target() 26 | .starts_with("solana_runtime::message_processor") 27 | { 28 | let msg = record.args().to_string(); 29 | if let Some(data) = msg.strip_prefix("Program log: ") { 30 | self.output.write().unwrap().logs.push(data.into()); 31 | } else if let Some(data) = msg.strip_prefix("Program data: ") { 32 | self.output.write().unwrap().data.push(data.into()); 33 | } 34 | } 35 | self.inner.log(record); 36 | } 37 | 38 | fn flush(&self) {} 39 | } 40 | -------------------------------------------------------------------------------- /programs/token-voter/tests/program_test/tools.rs: -------------------------------------------------------------------------------- 1 | use anchor_lang::prelude::ERROR_CODE_OFFSET; 2 | use gpl_token_voter::error::TokenVoterError; 3 | use solana_program::instruction::InstructionError; 4 | use solana_program_test::BanksClientError; 5 | use solana_sdk::{signature::Keypair, transaction::TransactionError, transport::TransportError}; 6 | use spl_governance_tools::error::GovernanceToolsError; 7 | 8 | pub fn clone_keypair(source: &Keypair) -> Keypair { 9 | Keypair::from_bytes(&source.to_bytes()).unwrap() 10 | } 11 | 12 | /// NOP (No Operation) Override function 13 | #[allow(non_snake_case)] 14 | pub fn NopOverride(_: &mut T) {} 15 | 16 | #[allow(dead_code)] 17 | pub fn assert_token_voter_err( 18 | banks_client_error: BanksClientError, 19 | token_voter_error: TokenVoterError, 20 | ) { 21 | let tx_error = banks_client_error.unwrap(); 22 | 23 | match tx_error { 24 | TransactionError::InstructionError(_, instruction_error) => match instruction_error { 25 | InstructionError::Custom(e) => { 26 | assert_eq!(e, token_voter_error as u32 + ERROR_CODE_OFFSET) 27 | } 28 | _ => panic!("{:?} Is not InstructionError::Custom()", instruction_error), 29 | }, 30 | _ => panic!("{:?} Is not InstructionError", tx_error), 31 | }; 32 | } 33 | 34 | #[allow(dead_code)] 35 | pub fn assert_gov_tools_err( 36 | banks_client_error: TransportError, 37 | gov_tools_error: GovernanceToolsError, 38 | ) { 39 | let tx_error = banks_client_error.unwrap(); 40 | 41 | match tx_error { 42 | TransactionError::InstructionError(_, instruction_error) => match instruction_error { 43 | InstructionError::Custom(e) => { 44 | assert_eq!(e, gov_tools_error as u32) 45 | } 46 | _ => panic!("{:?} Is not InstructionError::Custom()", instruction_error), 47 | }, 48 | _ => panic!("{:?} Is not InstructionError", tx_error), 49 | }; 50 | } 51 | 52 | #[allow(dead_code)] 53 | pub fn assert_anchor_err( 54 | banks_client_error: BanksClientError, 55 | anchor_error: anchor_lang::error::ErrorCode, 56 | ) { 57 | let tx_error = banks_client_error.unwrap(); 58 | 59 | match tx_error { 60 | TransactionError::InstructionError(_, instruction_error) => match instruction_error { 61 | InstructionError::Custom(e) => { 62 | assert_eq!(e, anchor_error as u32) 63 | } 64 | _ => panic!("{:?} Is not InstructionError::Custom()", instruction_error), 65 | }, 66 | _ => panic!("{:?} Is not InstructionError", tx_error), 67 | }; 68 | } 69 | 70 | #[allow(dead_code)] 71 | pub fn assert_ix_err(banks_client_error: BanksClientError, ix_error: InstructionError) { 72 | let tx_error = banks_client_error.unwrap(); 73 | 74 | match tx_error { 75 | TransactionError::InstructionError(_, instruction_error) => { 76 | assert_eq!(instruction_error, ix_error); 77 | } 78 | _ => panic!("{:?} Is not InstructionError", tx_error), 79 | }; 80 | } 81 | 82 | #[allow(dead_code)] 83 | pub fn assert_ix_err_transport(banks_client_error: TransportError, ix_error: InstructionError) { 84 | let tx_error = banks_client_error.unwrap(); 85 | 86 | match tx_error { 87 | TransactionError::InstructionError(_, instruction_error) => { 88 | assert_eq!(instruction_error, ix_error); 89 | } 90 | _ => panic!("{:?} Is not InstructionError", tx_error), 91 | }; 92 | } 93 | -------------------------------------------------------------------------------- /run-release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # if [[ -z "${PROVIDER_WALLET}" ]]; then 6 | # echo "Please provide path to a provider wallet keypair." 7 | # exit -1 8 | # fi 9 | 10 | # if [[ -z "${VERSION_MANUALLY_BUMPED}" ]]; then 11 | # echo "Please bump versions in package.json and in cargo.toml." 12 | # exit -1 13 | # fi 14 | 15 | # build program 16 | anchor build 17 | 18 | # update on chain program and IDL, atm used for testing/developing 19 | # anchor deploy --provider.cluster devnet --provider.wallet ${PROVIDER_WALLET} 20 | # anchor idl upgrade --provider.cluster devnet --provider.wallet ${PROVIDER_WALLET}\ 21 | # --filepath target/idl/nft_voter.json GnftV5kLjd67tvHpNGyodwWveEKivz3ZWvvE3Z4xi2iw 22 | 23 | # update types in npm package and publish the npm package 24 | cp ./target/types/nft_voter.ts src/nftVoter/nft_voter.ts 25 | cp ./target/types/gateway.ts src/gateway/gateway.ts 26 | cp ./target/types/realm_voter.ts src/realmVoter/realm_voter.ts 27 | yarn clean && yarn build && yarn publish 28 | 29 | echo 30 | echo Remember to commit and push the version update as well as the changes 31 | echo to src/nft_voter.ts and/or rc/realm_voter.ts 32 | echo 33 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.79.0" 3 | -------------------------------------------------------------------------------- /scripts/getQuadraticVoterWeight.ts: -------------------------------------------------------------------------------- 1 | // Given a governance token wallet, a realm and a plugin name, send a transaction to update the voter weight record for the given wallet address. 2 | import { PublicKey, TransactionInstruction } from '@solana/web3.js'; 3 | import { GatewayClient, QuadraticClient } from '../src'; 4 | import { Provider } from '@coral-xyz/anchor'; 5 | import { createAndSendV0Tx } from './utils/plugin'; 6 | import { DEFAULT_RPC_URL } from './utils/constants'; 7 | import { getProvider, payer } from './utils/common'; 8 | 9 | // Parse the command line arguments 10 | const [voterString, realmString, communityMintString, rpcUrl = DEFAULT_RPC_URL] = process.argv.slice(2); 11 | if (!voterString || !realmString) { 12 | console.error('Usage: getQuadraticVoterWeight [rpcUrl]'); 13 | process.exit(1); 14 | } 15 | 16 | const voterPk = new PublicKey(voterString); 17 | const realmPk = new PublicKey(realmString); 18 | const communityMintPk = new PublicKey(communityMintString); 19 | 20 | // Connect to the cluster 21 | const provider = getProvider(rpcUrl); 22 | 23 | const loadClient = (provider: Provider) => QuadraticClient.connect(provider); 24 | 25 | (async () => { 26 | // Get the plugin client 27 | const client = await loadClient(provider); 28 | 29 | const registrar = await client.getRegistrarAccount(realmPk, communityMintPk); 30 | const voterWeightRecord = await client.getVoterWeightRecord(realmPk, communityMintPk, voterPk); 31 | if (!voterWeightRecord) { 32 | console.error("Voter weight record not found"); 33 | process.exit(1); 34 | } 35 | 36 | console.log("Quadratic coefficients: ", registrar?.quadraticCoefficients); 37 | console.log("Voter weight:" + voterWeightRecord.voterWeight.toString()); 38 | })().catch((err) => { 39 | console.error(err); 40 | process.exit(1); 41 | }); -------------------------------------------------------------------------------- /scripts/utils/common.ts: -------------------------------------------------------------------------------- 1 | // Load the payer keypair 2 | import { Connection, Keypair } from '@solana/web3.js'; 3 | import { DEFAULT_KEYPAIR_PATH } from './constants'; 4 | import { AnchorProvider, Wallet } from '@coral-xyz/anchor'; 5 | 6 | const keypairPath = process.env.KEYPAIR_PATH || DEFAULT_KEYPAIR_PATH; 7 | let keypair = Keypair.fromSecretKey(Buffer.from(require(keypairPath), 'hex')); 8 | try { 9 | keypair = Keypair.fromSecretKey(Buffer.from(require(keypairPath), 'hex')); 10 | } catch (e) { 11 | console.error(`Unable to read keypair file at ${keypairPath}: ${e}`); 12 | process.exit(1); 13 | } 14 | 15 | const getConnection = (rpcUrl: string) => new Connection(rpcUrl, 'confirmed'); 16 | export const getProvider = (rpcUrl: string) => new AnchorProvider( 17 | getConnection(rpcUrl), 18 | new Wallet(payer), {}); 19 | 20 | export const payer = keypair; -------------------------------------------------------------------------------- /scripts/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import os from 'os'; 2 | 3 | export const DEFAULT_KEYPAIR_PATH = os.homedir() + '/.config/solana/id.json'; 4 | export const DEFAULT_RPC_URL = 'https://api.devnet.solana.com'; -------------------------------------------------------------------------------- /scripts/utils/plugin.ts: -------------------------------------------------------------------------------- 1 | import { Connection, Keypair, PublicKey, TransactionInstruction, TransactionMessage, VersionedTransaction } from '@solana/web3.js'; 2 | 3 | export const createAndSendV0Tx = async (connection: Connection, signer: Keypair, txInstructions: TransactionInstruction[]) => { 4 | const latestBlockhash = await connection.getLatestBlockhash('confirmed'); 5 | const messageV0 = new TransactionMessage({ 6 | payerKey: signer.publicKey, 7 | recentBlockhash: latestBlockhash.blockhash, 8 | instructions: txInstructions 9 | }).compileToV0Message(); 10 | const transaction = new VersionedTransaction(messageV0); 11 | transaction.sign([signer]); 12 | const txid = await connection.sendTransaction(transaction, { maxRetries: 5 }); 13 | console.log("Sent transaction", txid); 14 | await connection.confirmTransaction({ signature: txid, ...latestBlockhash }, 'confirmed'); 15 | console.log("Confirmed transaction", txid); 16 | return txid; 17 | }; 18 | 19 | export const getMaxVoterWeightRecord = async ( 20 | realmPk: PublicKey, 21 | mint: PublicKey, 22 | clientProgramId: PublicKey 23 | ) => { 24 | const [ 25 | maxVoterWeightRecord, 26 | maxVoterWeightRecordBump, 27 | ] = await PublicKey.findProgramAddress( 28 | [ 29 | Buffer.from('max-voter-weight-record'), 30 | realmPk.toBuffer(), 31 | mint.toBuffer(), 32 | ], 33 | clientProgramId 34 | ) 35 | return { 36 | maxVoterWeightRecord, 37 | maxVoterWeightRecordBump, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/common/types.ts: -------------------------------------------------------------------------------- 1 | import { AccountClient, Idl, Program } from '@coral-xyz/anchor'; 2 | import { IdlDefinedFieldsNamed, IdlField, IdlTypeDefTyStruct } from '@coral-xyz/anchor/dist/cjs/idl'; 3 | 4 | type RegistrarAccountTemplate = { 5 | name: 'registrar' 6 | type: { 7 | fields: [ 8 | { 9 | "name": "previousVoterWeightPluginProgramId", 10 | "type": { 11 | "option": "pubkey" 12 | } 13 | } 14 | ] 15 | } 16 | } 17 | 18 | // A type function that checks if one of the fields of an account type has the name 'previousVoterWeightPluginProgramId' 19 | type HasPreviousVotingWeightPlugin = 20 | T extends [infer First extends IdlDefinedFieldsNamed[number], ...infer Rest] 21 | ? First['name'] extends RegistrarAccountTemplate['type']['fields'][0]['name'] 22 | ? T 23 | : Rest extends IdlDefinedFieldsNamed[number] 24 | ? (Rest extends HasPreviousVotingWeightPlugin ? T : never) 25 | : never 26 | : never; 27 | 28 | // A type function that checks if an account is a registrar account 29 | type MatchesRegistrarAccountType = T['name'] extends 'registrar' ? ( 30 | T extends HasPreviousVotingWeightPlugin ? T : never 31 | ) : never; 32 | 33 | // A type function that checks that an IDLAccountDef array has a registrar account 34 | type HasRegistrar = 35 | T extends [infer First extends IdlDefinedFieldsNamed[number], ...infer Rest] 36 | ? First extends MatchesRegistrarAccountType 37 | ? T 38 | : Rest extends IdlDefinedFieldsNamed[number] 39 | ? (Rest extends HasRegistrar ? T : never) 40 | : never 41 | : never; 42 | 43 | 44 | // A type function that defines a program that uses a plugin IDL 45 | export type PluginProgramAccounts = Program['account'] extends { 46 | registrar: AccountClient, 47 | voterWeightRecord: AccountClient, 48 | // may be undefined if the plugin does not support maxVoterWeightRecord - should be inferrable from T 49 | maxVoterWeightRecord: AccountClient, 50 | } ? Program['account'] : never; 51 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './nftVoter/client'; 2 | export * from './gateway/client'; 3 | export * from './realmVoter/client'; 4 | export * from './quadraticVoter/client'; 5 | export * from './common/Client'; 6 | export * from './tokenHaver/client'; 7 | 8 | -------------------------------------------------------------------------------- /src/nftVoter/client.ts: -------------------------------------------------------------------------------- 1 | import { Program, Provider } from '@coral-xyz/anchor'; 2 | import { NftVoter } from './nft_voter'; 3 | import NftVoterIDL from './nft_voter.json'; 4 | 5 | export class NftVoterClient { 6 | constructor(public program: Program, public devnet?: boolean) {} 7 | 8 | static async connect( 9 | provider: Provider, 10 | devnet?: boolean, 11 | ): Promise { 12 | return new NftVoterClient( 13 | new Program(NftVoterIDL as NftVoter, provider), 14 | devnet, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/realmVoter/client.ts: -------------------------------------------------------------------------------- 1 | import { Program, Provider } from '@coral-xyz/anchor'; 2 | import { RealmVoter } from './realm_voter'; 3 | import RealmVoterIDL from './realm_voter.json'; 4 | 5 | export class RealmVoterClient { 6 | constructor(public program: Program, public devnet?: boolean) {} 7 | 8 | static async connect( 9 | provider: Provider, 10 | devnet?: boolean, 11 | ): Promise { 12 | return new RealmVoterClient( 13 | new Program(RealmVoterIDL as RealmVoter, provider), 14 | devnet, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/tokenHaver/client.ts: -------------------------------------------------------------------------------- 1 | import { Program, Provider } from '@coral-xyz/anchor'; 2 | import { TokenHaver } from './token_haver'; 3 | import TokenHaverIDL from './token_haver.json'; 4 | 5 | export class TokenHaverClient { 6 | constructor(public program: Program, public devnet?: boolean) {} 7 | 8 | static async connect( 9 | provider: Provider, 10 | devnet?: boolean, 11 | ): Promise { 12 | return new TokenHaverClient( 13 | new Program(TokenHaverIDL as TokenHaver, provider), 14 | devnet, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/tokenVoter/client.ts: -------------------------------------------------------------------------------- 1 | import { Program, Provider } from '@coral-xyz/anchor'; 2 | import { TokenVoter } from './token_voter'; 3 | import TokenVoterIDL from './token_voter.json'; 4 | 5 | export class TokenVoterClient { 6 | constructor(public program: Program, public devnet?: boolean) {} 7 | 8 | static async connect( 9 | provider: Provider, 10 | devnet?: boolean, 11 | ): Promise { 12 | return new TokenVoterClient( 13 | new Program(TokenVoterIDL as TokenVoter, provider), 14 | devnet, 15 | ); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tests/nft-voter.ts: -------------------------------------------------------------------------------- 1 | import * as anchor from "@coral-xyz/anchor"; 2 | import { Program } from "@coral-xyz/anchor"; 3 | import { NftVoter } from "../target/types/nft_voter"; 4 | 5 | describe("nft-voter", () => { 6 | 7 | const program = anchor.workspace.NftVoter as Program; 8 | 9 | it("Is initialized!", async () => { 10 | 11 | const records = program.account.voterWeightRecord.all(); 12 | // Add your test here. 13 | //const tx = await program.rpc.createRegistrar({}); 14 | console.log("Your transaction signature", records); 15 | }); 16 | }); 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@tsconfig/recommended/tsconfig.json", 3 | 4 | "compilerOptions": { 5 | "allowJs": true, 6 | "checkJs": true, 7 | "declaration": true, 8 | "declarationDir": "dist", 9 | "declarationMap": true, 10 | "esModuleInterop": true, 11 | "lib": ["es2019"], 12 | "noImplicitAny": false, 13 | "outDir": "dist", 14 | "resolveJsonModule": true, 15 | "sourceMap": true, 16 | "target": "es6" 17 | }, 18 | "include": ["./src/**/*"], 19 | "exclude": ["./src/**/*.test.js", "node_modules", "**/node_modules"] 20 | } --------------------------------------------------------------------------------