Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(WIP) NFT Proxy Voting #74

Draft
wants to merge 18 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,22 @@
# governance-program-library
# governance-program-library

### Test

Unit tests contained within all projects can be run with:
```bash
$ cargo test # <-- runs host-based tests
$ cargo test-bpf # <-- runs BPF program tests
```

To run a specific program's tests, such as for NFT Voter Plugin:
```bash
$ cd programs/nft-voter
$ cargo test # <-- runs host-based tests
$ cargo test-bpf # <-- runs BPF program tests
```

To run a specific test, give the test name (doesnt include the file name)
```bash
$ cargo test test_create_governance_token_holding_account -- --exact # <-- runs host-based tests
$ cargo test-bpf test_create_governance_token_holding_account -- --exact # <-- runs BPF program tests
```
9 changes: 7 additions & 2 deletions programs/gateway/tests/program_test/program_test_bench.rs
Original file line number Diff line number Diff line change
Expand Up @@ -258,12 +258,17 @@ impl ProgramTestBench {
#[allow(dead_code)]
pub async fn with_wallet(&self) -> WalletCookie {
let account_rent = self.rent.minimum_balance(0);
self.with_wallet_funded(account_rent).await
}

#[allow(dead_code)]
pub async fn with_wallet_funded(&self, lamports: u64) -> WalletCookie {
let account_keypair = Keypair::new();

let create_account_ix = system_instruction::create_account(
&self.context.borrow().payer.pubkey(),
&account_keypair.pubkey(),
account_rent,
lamports,
0,
&system_program::id(),
);
Expand All @@ -273,7 +278,7 @@ impl ProgramTestBench {
.unwrap();

let account = Account {
lamports: account_rent,
lamports,
data: vec![],
owner: system_program::id(),
executable: false,
Expand Down
12 changes: 12 additions & 0 deletions programs/nft-voter/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,4 +76,16 @@ pub enum NftVoterError {

#[msg("Cannot configure collection with voting proposals")]
CannotConfigureCollectionWithVotingProposals,

#[msg("NFT owner must withdraw all votes or voting period must end before you may withdraw tokens")]
CannotWithdrawTokensWithActiveVotes,

#[msg("NFT must belong to a collection configured for the realm")]
InvalidNftCollection,

#[msg("Invalid NFT voting power holding account token mint")]
InvalidHoldingAccountMint,

#[msg("Provided NFT power holding account address doesnt match expected")]
InvalidHoldingAccountAddress,
}
3 changes: 2 additions & 1 deletion programs/nft-voter/src/instructions/cast_nft_vote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,14 +71,15 @@ pub fn cast_nft_vote<'a, 'b, 'c, 'info>(

let rent = Rent::get()?;

for (nft_info, nft_metadata_info, nft_vote_record_info) in
for (nft_info, nft_metadata_info, nft_vote_record_info, nft_power_holding_account_info) in
ctx.remaining_accounts.iter().tuples()
{
let (nft_vote_weight, nft_mint) = resolve_nft_vote_weight_and_mint(
registrar,
&governing_token_owner,
nft_info,
nft_metadata_info,
nft_power_holding_account_info,
&mut unique_nft_mints,
)?;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use anchor_lang::prelude::*;
use anchor_spl::{
associated_token::AssociatedToken,
token::{Mint, Token, TokenAccount},
};

use crate::{
error::NftVoterError,
state::Registrar,
tools::{
governance::NFT_POWER_HOLDING_ACCOUNT_SEED_PREFIX,
token_metadata::get_token_metadata_for_mint,
},
};

/// Creates a governance token holding account for a given NFT to boost its voting power
/// This instruction should only be executed once per realm/governing_token_mint/nft
/// to create the account
#[derive(Accounts)]
pub struct CreateGovernanceTokenHoldingAccount<'info> {
/// Associated fungible token account for the NFT being backed
#[account(
init,
seeds = [ &NFT_POWER_HOLDING_ACCOUNT_SEED_PREFIX,
registrar.realm.as_ref(),
realm_governing_token_mint.key().as_ref(),
nft_mint.key().as_ref()],
bump,
payer = payer,
token::mint = realm_governing_token_mint,
token::authority = governance_program_id
)]
pub holding_account_info: Account<'info, TokenAccount>,

/// 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>,

pub registrar: Account<'info, Registrar>,

/// Either the realm community mint or the council mint.
pub realm_governing_token_mint: Account<'info, Mint>,

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

/// Mint of the NFT for which the holding account is being created
pub nft_mint: Account<'info, Mint>,

/// Metadata of the NFT for which the holding account is being created. The
/// NFT must have a verified collection configured for the realm.
/// CHECK: metadata account cant be automatically deserialized by anchor
pub nft_metadata: UncheckedAccount<'info>,

/// Associated token program that will own the holding account
pub associated_token_program: Program<'info, AssociatedToken>,

/// Token program of the governance token mint
pub token_program: Program<'info, Token>,

/// System program required for creating the holding account
pub system_program: Program<'info, System>,

/// Rent required for creating the holding account
pub rent: Sysvar<'info, Rent>,
}

/// Deposits tokens into the holding account for a given NFT to boost its voting power
pub fn create_governance_token_holding_account(
ctx: Context<CreateGovernanceTokenHoldingAccount>,
) -> Result<()> {
let registrar = &ctx.accounts.registrar;
let nft_mint = &ctx.accounts.nft_mint;
let nft_metadata = get_token_metadata_for_mint(
&ctx.accounts.nft_metadata.to_account_info(),
&nft_mint.key(),
)?;

// The NFT must have a collection and the collection must be verified
let nft_collection = nft_metadata
.collection
.ok_or(NftVoterError::MissingMetadataCollection)?;

require!(
nft_collection.verified,
NftVoterError::CollectionMustBeVerified
);

require!(
registrar
.collection_configs
.iter()
.map(|c| c.collection.key())
.any(|c| c.key() == nft_collection.key),
NftVoterError::InvalidNftCollection
);

Ok(())
}
111 changes: 111 additions & 0 deletions programs/nft-voter/src/instructions/deposit_governance_tokens.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
use anchor_lang::prelude::*;
use anchor_spl::token::{Mint, Token, TokenAccount};
use spl_governance::state::realm;

use crate::{
state::delegator_token_owner_record::DelegatorTokenOwnerRecord,
tools::governance::NFT_POWER_HOLDING_ACCOUNT_SEED_PREFIX,
};

/// Deposits tokens into the holding account for a given NFT to boost its voting power
#[derive(Accounts)]
pub struct DepositGovernanceTokens<'info> {
/// Record tracking what amount of the tokens in the holding
/// account belong to this delegator
#[account(
init_if_needed,
seeds = [&DelegatorTokenOwnerRecord::SEED_PREFIX,
realm.key().as_ref(),
realm_governing_token_mint.key().as_ref(),
nft_mint.key().as_ref(),
governing_token_owner.key().as_ref()
],
bump,
payer = governing_token_owner,
space = DelegatorTokenOwnerRecord::SPACE
)]
pub token_owner_record: Account<'info, DelegatorTokenOwnerRecord>,

/// Associated fungible token account for the NFT being backed
#[account(
mut,
seeds = [ &NFT_POWER_HOLDING_ACCOUNT_SEED_PREFIX,
realm.key().as_ref(),
realm_governing_token_mint.key().as_ref(),
nft_mint.key().as_ref()],
bump,
token::mint = realm_governing_token_mint,
token::authority = governance_program_id
)]
pub holding_account_info: Account<'info, TokenAccount>,

/// 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>,

/// 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.
pub realm_governing_token_mint: Account<'info, Mint>,

/// Delegator, payer, and wallet that should receive the deposited tokens
/// upon withdrawal
#[account(mut)]
pub governing_token_owner: Signer<'info>,

/// Mint of the NFT being backed.
// We dont need to check that the NFT has a collection or that the collection
// is one configured for the realm, because a) this already happened when
// creating the holding account, and b) we have a constraint here that the
// holding account's seeds include this mint
pub nft_mint: Account<'info, Mint>,

/// Associated token account owned by governing_token_owner from which
/// tokens are being withdrawn for the deposit
#[account(mut, constraint = governing_token_source_account.owner == governing_token_owner.key())]
pub governing_token_source_account: Account<'info, TokenAccount>,

/// System program required for creating the DelegatorTokenOwnerRecord
pub system_program: Program<'info, System>,

/// Token program required for withdrawing (mutating) the source and holding accounts
pub token_program: Program<'info, Token>,
}

/// Deposits tokens into the holding account for a given NFT to boost its voting power
pub fn deposit_governance_tokens(ctx: Context<DepositGovernanceTokens>, amount: u64) -> Result<()> {
// Deserialize the Realm to validate it
let _realm = realm::get_realm_data_for_governing_token_mint(
&ctx.accounts.governance_program_id.key(),
&ctx.accounts.realm,
&ctx.accounts.realm_governing_token_mint.key(),
)?;

spl_governance::tools::spl_token::transfer_spl_tokens(
&ctx.accounts
.governing_token_source_account
.to_account_info(),
&ctx.accounts.holding_account_info.to_account_info(),
&ctx.accounts.governing_token_owner.to_account_info(),
amount,
&ctx.accounts.realm_governing_token_mint.to_account_info(),
)
.unwrap();

let token_owner_record = &mut ctx.accounts.token_owner_record;
token_owner_record.set_inner(DelegatorTokenOwnerRecord {
realm: ctx.accounts.realm.key(),
governing_token_mint: ctx.accounts.realm_governing_token_mint.key(),
nft_mint: ctx.accounts.nft_mint.key(),
governing_token_owner: ctx.accounts.governing_token_owner.key(),
governing_token_deposit_amount: token_owner_record
.governing_token_deposit_amount
.checked_add(amount)
.unwrap(),
});

Ok(())
}
9 changes: 9 additions & 0 deletions programs/nft-voter/src/instructions/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,12 @@ mod relinquish_nft_vote;

pub use cast_nft_vote::*;
mod cast_nft_vote;

pub use create_governance_token_holding_account::*;
mod create_governance_token_holding_account;

pub use deposit_governance_tokens::*;
mod deposit_governance_tokens;

pub use withdraw_governance_tokens::*;
mod withdraw_governance_tokens;
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,15 @@ pub fn update_voter_weight_record(
// Ensure all nfts are unique
let mut unique_nft_mints = vec![];

for (nft_info, nft_metadata_info) in ctx.remaining_accounts.iter().tuples() {
for (nft_info, nft_metadata_info, nft_power_holding_account_info) in
ctx.remaining_accounts.iter().tuples()
{
let (nft_vote_weight, _) = resolve_nft_vote_weight_and_mint(
registrar,
governing_token_owner,
nft_info,
nft_metadata_info,
nft_power_holding_account_info,
&mut unique_nft_mints,
)?;

Expand Down
Loading