Skip to content

Commit

Permalink
[wip] Token haver plugin (#92)
Browse files Browse the repository at this point in the history
* init

* .

* start tests

* more test updates

* check locked

* typo

* typo

* pubkey

* rn

* answer comments
  • Loading branch information
asktree authored May 17, 2024
1 parent 513a77b commit b117178
Show file tree
Hide file tree
Showing 31 changed files with 2,605 additions and 0 deletions.
52 changes: 52 additions & 0 deletions .devcontainer/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#
# Docker image to generate deterministic, verifiable builds of Anchor programs.
# This must be run *after* a given ANCHOR_CLI version is published and a git tag
# is released on GitHub.
#

FROM ubuntu:22.04

ARG DEBIAN_FRONTEND=noninteractive

ARG SOLANA_CLI="1.14.7"
ARG ANCHOR_CLI="0.27.0"
ARG NODE_VERSION="v18.16.0"

ENV HOME="/root"
ENV PATH="${HOME}/.cargo/bin:${PATH}"
ENV PATH="${HOME}/.local/share/solana/install/active_release/bin:${PATH}"
ENV PATH="${HOME}/.nvm/versions/node/${NODE_VERSION}/bin:${PATH}"

# Install base utilities.
RUN mkdir -p /workdir && mkdir -p /tmp && \
apt-get update -qq && apt-get upgrade -qq && apt-get install -qq \
build-essential git curl wget jq pkg-config python3-pip \
libssl-dev libudev-dev

RUN wget http://nz2.archive.ubuntu.com/ubuntu/pool/main/o/openssl/libssl1.1_1.1.1f-1ubuntu2_amd64.deb
RUN dpkg -i libssl1.1_1.1.1f-1ubuntu2_amd64.deb

# Install rust.
RUN curl "https://sh.rustup.rs" -sfo rustup.sh && \
sh rustup.sh -y && \
rustup component add rustfmt clippy

# Install node / npm / yarn.
RUN curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.11/install.sh | bash
ENV NVM_DIR="${HOME}/.nvm"
RUN . $NVM_DIR/nvm.sh && \
nvm install ${NODE_VERSION} && \
nvm use ${NODE_VERSION} && \
nvm alias default node && \
npm install -g yarn && \
yarn add ts-mocha

# Install Solana tools.
RUN sh -c "$(curl -sSfL https://release.solana.com/v${SOLANA_CLI}/install)"

# Install anchor.
RUN cargo install --git https://github.com/coral-xyz/anchor avm --locked --force
RUN avm install ${ANCHOR_CLI} && avm use ${ANCHOR_CLI}

WORKDIR /workdir
#be sure to add `/root/.avm/bin` to your PATH to be able to run the installed binaries
3 changes: 3 additions & 0 deletions .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"build": { "dockerfile": "Dockerfile" }
}
3 changes: 3 additions & 0 deletions .devcontainer/token-haver/devcontainer.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"build": { "dockerfile": "Dockerfile" }
}
1 change: 1 addition & 0 deletions Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ seeds = false
nft_voter = "GnftV5kLjd67tvHpNGyodwWveEKivz3ZWvvE3Z4xi2iw"
gateway = "GgathUhdrCWRHowoRKACjgWhYHfxCEdBi5ViqYN6HVxk"
solana-gateway-program = "gatem74V238djXdzWnJf94Wo1DcnuGkfijbf3AuBhfs"
token-haver = "vo65JQC6U8HCDPfoJxRrr17n7RZ5fKdfyjHm3erHJ2V"

[registry]
url = "https://anchor.projectserum.com"
Expand Down
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

31 changes: 31 additions & 0 deletions programs/token-haver/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "gpl-token-haver"
version = "0.0.1"
description = "SPL Governance plugin granting governance power based on the nonzero presence of locked tokens"
license = "Apache-2.0"
edition = "2018"

[lib]
crate-type = ["cdylib", "lib"]
name = "gpl_token_haver"

[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []

[dependencies]
arrayref = "0.3.6"
anchor-lang = { version = "0.26.0" }
anchor-spl = "0.26.0"
solana-program = "1.14.16"
spl-governance = { version = "3.1.1", features = ["no-entrypoint"] }
spl-governance-tools= "0.1.3"
spl-token = { version = "3.3", features = [ "no-entrypoint" ] }

[dev-dependencies]
borsh = "0.9.1"
solana-sdk = "1.14.16"
solana-program-test = "1.14.16"
2 changes: 2 additions & 0 deletions programs/token-haver/Xargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
6 changes: 6 additions & 0 deletions programs/token-haver/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
This plugin checks for the presence of nonzero tokens from certain mints in the user's wallet.

### You would use this if:

- You want voting power to be based on having an _indefinitely locked_ token, but not proportional to the amount of the token
- You don't want to use a Membership (for UX reasons)
28 changes: 28 additions & 0 deletions programs/token-haver/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use anchor_lang::prelude::*;

#[error_code]
pub enum TokenHaverError {
#[msg("Invalid Realm Authority")]
InvalidRealmAuthority,

#[msg("Invalid Realm for Registrar")]
InvalidRealmForRegistrar,

#[msg("Invalid VoterWeightRecord Realm")]
InvalidVoterWeightRecordRealm,

#[msg("Invalid VoterWeightRecord Mint")]
InvalidVoterWeightRecordMint,

#[msg("Governing TokenOwner must match")]
GoverningTokenOwnerMustMatch,

#[msg("All token accounts must be owned by the governing token owner")]
TokenAccountWrongOwner,

#[msg("All token accounts' mints must be included in the registrar")]
TokenAccountWrongMint,

#[msg("All token accounts' mints must be included in the registrar")]
TokenAccountNotLocked,
}
73 changes: 73 additions & 0 deletions programs/token-haver/src/instructions/configure_mints.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
use crate::error::TokenHaverError;
use crate::state::*;
use anchor_lang::system_program::Transfer;
use anchor_lang::{prelude::*, system_program};
use spl_governance::state::realm;

/// Configures mints for Registrar
#[derive(Accounts)]
#[instruction(mints: Vec<Pubkey>)]
pub struct ConfigureMints<'info> {
/// The Registrar for the given realm and governing_token_mint
#[account(mut)]
pub registrar: Account<'info, Registrar>,

#[account(
address = registrar.realm @ TokenHaverError::InvalidRealmForRegistrar,
owner = registrar.governance_program_id
)]
/// CHECK: Owned by spl-governance instance specified in registrar.governance_program_id
pub realm: UncheckedAccount<'info>,

// will pay in the event of a resize
pub payer: Signer<'info>,

/// Authority of the Realm must sign and match realm.authority
pub realm_authority: Signer<'info>,

pub system_program: Program<'info, System>,
}

pub fn configure_mints(ctx: Context<ConfigureMints>, mints: Vec<Pubkey>) -> Result<()> {
let new_size = Registrar::get_space(mints.len() as u8);

let rent = Rent::get()?;
let new_minimum_balance = rent.minimum_balance(new_size);

let lamports_diff =
new_minimum_balance.saturating_sub(ctx.accounts.registrar.to_account_info().lamports());

// if lamports_diff is positive, we need to fund the account
if lamports_diff > 0 {
// Create a CPI context for the transfer
let cpi_accounts = Transfer {
from: ctx.accounts.payer.to_account_info().clone(),
to: ctx.accounts.registrar.to_account_info().clone(),
};

let cpi_program = ctx.accounts.system_program.to_account_info();
let cpi_ctx = CpiContext::new(cpi_program, cpi_accounts);

// Perform the transfer
system_program::transfer(cpi_ctx, lamports_diff)?;
}

let registrar = &mut ctx.accounts.registrar;
registrar.to_account_info().realloc(new_size, false)?;

registrar.mints = mints;

let realm = realm::get_realm_data_for_governing_token_mint(
&registrar.governance_program_id,
&ctx.accounts.realm,
&registrar.governing_token_mint,
)?;

require_eq!(
realm.authority.unwrap(),
ctx.accounts.realm_authority.key(),
TokenHaverError::InvalidRealmAuthority
);

Ok(())
}
83 changes: 83 additions & 0 deletions programs/token-haver/src/instructions/create_registrar.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
use crate::error::TokenHaverError;
use crate::state::*;
use anchor_lang::prelude::*;
use anchor_spl::token::Mint;
use spl_governance::state::realm;

/// Creates Registrar storing Realm Voter configuration for spl-governance Realm
/// This instruction should only be executed once per realm/governing_token_mint to create the account
#[derive(Accounts)]
#[instruction(mints: Vec<Pubkey>)]
pub struct CreateRegistrar<'info> {
/// The Realm Voter Registrar
/// There can only be a single registrar per governance Realm and governing mint of the Realm
#[account(
init,
seeds = [b"registrar".as_ref(),realm.key().as_ref(), governing_token_mint.key().as_ref()],
bump,
payer = payer,
space = Registrar::get_space(mints.len() as u8),
)]
pub registrar: Account<'info, Registrar>,

/// The program id of the spl-governance program the realm belongs to
/// CHECK: Can be any instance of spl-governance and it's not known at the compilation time
#[account(executable)]
pub governance_program_id: UncheckedAccount<'info>,

/// An spl-governance Realm
///
/// Realm is validated in the instruction:
/// - Realm is owned by the governance_program_id
/// - governing_token_mint must be the community or council mint
/// - realm_authority is realm.authority
/// CHECK: Owned by spl-governance instance specified in governance_program_id
#[account(owner = governance_program_id.key())]
pub realm: UncheckedAccount<'info>,

/// Either the realm community mint or the council mint.
/// It must match Realm.community_mint or Realm.config.council_mint
///
/// Note: Once the Realm voter plugin is enabled the governing_token_mint is used only as identity
/// for the voting population and the tokens of that are no longer used
pub governing_token_mint: Account<'info, Mint>,

/// realm_authority must sign and match Realm.authority
pub realm_authority: Signer<'info>,

#[account(mut)]
pub payer: Signer<'info>,

pub system_program: Program<'info, System>,
}

/// Creates a new Registrar which stores Realms voter configuration for the given Realm
///
/// To use the registrar, call ConfigureGovernanceProgram to register spl-governance instance which will be
/// used for governance
///
pub fn create_registrar(ctx: Context<CreateRegistrar>, mints: Vec<Pubkey>) -> Result<()> {
let registrar = &mut ctx.accounts.registrar;
**registrar = Registrar {
governance_program_id: ctx.accounts.governance_program_id.key(),
realm: ctx.accounts.realm.key(),
governing_token_mint: ctx.accounts.governing_token_mint.key(),
mints,
};

// Verify that realm_authority is the expected authority of the Realm
// and that the mint matches one of the realm mints too
let realm = realm::get_realm_data_for_governing_token_mint(
&registrar.governance_program_id,
&ctx.accounts.realm,
&registrar.governing_token_mint,
)?;

require_eq!(
realm.authority.unwrap(),
ctx.accounts.realm_authority.key(),
TokenHaverError::InvalidRealmAuthority
);

Ok(())
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
use crate::state::*;
use anchor_lang::prelude::*;

/// Creates VoterWeightRecord used by spl-gov
/// This instruction should only be executed once per realm/governing_token_mint/governing_token_owner
/// to create the account
#[derive(Accounts)]
#[instruction(governing_token_owner: Pubkey)]
pub struct CreateVoterWeightRecord<'info> {
// The Registrar the VoterWeightRecord account belongs to
pub registrar: Account<'info, Registrar>,

#[account(
init,
seeds = [ b"voter-weight-record".as_ref(),
registrar.realm.key().as_ref(),
registrar.governing_token_mint.key().as_ref(),
governing_token_owner.as_ref()],
bump,
payer = payer,
space = VoterWeightRecord::get_space()
)]
pub voter_weight_record: Account<'info, VoterWeightRecord>,

#[account(mut)]
pub payer: Signer<'info>,

pub system_program: Program<'info, System>,
}

pub fn create_voter_weight_record(
ctx: Context<CreateVoterWeightRecord>,
governing_token_owner: Pubkey,
) -> Result<()> {
let voter_weight_record = &mut ctx.accounts.voter_weight_record;
let registrar = &ctx.accounts.registrar;

voter_weight_record.realm = registrar.realm.key();
voter_weight_record.governing_token_mint = registrar.governing_token_mint.key();
voter_weight_record.governing_token_owner = governing_token_owner;

// Set expiry to expired
voter_weight_record.voter_weight_expiry = Some(0);

Ok(())
}
11 changes: 11 additions & 0 deletions programs/token-haver/src/instructions/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
pub use create_registrar::*;
mod create_registrar;

pub use create_voter_weight_record::*;
mod create_voter_weight_record;

pub use update_voter_weight_record::*;
mod update_voter_weight_record;

pub use configure_mints::*;
mod configure_mints;
Loading

0 comments on commit b117178

Please sign in to comment.