This repository was archived by the owner on Feb 9, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 44
tony/metaplex-core-voter #103
Open
tonyboylehub
wants to merge
22
commits into
solana-labs:master
Choose a base branch
from
tonyboylehub:master
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
0529118
initial commit
tonyboylehub ebd28e2
added anchor flag to crate import
tonyboylehub b25a2d5
Merge pull request #1 from tonyboylehub/initial-voter-clone
tonyboylehub 6529008
tony/start-core-migration
tonyboylehub c2628a6
Changed how to get size for the collection since now we have that dat…
L0STE 61b9c45
Merge pull request #2 from tonyboylehub/leo/metaplex-core
L0STE 01a4ac8
Started refactoring tests
L0STE f53301a
Finished updating Tests. Need to run them to see if i'm missing anything
L0STE db97e07
Finished all the test refactoring
L0STE 28ac89f
Fixed the cast_nft_vote test
L0STE 3bdcde0
Fixed the fetching of accounts (Thanks to Tony's hunch)
L0STE 857e61d
Address used was still the wrong one, now it's the right one
L0STE 34dc316
Updated cast_nft_vote tests
L0STE 002f314
Fixed all cast_nft_vote tests
L0STE e2a1d8b
Missing to fix 1 test in `relinquish_nft_vote` and 2 tests in `update…
L0STE 40deb34
Fixed the `update_voter_weight_record` tests
L0STE d8cfb19
Fixed configure_collection tests
L0STE cb897af
working tests
tonyboylehub 1f0700b
adjustment
tonyboylehub 40a52f2
added max_voter_weight_record tests
tonyboylehub 9646bce
Merge branch 'solana-labs:master' into master
tonyboylehub 1007e67
Merge pull request #3 from tonyboylehub/tony/passing-tests
tonyboylehub File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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" |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| [target.bpfel-unknown-unknown.dependencies.std] | ||
| features = [] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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(()) | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.