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

tony/metaplex-core-voter #103

Open
wants to merge 22 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
0529118
initial commit
tonyboylehub Jul 25, 2024
ebd28e2
added anchor flag to crate import
tonyboylehub Jul 25, 2024
b25a2d5
Merge pull request #1 from tonyboylehub/initial-voter-clone
tonyboylehub Jul 25, 2024
6529008
tony/start-core-migration
tonyboylehub Jul 28, 2024
c2628a6
Changed how to get size for the collection since now we have that dat…
L0STE Jul 29, 2024
61b9c45
Merge pull request #2 from tonyboylehub/leo/metaplex-core
L0STE Jul 29, 2024
01a4ac8
Started refactoring tests
L0STE Jul 29, 2024
f53301a
Finished updating Tests. Need to run them to see if i'm missing anything
L0STE Jul 29, 2024
db97e07
Finished all the test refactoring
L0STE Jul 30, 2024
28ac89f
Fixed the cast_nft_vote test
L0STE Jul 30, 2024
3bdcde0
Fixed the fetching of accounts (Thanks to Tony's hunch)
L0STE Jul 30, 2024
857e61d
Address used was still the wrong one, now it's the right one
L0STE Jul 30, 2024
34dc316
Updated cast_nft_vote tests
L0STE Jul 30, 2024
002f314
Fixed all cast_nft_vote tests
L0STE Jul 30, 2024
e2a1d8b
Missing to fix 1 test in `relinquish_nft_vote` and 2 tests in `update…
L0STE Jul 30, 2024
40deb34
Fixed the `update_voter_weight_record` tests
L0STE Jul 30, 2024
d8cfb19
Fixed configure_collection tests
L0STE Jul 30, 2024
cb897af
working tests
tonyboylehub Jul 31, 2024
1f0700b
adjustment
tonyboylehub Jul 31, 2024
40a52f2
added max_voter_weight_record tests
tonyboylehub Aug 10, 2024
9646bce
Merge branch 'solana-labs:master' into master
tonyboylehub Aug 10, 2024
1007e67
Merge pull request #3 from tonyboylehub/tony/passing-tests
tonyboylehub Aug 12, 2024
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
1 change: 1 addition & 0 deletions Anchor.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ solana_version = "1.18.18"
seeds = false

[programs.localnet]
core-voter = "Gcore62Vw7rfgmXMG8T7B9Ye2smpE35rk12RxkuMNc6a"
tonyboylehub marked this conversation as resolved.
Show resolved Hide resolved
nft_voter = "GnftV5kLjd67tvHpNGyodwWveEKivz3ZWvvE3Z4xi2iw"
gateway = "GgathUhdrCWRHowoRKACjgWhYHfxCEdBi5ViqYN6HVxk"
quadratic = "quadCSapU8nTdLg73KHDnmdxKnJQsh7GUbu5tZfnRRr"
Expand Down
65 changes: 65 additions & 0 deletions Cargo.lock

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

35 changes: 35 additions & 0 deletions programs/core-voter/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[package]
name = "gpl-core-voter"
version = "0.2.2"
description = "SPL Governance addin implementing Metaplex Core NFT Asset based governance"
license = "Apache-2.0"
edition = "2018"

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

[features]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
cpi = ["no-entrypoint"]
default = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"]

[dependencies]
arrayref = "0.3.6"
anchor-lang = { version = "0.30.1", features = ["init-if-needed"] }
anchor-spl = { version = "0.30.1", features = ["token"] }
itertools = "0.10.2"
mpl-token-metadata = "^4.1.2"
mpl-core = {version = "0.7.1", features = ["anchor"]}
solana-program = "1.18.18"
spl-governance = { version = "4.0", features = ["no-entrypoint"] }
spl-governance-tools = "0.1.4"
spl-token = { version = "4.0", features = [ "no-entrypoint" ] }

[dev-dependencies]
borsh = "0.10.3"
solana-sdk = "1.18.18"
solana-program-test = "1.18.18"
2 changes: 2 additions & 0 deletions programs/core-voter/Xargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
[target.bpfel-unknown-unknown.dependencies.std]
features = []
82 changes: 82 additions & 0 deletions programs/core-voter/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use anchor_lang::prelude::*;

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

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

#[msg("Invalid Collection Size")]
InvalidCollectionSize,

#[msg("Invalid MaxVoterWeightRecord Realm")]
InvalidMaxVoterWeightRecordRealm,

#[msg("Invalid MaxVoterWeightRecord Mint")]
InvalidMaxVoterWeightRecordMint,

#[msg("CastVote Is Not Allowed")]
CastVoteIsNotAllowed,

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

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

#[msg("Invalid TokenOwner for VoterWeightRecord")]
InvalidTokenOwnerForVoterWeightRecord,

#[msg("Collection must be verified")]
CollectionMustBeVerified,

//10
#[msg("Voter does not own NFT")]
VoterDoesNotOwnNft,

#[msg("Collection not found")]
CollectionNotFound,

#[msg("Missing Metadata collection")]
MissingMetadataCollection,

#[msg("Token Metadata doesn't match")]
TokenMetadataDoesNotMatch,

#[msg("Invalid account owner")]
InvalidAccountOwner,

#[msg("Invalid token metadata account")]
InvalidTokenMetadataAccount,

#[msg("Duplicated NFT detected")]
DuplicatedNftDetected,

#[msg("Invalid NFT amount")]
InvalidNftAmount,

#[msg("NFT already voted")]
NftAlreadyVoted,

#[msg("Invalid Proposal for NftVoteRecord")]
InvalidProposalForNftVoteRecord,

// 20
#[msg("Invalid TokenOwner for NftVoteRecord")]
InvalidTokenOwnerForNftVoteRecord,

#[msg("VoteRecord must be withdrawn")]
VoteRecordMustBeWithdrawn,

#[msg("Invalid VoteRecord for NftVoteRecord")]
InvalidVoteRecordForNftVoteRecord,

#[msg("VoterWeightRecord must be expired")]
VoterWeightRecordMustBeExpired,

#[msg("Invalid NFT collection")]
InvalidNftCollection,
}
145 changes: 145 additions & 0 deletions programs/core-voter/src/instructions/cast_nft_vote.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use crate::error::NftVoterError;
use crate::{id, state::*};
use anchor_lang::prelude::*;
use anchor_lang::Accounts;
use itertools::Itertools;
use mpl_core::accounts::BaseAssetV1;
use spl_governance_tools::account::create_and_serialize_account_signed;

/// Casts NFT vote. The NFTs used for voting are tracked using NftVoteRecord accounts
/// This instruction updates VoterWeightRecord which is valid for the current Slot and the target Proposal only
/// and hance the instruction has to be executed inside the same transaction as spl-gov.CastVote
///
/// CastNftVote is accumulative and can be invoked using several transactions if voter owns more than 5 NFTs to calculate total voter_weight
/// In this scenario only the last CastNftVote should be bundled with spl-gov.CastVote in the same transaction
///
/// CastNftVote instruction and NftVoteRecord are not directional. They don't record vote choice (ex Yes/No)
/// VoteChoice is recorded by spl-gov in VoteRecord and this CastNftVote only tracks voting NFTs
///
#[derive(Accounts)]
#[instruction(proposal: Pubkey)]
pub struct CastNftVote<'info> {
/// The NFT voting registrar
pub registrar: Account<'info, Registrar>,

#[account(
mut,
constraint = voter_weight_record.realm == registrar.realm
@ NftVoterError::InvalidVoterWeightRecordRealm,

constraint = voter_weight_record.governing_token_mint == registrar.governing_token_mint
@ NftVoterError::InvalidVoterWeightRecordMint,
)]
pub voter_weight_record: Account<'info, VoterWeightRecord>,

/// TokenOwnerRecord of the voter who casts the vote
#[account(
owner = registrar.governance_program_id
)]
/// CHECK: Owned by spl-governance instance specified in registrar.governance_program_id
voter_token_owner_record: UncheckedAccount<'info>,

/// Authority of the voter who casts the vote
/// It can be either governing_token_owner or its delegate and must sign this instruction
pub voter_authority: Signer<'info>,

/// The account which pays for the transaction
#[account(mut)]
pub payer: Signer<'info>,

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

/// Casts vote with the NFT
pub fn cast_nft_vote<'a, 'b, 'c, 'info>(
ctx: Context<'a, 'b, 'c, 'info, CastNftVote<'info>>,
proposal: Pubkey,
) -> Result<()> {
let registrar = &ctx.accounts.registrar;
let voter_weight_record = &mut ctx.accounts.voter_weight_record;

let governing_token_owner = resolve_governing_token_owner(
registrar,
&ctx.accounts.voter_token_owner_record,
&ctx.accounts.voter_authority,
voter_weight_record,
)?;

let mut voter_weight = 0u64;

// Ensure all voting nfts in the batch are unique
let mut unique_asset_mints = vec![];

let rent = Rent::get()?;

for (asset, asset_vote_record_info) in
ctx.remaining_accounts.iter().tuples()
{
let (asset_vote_weight, asset_mint) = resolve_nft_vote_weight_and_mint(
registrar,
&governing_token_owner,
asset.key.clone(),
&BaseAssetV1::from_bytes(&asset.data.borrow()).unwrap(),
&mut unique_asset_mints,
)?;

voter_weight = voter_weight.checked_add(asset_vote_weight as u64).unwrap();

// Create NFT vote record to ensure the same NFT hasn't been already used for voting
// Note: The correct PDA of the NftVoteRecord is validated in create_and_serialize_account_signed
// It ensures the NftVoteRecord is for ('nft-vote-record',proposal,nft_mint) seeds
require!(
asset_vote_record_info.data_is_empty(),
NftVoterError::NftAlreadyVoted
);

// Note: proposal.governing_token_mint must match voter_weight_record.governing_token_mint
// We don't verify it here because spl-gov does the check in cast_vote
// and it would reject voter_weight_record if governing_token_mint doesn't match

// Note: Once the NFT plugin is enabled the governing_token_mint is used only as identity
// for the voting population and the tokens of that mint are no longer used
let asset_vote_record = AssetVoteRecord {
account_discriminator: AssetVoteRecord::ACCOUNT_DISCRIMINATOR,
proposal,
asset_mint,
governing_token_owner,
reserved: [0; 8],
};

// Anchor doesn't natively support dynamic account creation using remaining_accounts
// and we have to take it on the manual drive
create_and_serialize_account_signed(
&ctx.accounts.payer.to_account_info(),
asset_vote_record_info,
&asset_vote_record,
&get_nft_vote_record_seeds(&proposal, &asset_mint),
&id(),
&ctx.accounts.system_program.to_account_info(),
&rent,
0,
)?;
}

if voter_weight_record.weight_action_target == Some(proposal)
&& voter_weight_record.weight_action == Some(VoterWeightAction::CastVote)
{
// If cast_nft_vote is called for the same proposal then we keep accumulating the weight
// this way cast_nft_vote can be called multiple times in different transactions to allow voting with any number of NFTs
voter_weight_record.voter_weight = voter_weight_record
.voter_weight
.checked_add(voter_weight)
.unwrap();
} else {
voter_weight_record.voter_weight = voter_weight;
}

// The record is only valid as of the current slot
voter_weight_record.voter_weight_expiry = Some(Clock::get()?.slot);

// The record is only valid for casting vote on the given Proposal
voter_weight_record.weight_action = Some(VoterWeightAction::CastVote);
voter_weight_record.weight_action_target = Some(proposal);

Ok(())
}
Loading
Loading