From 5f7b7aa96c63f27d80582585207d5204c3f5097c Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 30 Mar 2023 00:16:26 -0700 Subject: [PATCH 01/13] Additional Metadata Delegates * Add support in Delegate and Revoke for Authority, Data, CollectionItem, and ProgrammableConfigItem Metadata delegates. * Remove Update Metadata delegate. --- .../program/src/instruction/delegate.rs | 48 +++++++++++++------ .../program/src/instruction/metadata.rs | 4 +- .../src/processor/delegate/delegate.rs | 26 ++++++++-- .../program/src/processor/delegate/revoke.rs | 13 +++-- 4 files changed, 65 insertions(+), 26 deletions(-) diff --git a/token-metadata/program/src/instruction/delegate.rs b/token-metadata/program/src/instruction/delegate.rs index 07bad02a46..667e088b6a 100644 --- a/token-metadata/program/src/instruction/delegate.rs +++ b/token-metadata/program/src/instruction/delegate.rs @@ -11,25 +11,42 @@ use solana_program::{ use super::InstructionBuilder; use crate::{instruction::MetadataInstruction, processor::AuthorizationData}; +/// Delegate args can specify Metadata delegates and Token delegates. #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] pub enum DelegateArgs { + AuthorityV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, + DataV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, CollectionV1 { /// Required authorization data to validate the request. authorization_data: Option, }, - SaleV1 { - amount: u64, + CollectionItemV1 { /// Required authorization data to validate the request. authorization_data: Option, }, - TransferV1 { + ProgrammableConfigV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, + ProgrammableConfigItemV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, + SaleV1 { amount: u64, /// Required authorization data to validate the request. authorization_data: Option, }, - UpdateV1 { + TransferV1 { + amount: u64, /// Required authorization data to validate the request. authorization_data: Option, }, @@ -53,25 +70,24 @@ pub enum DelegateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, - ProgrammableConfigV1 { - /// Required authorization data to validate the request. - authorization_data: Option, - }, } #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] pub enum RevokeArgs { + AuthorityV1, + DataV1, CollectionV1, + CollectionItemV1, + ProgrammableConfigV1, + ProgrammableConfigItemV1, SaleV1, TransferV1, - UpdateV1, UtilityV1, StakingV1, StandardV1, LockedTransferV1, - ProgrammableConfigV1, MigrationV1, } @@ -80,20 +96,24 @@ pub enum RevokeArgs { #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Copy)] pub enum MetadataDelegateRole { Authority, - Collection, + Data, Use, - Update, + Collection, + CollectionItem, ProgrammableConfig, + ProgrammableConfigItem, } impl fmt::Display for MetadataDelegateRole { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let message = match self { Self::Authority => "authority_delegate".to_string(), - Self::Collection => "collection_delegate".to_string(), + Self::Data => "data_delegate".to_string(), Self::Use => "use_delegate".to_string(), - Self::Update => "update_delegate".to_string(), + Self::Collection => "collection_delegate".to_string(), + Self::CollectionItem => "collection_item_delegate".to_string(), Self::ProgrammableConfig => "programmable_config_delegate".to_string(), + Self::ProgrammableConfigItem => "prog_config_item_delegate".to_string(), }; write!(f, "{message}") diff --git a/token-metadata/program/src/instruction/metadata.rs b/token-metadata/program/src/instruction/metadata.rs index 1d80a5083e..894812c0b8 100644 --- a/token-metadata/program/src/instruction/metadata.rs +++ b/token-metadata/program/src/instruction/metadata.rs @@ -72,8 +72,8 @@ pub enum TransferArgs { /// Struct representing the values to be updated for an `update` instructions. /// /// Values that are set to 'None' are not changed; any value set to `Some(_)` will -/// have its value updated. There are properties that have three valid states, which -/// allow the value to remaing the same, to be cleared or to set a new value. +/// have its value updated. There are properties that have three valid states, and +/// use a "toggle" type that allows the value to be set, cleared, or remain the same. #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] diff --git a/token-metadata/program/src/processor/delegate/delegate.rs b/token-metadata/program/src/processor/delegate/delegate.rs index c6e632a0b4..1e555c137a 100644 --- a/token-metadata/program/src/processor/delegate/delegate.rs +++ b/token-metadata/program/src/processor/delegate/delegate.rs @@ -36,17 +36,21 @@ impl Display for DelegateScenario { let message = match self { Self::Metadata(role) => match role { MetadataDelegateRole::Authority => "Authority".to_string(), - MetadataDelegateRole::Collection => "Collection".to_string(), + MetadataDelegateRole::Data => "Data".to_string(), MetadataDelegateRole::Use => "Use".to_string(), - MetadataDelegateRole::Update => "Update".to_string(), + MetadataDelegateRole::Collection => "Collection".to_string(), + MetadataDelegateRole::CollectionItem => "CollectionItem".to_string(), MetadataDelegateRole::ProgrammableConfig => "ProgrammableConfig".to_string(), + MetadataDelegateRole::ProgrammableConfigItem => { + "ProgrammableConfigItem".to_string() + } }, Self::Token(role) => match role { TokenDelegateRole::Sale => "Sale".to_string(), TokenDelegateRole::Transfer => "Transfer".to_string(), - TokenDelegateRole::LockedTransfer => "LockedTransfer".to_string(), TokenDelegateRole::Utility => "Utility".to_string(), TokenDelegateRole::Staking => "Staking".to_string(), + TokenDelegateRole::LockedTransfer => "LockedTransfer".to_string(), _ => panic!("Invalid delegate role"), }, }; @@ -115,15 +119,27 @@ pub fn delegate<'a>( // checks if it is a MetadataDelegate creation let delegate_args = match &args { + DelegateArgs::AuthorityV1 { authorization_data } => { + Some((MetadataDelegateRole::Authority, authorization_data)) + } + DelegateArgs::DataV1 { authorization_data } => { + Some((MetadataDelegateRole::Data, authorization_data)) + } + DelegateArgs::CollectionV1 { authorization_data } => { Some((MetadataDelegateRole::Collection, authorization_data)) } - DelegateArgs::UpdateV1 { authorization_data } => { - Some((MetadataDelegateRole::Update, authorization_data)) + DelegateArgs::CollectionItemV1 { authorization_data } => { + Some((MetadataDelegateRole::CollectionItem, authorization_data)) } DelegateArgs::ProgrammableConfigV1 { authorization_data } => { Some((MetadataDelegateRole::ProgrammableConfig, authorization_data)) } + DelegateArgs::ProgrammableConfigItemV1 { authorization_data } => Some(( + MetadataDelegateRole::ProgrammableConfigItem, + authorization_data, + )), + // we don't need to fail if did not find a match at this point _ => None, }; diff --git a/token-metadata/program/src/processor/delegate/revoke.rs b/token-metadata/program/src/processor/delegate/revoke.rs index 827e39925f..585a50ea29 100644 --- a/token-metadata/program/src/processor/delegate/revoke.rs +++ b/token-metadata/program/src/processor/delegate/revoke.rs @@ -33,16 +33,16 @@ pub fn revoke<'a>( RevokeArgs::SaleV1 => Some(TokenDelegateRole::Sale), // Transfer RevokeArgs::TransferV1 => Some(TokenDelegateRole::Transfer), - // LockedTransfer - RevokeArgs::LockedTransferV1 => Some(TokenDelegateRole::LockedTransfer), // Utility RevokeArgs::UtilityV1 => Some(TokenDelegateRole::Utility), // Staking RevokeArgs::StakingV1 => Some(TokenDelegateRole::Staking), - // Migration - RevokeArgs::MigrationV1 => Some(TokenDelegateRole::Migration), // Standard RevokeArgs::StandardV1 => Some(TokenDelegateRole::Standard), + // LockedTransfer + RevokeArgs::LockedTransferV1 => Some(TokenDelegateRole::LockedTransfer), + // Migration + RevokeArgs::MigrationV1 => Some(TokenDelegateRole::Migration), // we don't need to fail if did not find a match at this point _ => None, }; @@ -54,9 +54,12 @@ pub fn revoke<'a>( // checks if it is a MetadataDelegate creation let metadata_delegate = match &args { + RevokeArgs::AuthorityV1 => Some(MetadataDelegateRole::Authority), + RevokeArgs::DataV1 => Some(MetadataDelegateRole::Data), RevokeArgs::CollectionV1 => Some(MetadataDelegateRole::Collection), - RevokeArgs::UpdateV1 => Some(MetadataDelegateRole::Update), + RevokeArgs::CollectionItemV1 => Some(MetadataDelegateRole::CollectionItem), RevokeArgs::ProgrammableConfigV1 => Some(MetadataDelegateRole::ProgrammableConfig), + RevokeArgs::ProgrammableConfigItemV1 => Some(MetadataDelegateRole::ProgrammableConfigItem), // we don't need to fail if did not find a match at this point _ => None, }; From 12ce7d72d3290f82f104983f1cd58f35399a505c Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Mon, 3 Apr 2023 09:00:48 -0700 Subject: [PATCH 02/13] Changes to Update to support new and changed delegates * Modify authority check to separate out item and collection-level delegates. * Add V2 Update args struct to allow user to specify token standard. * Check that new delegates are only changing metadata for which they are meant to have access. * Modify Update handler to update metadata fields based on the delegate type. --- .../program/src/instruction/metadata.rs | 33 ++- .../program/src/processor/metadata/update.rs | 221 +++++++++++++++--- .../src/processor/verification/collection.rs | 5 +- token-metadata/program/src/state/metadata.rs | 82 +++++-- .../program/src/state/programmable.rs | 13 +- token-metadata/program/src/utils/mod.rs | 5 + 6 files changed, 298 insertions(+), 61 deletions(-) diff --git a/token-metadata/program/src/instruction/metadata.rs b/token-metadata/program/src/instruction/metadata.rs index 894812c0b8..e6f057d5ea 100644 --- a/token-metadata/program/src/instruction/metadata.rs +++ b/token-metadata/program/src/instruction/metadata.rs @@ -15,7 +15,7 @@ use crate::{ processor::AuthorizationData, state::{ AssetData, Collection, CollectionDetails, Creator, Data, DataV2, MigrationType, - PrintSupply, Uses, + PrintSupply, TokenStandard, Uses, }, }; @@ -100,20 +100,45 @@ pub enum UpdateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, + V2 { + /// The new update authority. + new_update_authority: Option, + /// The metadata details. + data: Option, + /// Indicates whether the primary sale has happened or not (once set to `true`, it cannot be + /// changed back). + primary_sale_happened: Option, + // Indicates Whether the data struct is mutable or not (once set to `true`, it cannot be + /// changed back). + is_mutable: Option, + /// Collection information. + collection: CollectionToggle, + /// Additional details of the collection. + collection_details: CollectionDetailsToggle, + /// Uses information. + uses: UsesToggle, + // Programmable rule set configuration (only applicable to `Programmable` asset types). + rule_set: RuleSetToggle, + /// Token standard. + token_standard: Option, + /// Required authorization data to validate the request. + authorization_data: Option, + }, } impl Default for UpdateArgs { fn default() -> Self { - Self::V1 { - authorization_data: None, + Self::V2 { new_update_authority: None, data: None, primary_sale_happened: None, is_mutable: None, collection: CollectionToggle::None, - uses: UsesToggle::None, collection_details: CollectionDetailsToggle::None, + uses: UsesToggle::None, rule_set: RuleSetToggle::None, + token_standard: None, + authorization_data: None, } } } diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index 29ce52e35e..859f08a553 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -16,7 +16,7 @@ use crate::{ AuthorityRequest, AuthorityResponse, AuthorityType, Collection, Metadata, ProgrammableConfig, TokenMetadataAccount, TokenStandard, }, - utils::{assert_derivation, check_token_standard}, + utils::{assert_derivation, check_token_standard, mint_decimals_is_zero}, }; #[derive(Clone, Debug, PartialEq, Eq)] @@ -45,6 +45,7 @@ pub fn update<'a>( match args { UpdateArgs::V1 { .. } => update_v1(program_id, context, args), + UpdateArgs::V2 { .. } => update_v1(program_id, context, args), } } @@ -63,7 +64,7 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro if let Some(edition) = ctx.accounts.edition_info { assert_owned_by(edition, program_id)?; - // checks that we got the correct master account + // checks that we got the correct edition account assert_derivation( program_id, edition, @@ -114,12 +115,6 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro return Err(MetadataError::MintMismatch.into()); } - let token_standard = if let Some(token_standard) = metadata.token_standard { - token_standard - } else { - check_token_standard(ctx.accounts.mint_info, ctx.accounts.edition_info)? - }; - let (token_pubkey, token) = if let Some(token_info) = ctx.accounts.token_info { ( Some(token_info.key), @@ -152,7 +147,18 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro token: token_pubkey, token_account: token.as_ref(), metadata_delegate_record_info: ctx.accounts.delegate_record_info, - metadata_delegate_roles: vec![MetadataDelegateRole::ProgrammableConfig], + metadata_delegate_roles: vec![ + MetadataDelegateRole::Authority, + MetadataDelegateRole::Data, + MetadataDelegateRole::Collection, + MetadataDelegateRole::CollectionItem, + MetadataDelegateRole::ProgrammableConfig, + MetadataDelegateRole::ProgrammableConfigItem, + ], + collection_metadata_delegate_roles: vec![ + MetadataDelegateRole::Collection, + MetadataDelegateRole::ProgrammableConfig, + ], precedence: &[ AuthorityType::Metadata, AuthorityType::MetadataDelegate, @@ -161,6 +167,34 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro ..Default::default() })?; + // Find existing token standard from metadata or infer it. + let existing_or_inferred_token_standard = if let Some(token_standard) = metadata.token_standard + { + token_standard + } else { + check_token_standard(ctx.accounts.mint_info, ctx.accounts.edition_info)? + }; + + // Check if caller passed in a desired token standard. + let desired_token_standard = match args { + UpdateArgs::V1 { .. } => None, + UpdateArgs::V2 { token_standard, .. } => token_standard, + }; + + // If there is a desired token standard, use it if it passes the check. If there is not a + // desired token standard, use the existing or inferred token standard. + let token_standard = match desired_token_standard { + Some(desired_token_standard) => { + check_desired_token_standard( + mint_decimals_is_zero(ctx.accounts.mint_info)?, + existing_or_inferred_token_standard, + desired_token_standard, + )?; + desired_token_standard + } + None => existing_or_inferred_token_standard, + }; + // For pNFTs, we need to validate the authorization rules. if matches!(token_standard, TokenStandard::ProgrammableNonFungible) { // If the metadata account has a current rule set, we validate that @@ -174,6 +208,8 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro } } + // Validate that authority has permission to update the fields that have been specified in the + // update args. validate_update(&args, &authority_type, metadata_delegate_role)?; // If we reach here without errors we have validated that the authority is allowed to @@ -199,58 +235,171 @@ fn validate_update( metadata_delegate_role: Option, ) -> ProgramResult { // validate the authority type - match authority_type { AuthorityType::Metadata => { - // metadata authority is the paramount (upadte) authority + // metadata authority is the paramount (update) authority msg!("Auth type: Metadata"); } - AuthorityType::MetadataDelegate => { - // support for delegate update - msg!("Auth type: Delegate"); - } AuthorityType::Holder => { // support for holder update msg!("Auth type: Holder"); return Err(MetadataError::FeatureNotSupported.into()); } + AuthorityType::MetadataDelegate => { + // support for delegate update + msg!("Auth type: Delegate"); + } _ => { return Err(MetadataError::InvalidAuthorityType.into()); } } - let UpdateArgs::V1 { + // Destructure args. + let ( + new_update_authority, data, primary_sale_happened, is_mutable, collection, - uses, - new_update_authority, collection_details, - .. - } = args; + uses, + rule_set, + token_standard, + ) = match args { + UpdateArgs::V1 { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + .. + } => ( + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + &None, + ), + UpdateArgs::V2 { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + token_standard, + .. + } => ( + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + token_standard, + ), + }; // validate the delegate role: this consist in checking that // the delegate is only updating fields that it has access to - match metadata_delegate_role { - Some(MetadataDelegateRole::ProgrammableConfig) => { - // can only update the programmable config - if data.is_some() - || primary_sale_happened.is_some() - || is_mutable.is_some() - || collection.is_some() - || uses.is_some() - || new_update_authority.is_some() - || collection_details.is_some() - { - return Err(MetadataError::InvalidUpdateArgs.into()); + if let Some(metadata_delegate_role) = metadata_delegate_role { + match metadata_delegate_role { + MetadataDelegateRole::Authority => { + // Fields allowed for `Authority`: + // `new_update_authority` + // `primary_sale_happened` + // `is_mutable` + // `token_standard` + if data.is_some() + || collection.is_some() + || collection_details.is_some() + || uses.is_some() + || rule_set.is_some() + { + return Err(MetadataError::InvalidUpdateArgs.into()); + } } + MetadataDelegateRole::Data => { + // Fields allowed for `Data`: + // `data` + if new_update_authority.is_some() + || primary_sale_happened.is_some() + || is_mutable.is_some() + || collection.is_some() + || collection_details.is_some() + || uses.is_some() + || rule_set.is_some() + || token_standard.is_some() + { + return Err(MetadataError::InvalidUpdateArgs.into()); + } + } + + MetadataDelegateRole::Collection | MetadataDelegateRole::CollectionItem => { + // Fields allowed for `Collection` and `CollectionItem`: + // `collection` + if new_update_authority.is_some() + || data.is_some() + || primary_sale_happened.is_some() + || is_mutable.is_some() + || collection_details.is_some() + || uses.is_some() + || rule_set.is_some() + || token_standard.is_some() + { + return Err(MetadataError::InvalidUpdateArgs.into()); + } + } + MetadataDelegateRole::ProgrammableConfig + | MetadataDelegateRole::ProgrammableConfigItem => { + // Fields allowed for `ProgrammableConfig` and `ProgrammableConfigItem`: + // `rule_set` + if new_update_authority.is_some() + || data.is_some() + || primary_sale_happened.is_some() + || is_mutable.is_some() + || collection.is_some() + || collection_details.is_some() + || uses.is_some() + || token_standard.is_some() + { + return Err(MetadataError::InvalidUpdateArgs.into()); + } + } + _ => return Err(MetadataError::InvalidAuthorityType.into()), } - Some(_) => { - return Err(MetadataError::InvalidAuthorityType.into()); - } - None => { /* no delegate role to check */ } } Ok(()) } + +fn check_desired_token_standard( + mint_decimals_is_zero: bool, + existing_or_inferred_token_standard: TokenStandard, + desired_token_standard: TokenStandard, +) -> ProgramResult { + // This code only allows switching between Fungible and FungibleAsset, and only when + // mint decimals is zero. + if !mint_decimals_is_zero { + return Err(MetadataError::InvalidTokenStandard.into()); + } + + match existing_or_inferred_token_standard { + TokenStandard::Fungible | TokenStandard::FungibleAsset => match desired_token_standard { + TokenStandard::Fungible | TokenStandard::FungibleAsset => Ok(()), + _ => Err(MetadataError::InvalidTokenStandard.into()), + }, + _ => Err(MetadataError::InvalidTokenStandard.into()), + } +} diff --git a/token-metadata/program/src/processor/verification/collection.rs b/token-metadata/program/src/processor/verification/collection.rs index a517e2f319..aea56548cd 100644 --- a/token-metadata/program/src/processor/verification/collection.rs +++ b/token-metadata/program/src/processor/verification/collection.rs @@ -164,7 +164,10 @@ pub(crate) fn unverify_collection_v1(program_id: &Pubkey, ctx: Context // or an update delegate for the item. This call fails if no valid authority is present. auth_request.mint = &metadata.mint; auth_request.update_authority = &metadata.update_authority; - auth_request.metadata_delegate_roles = vec![MetadataDelegateRole::Update]; + auth_request.metadata_delegate_roles = vec![ + MetadataDelegateRole::Collection, + MetadataDelegateRole::CollectionItem, + ]; AuthorityType::get_authority_type(auth_request) } else { // If the parent is not burned, we need to ensure the collection metadata account is owned diff --git a/token-metadata/program/src/state/metadata.rs b/token-metadata/program/src/state/metadata.rs index 32b33e71de..a43e427bff 100644 --- a/token-metadata/program/src/state/metadata.rs +++ b/token-metadata/program/src/state/metadata.rs @@ -91,17 +91,49 @@ impl Metadata { authority_type: AuthorityType, delegate_role: Option, ) -> ProgramResult { - let UpdateArgs::V1 { + // Destructure args. + let ( + new_update_authority, data, primary_sale_happened, is_mutable, collection, + collection_details, uses, - new_update_authority, rule_set, - collection_details, - .. - } = args; + ) = match args { + UpdateArgs::V1 { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + .. + } + | UpdateArgs::V2 { + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + .. + } => ( + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set, + ), + }; // updates the token standard only if the current value is None let token_standard = match self.token_standard { @@ -116,7 +148,9 @@ impl Metadata { } }; - if matches!(authority_type, AuthorityType::Metadata) { + if matches!(authority_type, AuthorityType::Metadata) + || matches!(delegate_role, Some(MetadataDelegateRole::Data)) + { if let Some(data) = data { if !self.is_mutable { return Err(MetadataError::DataIsImmutable.into()); @@ -131,7 +165,14 @@ impl Metadata { )?; self.data = data; } + } + if matches!(authority_type, AuthorityType::Metadata) + || matches!( + delegate_role, + Some(MetadataDelegateRole::Collection | MetadataDelegateRole::CollectionItem) + ) + { // if the Collection data is 'Set', only allow updating if it is unverified // or if it exactly matches the existing collection info; if the Collection data // is 'Clear', then only set to 'None' if it is unverified. @@ -153,7 +194,9 @@ impl Metadata { } CollectionToggle::None => { /* nothing to do */ } } + } + if matches!(authority_type, AuthorityType::Metadata) { if uses.is_some() { let uses_option = uses.to_option(); // If already None leave it as None. @@ -161,6 +204,19 @@ impl Metadata { self.uses = uses_option; } + if let CollectionDetailsToggle::Set(collection_details) = collection_details { + // only unsized collections can have the size set, and only once. + if self.collection_details.is_some() { + return Err(MetadataError::SizedCollection.into()); + } + + self.collection_details = Some(collection_details); + } + } + + if matches!(authority_type, AuthorityType::Metadata) + || matches!(delegate_role, Some(MetadataDelegateRole::Authority)) + { if let Some(authority) = new_update_authority { self.update_authority = authority; } @@ -182,21 +238,15 @@ impl Metadata { return Err(MetadataError::IsMutableCanOnlyBeFlippedToFalse.into()); } } - - if let CollectionDetailsToggle::Set(collection_details) = collection_details { - // only unsized collections can have the size set, and only once. - if self.collection_details.is_some() { - return Err(MetadataError::SizedCollection.into()); - } - - self.collection_details = Some(collection_details); - } } if matches!(authority_type, AuthorityType::Metadata) || matches!( delegate_role, - Some(MetadataDelegateRole::ProgrammableConfig) + Some( + MetadataDelegateRole::ProgrammableConfig + | MetadataDelegateRole::ProgrammableConfigItem + ) ) { // if the rule_set data is either 'Set' or 'Clear', only allow updating if the diff --git a/token-metadata/program/src/state/programmable.rs b/token-metadata/program/src/state/programmable.rs index 1a5f55a1b7..91023135b3 100644 --- a/token-metadata/program/src/state/programmable.rs +++ b/token-metadata/program/src/state/programmable.rs @@ -219,6 +219,8 @@ pub struct AuthorityRequest<'a, 'b> { pub metadata_delegate_record_info: Option<&'a AccountInfo<'a>>, /// Expected `MetadataDelegateRole` for the request. pub metadata_delegate_roles: Vec, + /// Expected collection-level `MetadataDelegateRole` for the request. + pub collection_metadata_delegate_roles: Vec, /// `TokenRecord` account. pub token_record_info: Option<&'a AccountInfo<'a>>, /// Expected `TokenDelegateRole` for the request. @@ -242,6 +244,7 @@ impl<'a, 'b> Default for AuthorityRequest<'a, 'b> { token_account: None, metadata_delegate_record_info: None, metadata_delegate_roles: Vec::with_capacity(0), + collection_metadata_delegate_roles: Vec::with_capacity(0), token_record_info: None, token_delegate_roles: Vec::with_capacity(0), } @@ -347,12 +350,14 @@ impl AuthorityType { }); } } + } - // looking up the delegate on the collection mint (this is for - // collection-level delegates) - if let Some(mint) = request.collection_mint { + // looking up the delegate on the collection mint (this is for + // collection-level delegates) + if let Some(collection_mint) = request.collection_mint { + for role in &request.collection_metadata_delegate_roles { let (pda_key, _) = find_metadata_delegate_record_account( - mint, + collection_mint, *role, request.update_authority, request.authority, diff --git a/token-metadata/program/src/utils/mod.rs b/token-metadata/program/src/utils/mod.rs index ffb1b6c403..ed05ae1eb7 100644 --- a/token-metadata/program/src/utils/mod.rs +++ b/token-metadata/program/src/utils/mod.rs @@ -69,6 +69,11 @@ pub fn check_token_standard( } } +pub fn mint_decimals_is_zero(mint_info: &AccountInfo) -> Result { + let mint_decimals = get_mint_decimals(mint_info)?; + Ok(mint_decimals == 0) +} + pub fn is_master_edition( edition_account_info: &AccountInfo, mint_decimals: u8, From 8d3e5f467fc8d78b52446d2ef2a43f0c1f545372 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 4 Apr 2023 11:08:58 -0700 Subject: [PATCH 03/13] Update tests to support new version of UpdateArgs * Also add macro to help destructure UpdateArgs fields. --- .../program/src/instruction/metadata.rs | 10 ++ .../program/src/processor/metadata/update.rs | 90 ++++++---------- token-metadata/program/src/state/metadata.rs | 45 +++----- token-metadata/program/tests/unverify.rs | 39 +++---- token-metadata/program/tests/update.rs | 51 ++++----- .../program/tests/utils/digital_asset.rs | 102 ++++++++++++++---- token-metadata/program/tests/verify.rs | 12 +-- 7 files changed, 177 insertions(+), 172 deletions(-) diff --git a/token-metadata/program/src/instruction/metadata.rs b/token-metadata/program/src/instruction/metadata.rs index e6f057d5ea..f44d4cb2c5 100644 --- a/token-metadata/program/src/instruction/metadata.rs +++ b/token-metadata/program/src/instruction/metadata.rs @@ -143,6 +143,16 @@ impl Default for UpdateArgs { } } +#[macro_export] +macro_rules! get_update_args_fields { + ($args:expr, $($field:ident),+) => { + match $args { + UpdateArgs::V1 { $($field,)+ .. } => ($($field,)+), + UpdateArgs::V2 { $($field,)+ .. } => ($($field,)+), + } + }; +} + //-- Toggle implementations #[repr(C)] diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index 859f08a553..fe59412f04 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -10,6 +10,7 @@ use spl_token::state::Account; use crate::{ assertions::{assert_owned_by, programmable::assert_valid_authorization}, error::MetadataError, + get_update_args_fields, instruction::{Context, MetadataDelegateRole, Update, UpdateArgs}, pda::{EDITION, PREFIX}, state::{ @@ -167,6 +168,21 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro ..Default::default() })?; + // Check if caller passed in a desired token standard. + let desired_token_standard = match args { + UpdateArgs::V1 { .. } => None, + UpdateArgs::V2 { token_standard, .. } => token_standard, + }; + + // Validate that authority has permission to update the fields that have been specified in the + // update args. + validate_update( + &args, + &authority_type, + metadata_delegate_role, + desired_token_standard, + )?; + // Find existing token standard from metadata or infer it. let existing_or_inferred_token_standard = if let Some(token_standard) = metadata.token_standard { @@ -175,12 +191,6 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro check_token_standard(ctx.accounts.mint_info, ctx.accounts.edition_info)? }; - // Check if caller passed in a desired token standard. - let desired_token_standard = match args { - UpdateArgs::V1 { .. } => None, - UpdateArgs::V2 { token_standard, .. } => token_standard, - }; - // If there is a desired token standard, use it if it passes the check. If there is not a // desired token standard, use the existing or inferred token standard. let token_standard = match desired_token_standard { @@ -208,10 +218,6 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro } } - // Validate that authority has permission to update the fields that have been specified in the - // update args. - validate_update(&args, &authority_type, metadata_delegate_role)?; - // If we reach here without errors we have validated that the authority is allowed to // perform an update. metadata.update_v1( @@ -233,6 +239,7 @@ fn validate_update( args: &UpdateArgs, authority_type: &AuthorityType, metadata_delegate_role: Option, + desired_token_standard: Option, ) -> ProgramResult { // validate the authority type match authority_type { @@ -264,52 +271,17 @@ fn validate_update( collection_details, uses, rule_set, - token_standard, - ) = match args { - UpdateArgs::V1 { - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - .. - } => ( - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - &None, - ), - UpdateArgs::V2 { - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - token_standard, - .. - } => ( - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - token_standard, - ), - }; + ) = get_update_args_fields!( + args, + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set + ); // validate the delegate role: this consist in checking that // the delegate is only updating fields that it has access to @@ -340,7 +312,7 @@ fn validate_update( || collection_details.is_some() || uses.is_some() || rule_set.is_some() - || token_standard.is_some() + || desired_token_standard.is_some() { return Err(MetadataError::InvalidUpdateArgs.into()); } @@ -356,7 +328,7 @@ fn validate_update( || collection_details.is_some() || uses.is_some() || rule_set.is_some() - || token_standard.is_some() + || desired_token_standard.is_some() { return Err(MetadataError::InvalidUpdateArgs.into()); } @@ -372,7 +344,7 @@ fn validate_update( || collection.is_some() || collection_details.is_some() || uses.is_some() - || token_standard.is_some() + || desired_token_standard.is_some() { return Err(MetadataError::InvalidUpdateArgs.into()); } diff --git a/token-metadata/program/src/state/metadata.rs b/token-metadata/program/src/state/metadata.rs index a43e427bff..a357998bea 100644 --- a/token-metadata/program/src/state/metadata.rs +++ b/token-metadata/program/src/state/metadata.rs @@ -4,6 +4,7 @@ use crate::{ collection::assert_collection_update_is_valid, metadata::assert_data_valid, uses::assert_valid_use, }, + get_update_args_fields, instruction::{ CollectionDetailsToggle, CollectionToggle, MetadataDelegateRole, RuleSetToggle, UpdateArgs, }, @@ -101,39 +102,17 @@ impl Metadata { collection_details, uses, rule_set, - ) = match args { - UpdateArgs::V1 { - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - .. - } - | UpdateArgs::V2 { - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - .. - } => ( - new_update_authority, - data, - primary_sale_happened, - is_mutable, - collection, - collection_details, - uses, - rule_set, - ), - }; + ) = get_update_args_fields!( + args, + new_update_authority, + data, + primary_sale_happened, + is_mutable, + collection, + collection_details, + uses, + rule_set + ); // updates the token standard only if the current value is None let token_standard = match self.token_standard { diff --git a/token-metadata/program/tests/unverify.rs b/token-metadata/program/tests/unverify.rs index 8f191146b9..d2d5460f1b 100644 --- a/token-metadata/program/tests/unverify.rs +++ b/token-metadata/program/tests/unverify.rs @@ -4,6 +4,7 @@ pub mod utils; use mpl_token_metadata::{ error::MetadataError, + get_update_args_fields, instruction::{BurnArgs, DelegateArgs, MetadataDelegateRole, UpdateArgs, VerificationArgs}, pda::{find_metadata_delegate_record_account, find_token_record_account}, state::{Collection, CollectionDetails, Creator, TokenStandard}, @@ -1262,12 +1263,12 @@ mod unverify_collection { } #[tokio::test] - async fn collections_update_delegate_cannot_unverify() { - let delegate_args = DelegateArgs::UpdateV1 { + async fn collections_collection_item_delegate_cannot_unverify() { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::Update; + let delegate_role = MetadataDelegateRole::CollectionItem; other_metadata_delegates_cannot_unverify( AssetToDelegate::CollectionParent, @@ -1310,12 +1311,12 @@ mod unverify_collection { } #[tokio::test] - async fn items_update_delegate_cannot_unverify() { - let delegate_args = DelegateArgs::UpdateV1 { + async fn items_collection_item_delegate_cannot_unverify() { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::Update; + let delegate_role = MetadataDelegateRole::CollectionItem; other_metadata_delegates_cannot_unverify( AssetToDelegate::Item, @@ -1909,11 +1910,8 @@ mod unverify_collection { .unwrap(); let mut args = UpdateArgs::default(); - let UpdateArgs::V1 { - new_update_authority, - .. - } = &mut args; - *new_update_authority = Some(new_collection_update_authority.pubkey()); + let new_update_authority = get_update_args_fields!(&mut args, new_update_authority); + *new_update_authority.0 = Some(new_collection_update_authority.pubkey()); let payer = context.payer.dirty_clone(); test_items @@ -1983,11 +1981,8 @@ mod unverify_collection { let new_collection_update_authority = Keypair::new(); let mut args = UpdateArgs::default(); - let UpdateArgs::V1 { - new_update_authority, - .. - } = &mut args; - *new_update_authority = Some(new_collection_update_authority.pubkey()); + let new_update_authority = get_update_args_fields!(&mut args, new_update_authority); + *new_update_authority.0 = Some(new_collection_update_authority.pubkey()); let payer = context.payer.dirty_clone(); test_items @@ -2036,7 +2031,7 @@ mod unverify_collection { } #[tokio::test] - async fn pass_unverify_burned_pnft_parent_using_item_update_delegate() { + async fn pass_unverify_burned_pnft_parent_using_item_collection_item_delegate() { let mut context = program_test().start_with_context().await; let mut test_items = create_mint_verify_collection_check( @@ -2071,7 +2066,7 @@ mod unverify_collection { let payer = context.payer.dirty_clone(); let payer_pubkey = payer.pubkey(); - let delegate_args = DelegateArgs::UpdateV1 { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; test_items @@ -2083,7 +2078,7 @@ mod unverify_collection { // Find delegate record PDA. let (delegate_record, _) = find_metadata_delegate_record_account( &test_items.da.mint.pubkey(), - MetadataDelegateRole::Update, + MetadataDelegateRole::CollectionItem, &payer_pubkey, &delegate.pubkey(), ); @@ -2127,12 +2122,12 @@ mod unverify_collection { } #[tokio::test] - async fn collections_update_delegate_cannot_unverify_burned_pnft_parent() { - let delegate_args = DelegateArgs::UpdateV1 { + async fn collections_collection_item_delegate_cannot_unverify_burned_pnft_parent() { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::Update; + let delegate_role = MetadataDelegateRole::CollectionItem; other_metadata_delegates_cannot_unverify_burned_pnft_parent( AssetToDelegate::CollectionParent, diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index c94a6ecc85..b3bc167514 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -2,6 +2,7 @@ pub mod utils; use mpl_token_metadata::{ + get_update_args_fields, instruction::{builders::UpdateBuilder, InstructionBuilder}, state::{MAX_NAME_LENGTH, MAX_SYMBOL_LENGTH, MAX_URI_LENGTH}, utils::puffed_out_string, @@ -67,10 +68,8 @@ mod update { }; let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { - data: current_data, .. - } = &mut update_args; - *current_data = Some(data); + let current_data = get_update_args_fields!(&mut update_args, data); + *current_data.0 = Some(data); let mut builder = UpdateBuilder::new(); builder @@ -139,9 +138,10 @@ mod update { } let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { rule_set, .. } = &mut update_args; + let rule_set = get_update_args_fields!(&mut update_args, rule_set); + // remove the rule set - *rule_set = RuleSetToggle::Clear; + *rule_set.0 = RuleSetToggle::Clear; let mut builder = UpdateBuilder::new(); builder @@ -213,8 +213,8 @@ mod update { } let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { rule_set, .. } = &mut update_args; - *rule_set = RuleSetToggle::Set(invalid_rule_set); + let rule_set = get_update_args_fields!(&mut update_args, rule_set); + *rule_set.0 = RuleSetToggle::Set(invalid_rule_set); let mut builder = UpdateBuilder::new(); builder @@ -285,8 +285,8 @@ mod update { // Finally, try to update with the valid rule set, and it should succeed. let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { rule_set, .. } = &mut update_args; - *rule_set = RuleSetToggle::Set(authorization_rules); + let rule_set = get_update_args_fields!(&mut update_args, rule_set); + *rule_set.0 = RuleSetToggle::Set(authorization_rules); let mut builder = UpdateBuilder::new(); builder @@ -382,9 +382,10 @@ mod update { // Try to clear the rule set. let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { rule_set, .. } = &mut update_args; + let rule_set = get_update_args_fields!(&mut update_args, rule_set); + // remove the rule set - *rule_set = RuleSetToggle::Clear; + *rule_set.0 = RuleSetToggle::Clear; let mut builder = UpdateBuilder::new(); builder @@ -418,11 +419,9 @@ mod update { // Try to update the rule set. let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { - rule_set, - authorization_data, - .. - } = &mut update_args; + let (rule_set, authorization_data) = + get_update_args_fields!(&mut update_args, rule_set, authorization_data); + // update the rule set *rule_set = RuleSetToggle::Set(new_auth_rules); *authorization_data = Some(new_auth_data); @@ -500,10 +499,8 @@ mod update { }; let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { - data: current_data, .. - } = &mut update_args; - *current_data = Some(data); + let current_data = get_update_args_fields!(&mut update_args, data); + *current_data.0 = Some(data); let err = da .update(context, update_authority.dirty_clone(), update_args) @@ -558,10 +555,8 @@ mod update { }; let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { - data: current_data, .. - } = &mut update_args; - *current_data = Some(data); + let current_data = get_update_args_fields!(&mut update_args, data); + *current_data.0 = Some(data); da.update(context, update_authority.dirty_clone(), update_args) .await @@ -582,10 +577,8 @@ mod update { }; let mut update_args = UpdateArgs::default(); - let UpdateArgs::V1 { - data: current_data, .. - } = &mut update_args; - *current_data = Some(data); + let current_data = get_update_args_fields!(&mut update_args, data); + *current_data.0 = Some(data); da.update(context, update_authority.dirty_clone(), update_args) .await diff --git a/token-metadata/program/tests/utils/digital_asset.rs b/token-metadata/program/tests/utils/digital_asset.rs index a3dbaff3fa..4fb2860c1a 100644 --- a/token-metadata/program/tests/utils/digital_asset.rs +++ b/token-metadata/program/tests/utils/digital_asset.rs @@ -561,6 +561,25 @@ impl DigitalAsset { .spl_token_program(spl_token::ID); match args { + DelegateArgs::AuthorityV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::Authority, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } + DelegateArgs::DataV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::Data, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } + DelegateArgs::CollectionV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), @@ -570,19 +589,10 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } - DelegateArgs::SaleV1 { .. } - | DelegateArgs::TransferV1 { .. } - | DelegateArgs::UtilityV1 { .. } - | DelegateArgs::StakingV1 { .. } - | DelegateArgs::LockedTransferV1 { .. } => { - let (token_record, _) = - find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); - builder.token_record(token_record); - } - DelegateArgs::UpdateV1 { .. } => { + DelegateArgs::CollectionItemV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::Update, + MetadataDelegateRole::CollectionItem, &payer.pubkey(), &delegate, ); @@ -597,6 +607,25 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } + DelegateArgs::ProgrammableConfigItemV1 { .. } => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::ProgrammableConfigItem, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } + + DelegateArgs::SaleV1 { .. } + | DelegateArgs::TransferV1 { .. } + | DelegateArgs::UtilityV1 { .. } + | DelegateArgs::StakingV1 { .. } + | DelegateArgs::LockedTransferV1 { .. } => { + let (token_record, _) = + find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); + builder.token_record(token_record); + } DelegateArgs::StandardV1 { .. } => { /* nothing to add */ } } @@ -762,6 +791,24 @@ impl DigitalAsset { .spl_token_program(spl_token::ID); match args { + RevokeArgs::AuthorityV1 => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::Authority, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } + RevokeArgs::DataV1 => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::Data, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } RevokeArgs::CollectionV1 => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), @@ -771,20 +818,10 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } - RevokeArgs::SaleV1 - | RevokeArgs::TransferV1 - | RevokeArgs::UtilityV1 - | RevokeArgs::StakingV1 - | RevokeArgs::LockedTransferV1 - | RevokeArgs::MigrationV1 => { - let (token_record, _) = - find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); - builder.token_record(token_record); - } - RevokeArgs::UpdateV1 => { + RevokeArgs::CollectionItemV1 => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::Update, + MetadataDelegateRole::CollectionItem, &payer.pubkey(), &delegate, ); @@ -799,6 +836,25 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } + RevokeArgs::ProgrammableConfigItemV1 => { + let (delegate_record, _) = find_metadata_delegate_record_account( + &self.mint.pubkey(), + MetadataDelegateRole::ProgrammableConfigItem, + &payer.pubkey(), + &delegate, + ); + builder.delegate_record(delegate_record); + } + RevokeArgs::SaleV1 + | RevokeArgs::TransferV1 + | RevokeArgs::UtilityV1 + | RevokeArgs::StakingV1 + | RevokeArgs::LockedTransferV1 + | RevokeArgs::MigrationV1 => { + let (token_record, _) = + find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); + builder.token_record(token_record); + } RevokeArgs::StandardV1 { .. } => { /* nothing to add */ } } diff --git a/token-metadata/program/tests/verify.rs b/token-metadata/program/tests/verify.rs index ad3ec1f5db..2536f8144b 100644 --- a/token-metadata/program/tests/verify.rs +++ b/token-metadata/program/tests/verify.rs @@ -2118,12 +2118,12 @@ mod verify_collection { } #[tokio::test] - async fn collections_update_delegate_cannot_verify() { - let delegate_args = DelegateArgs::UpdateV1 { + async fn collections_collection_item_delegate_cannot_verify() { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::Update; + let delegate_role = MetadataDelegateRole::CollectionItem; other_metadata_delegates_cannot_verify( AssetToDelegate::CollectionParent, @@ -2162,12 +2162,12 @@ mod verify_collection { } #[tokio::test] - async fn items_update_delegate_cannot_verify() { - let delegate_args = DelegateArgs::UpdateV1 { + async fn items_collection_item_delegate_cannot_verify() { + let delegate_args = DelegateArgs::CollectionItemV1 { authorization_data: None, }; - let delegate_role = MetadataDelegateRole::Update; + let delegate_role = MetadataDelegateRole::CollectionItem; other_metadata_delegates_cannot_verify(AssetToDelegate::Item, delegate_args, delegate_role) .await; From 7fcadd81911a21918c32ebb0777ecc25e5f15b3b Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Tue, 4 Apr 2023 11:25:56 -0700 Subject: [PATCH 04/13] Fix delegate test based on auth rules update --- token-metadata/program/src/processor/delegate/delegate.rs | 1 - token-metadata/program/tests/delegate.rs | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/token-metadata/program/src/processor/delegate/delegate.rs b/token-metadata/program/src/processor/delegate/delegate.rs index 1e555c137a..5f7597267d 100644 --- a/token-metadata/program/src/processor/delegate/delegate.rs +++ b/token-metadata/program/src/processor/delegate/delegate.rs @@ -125,7 +125,6 @@ pub fn delegate<'a>( DelegateArgs::DataV1 { authorization_data } => { Some((MetadataDelegateRole::Data, authorization_data)) } - DelegateArgs::CollectionV1 { authorization_data } => { Some((MetadataDelegateRole::Collection, authorization_data)) } diff --git a/token-metadata/program/tests/delegate.rs b/token-metadata/program/tests/delegate.rs index b296e09134..3a5c30db71 100644 --- a/token-metadata/program/tests/delegate.rs +++ b/token-metadata/program/tests/delegate.rs @@ -587,6 +587,6 @@ mod delegate { // asserts - assert_custom_error_ix!(1, error, RuleSetError::ProgramOwnedListCheckFailed); + assert_custom_error_ix!(1, error, RuleSetError::DataIsEmpty); } } From ce19636e54e8da5792090f6c4cd1129e50841686 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 01:43:25 -0700 Subject: [PATCH 05/13] Put enums back in original order * Update is still changed to Data but order is preserved. * Also remove unnecessary Option for token_record in Update. * Add some comments clarifying authority types in Unverify. --- .../program/src/instruction/delegate.rs | 62 +++++++------- .../src/processor/delegate/delegate.rs | 41 ++++----- .../program/src/processor/delegate/revoke.rs | 6 +- .../program/src/processor/metadata/update.rs | 28 +++---- .../src/processor/verification/collection.rs | 9 +- token-metadata/program/src/state/metadata.rs | 16 +--- .../program/tests/utils/digital_asset.rs | 83 ++++++++++--------- 7 files changed, 122 insertions(+), 123 deletions(-) diff --git a/token-metadata/program/src/instruction/delegate.rs b/token-metadata/program/src/instruction/delegate.rs index 667e088b6a..8f4f31b3a2 100644 --- a/token-metadata/program/src/instruction/delegate.rs +++ b/token-metadata/program/src/instruction/delegate.rs @@ -16,30 +16,10 @@ use crate::{instruction::MetadataInstruction, processor::AuthorizationData}; #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] pub enum DelegateArgs { - AuthorityV1 { - /// Required authorization data to validate the request. - authorization_data: Option, - }, - DataV1 { - /// Required authorization data to validate the request. - authorization_data: Option, - }, CollectionV1 { /// Required authorization data to validate the request. authorization_data: Option, }, - CollectionItemV1 { - /// Required authorization data to validate the request. - authorization_data: Option, - }, - ProgrammableConfigV1 { - /// Required authorization data to validate the request. - authorization_data: Option, - }, - ProgrammableConfigItemV1 { - /// Required authorization data to validate the request. - authorization_data: Option, - }, SaleV1 { amount: u64, /// Required authorization data to validate the request. @@ -50,6 +30,10 @@ pub enum DelegateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, + DataV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, UtilityV1 { amount: u64, /// Required authorization data to validate the request. @@ -70,25 +54,41 @@ pub enum DelegateArgs { /// Required authorization data to validate the request. authorization_data: Option, }, + ProgrammableConfigV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, + AuthorityV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, + CollectionItemV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, + ProgrammableConfigItemV1 { + /// Required authorization data to validate the request. + authorization_data: Option, + }, } #[repr(C)] #[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone)] pub enum RevokeArgs { - AuthorityV1, - DataV1, CollectionV1, - CollectionItemV1, - ProgrammableConfigV1, - ProgrammableConfigItemV1, SaleV1, TransferV1, + DataV1, UtilityV1, StakingV1, StandardV1, LockedTransferV1, + ProgrammableConfigV1, MigrationV1, + AuthorityV1, + CollectionItemV1, + ProgrammableConfigItemV1, } #[repr(C)] @@ -96,11 +96,11 @@ pub enum RevokeArgs { #[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, Copy)] pub enum MetadataDelegateRole { Authority, - Data, - Use, Collection, - CollectionItem, + Use, + Data, ProgrammableConfig, + CollectionItem, ProgrammableConfigItem, } @@ -108,11 +108,11 @@ impl fmt::Display for MetadataDelegateRole { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { let message = match self { Self::Authority => "authority_delegate".to_string(), - Self::Data => "data_delegate".to_string(), - Self::Use => "use_delegate".to_string(), Self::Collection => "collection_delegate".to_string(), - Self::CollectionItem => "collection_item_delegate".to_string(), + Self::Use => "use_delegate".to_string(), + Self::Data => "data_delegate".to_string(), Self::ProgrammableConfig => "programmable_config_delegate".to_string(), + Self::CollectionItem => "collection_item_delegate".to_string(), Self::ProgrammableConfigItem => "prog_config_item_delegate".to_string(), }; diff --git a/token-metadata/program/src/processor/delegate/delegate.rs b/token-metadata/program/src/processor/delegate/delegate.rs index 5f7597267d..c3be34ceae 100644 --- a/token-metadata/program/src/processor/delegate/delegate.rs +++ b/token-metadata/program/src/processor/delegate/delegate.rs @@ -36,11 +36,11 @@ impl Display for DelegateScenario { let message = match self { Self::Metadata(role) => match role { MetadataDelegateRole::Authority => "Authority".to_string(), - MetadataDelegateRole::Data => "Data".to_string(), - MetadataDelegateRole::Use => "Use".to_string(), MetadataDelegateRole::Collection => "Collection".to_string(), - MetadataDelegateRole::CollectionItem => "CollectionItem".to_string(), + MetadataDelegateRole::Use => "Use".to_string(), + MetadataDelegateRole::Data => "Data".to_string(), MetadataDelegateRole::ProgrammableConfig => "ProgrammableConfig".to_string(), + MetadataDelegateRole::CollectionItem => "CollectionItem".to_string(), MetadataDelegateRole::ProgrammableConfigItem => { "ProgrammableConfigItem".to_string() } @@ -79,16 +79,6 @@ pub fn delegate<'a>( amount, authorization_data, } => Some((TokenDelegateRole::Transfer, amount, authorization_data)), - // LockedTransfer - DelegateArgs::LockedTransferV1 { - amount, - authorization_data, - .. - } => Some(( - TokenDelegateRole::LockedTransfer, - amount, - authorization_data, - )), // Utility DelegateArgs::UtilityV1 { amount, @@ -101,6 +91,17 @@ pub fn delegate<'a>( } => Some((TokenDelegateRole::Staking, amount, authorization_data)), // Standard DelegateArgs::StandardV1 { amount } => Some((TokenDelegateRole::Standard, amount, &None)), + // LockedTransfer + DelegateArgs::LockedTransferV1 { + amount, + authorization_data, + .. + } => Some(( + TokenDelegateRole::LockedTransfer, + amount, + authorization_data, + )), + // we don't need to fail if did not find a match at this point _ => None, }; @@ -119,21 +120,21 @@ pub fn delegate<'a>( // checks if it is a MetadataDelegate creation let delegate_args = match &args { - DelegateArgs::AuthorityV1 { authorization_data } => { - Some((MetadataDelegateRole::Authority, authorization_data)) + DelegateArgs::CollectionV1 { authorization_data } => { + Some((MetadataDelegateRole::Collection, authorization_data)) } DelegateArgs::DataV1 { authorization_data } => { Some((MetadataDelegateRole::Data, authorization_data)) } - DelegateArgs::CollectionV1 { authorization_data } => { - Some((MetadataDelegateRole::Collection, authorization_data)) + DelegateArgs::ProgrammableConfigV1 { authorization_data } => { + Some((MetadataDelegateRole::ProgrammableConfig, authorization_data)) + } + DelegateArgs::AuthorityV1 { authorization_data } => { + Some((MetadataDelegateRole::Authority, authorization_data)) } DelegateArgs::CollectionItemV1 { authorization_data } => { Some((MetadataDelegateRole::CollectionItem, authorization_data)) } - DelegateArgs::ProgrammableConfigV1 { authorization_data } => { - Some((MetadataDelegateRole::ProgrammableConfig, authorization_data)) - } DelegateArgs::ProgrammableConfigItemV1 { authorization_data } => Some(( MetadataDelegateRole::ProgrammableConfigItem, authorization_data, diff --git a/token-metadata/program/src/processor/delegate/revoke.rs b/token-metadata/program/src/processor/delegate/revoke.rs index 585a50ea29..ef17d273b4 100644 --- a/token-metadata/program/src/processor/delegate/revoke.rs +++ b/token-metadata/program/src/processor/delegate/revoke.rs @@ -54,11 +54,11 @@ pub fn revoke<'a>( // checks if it is a MetadataDelegate creation let metadata_delegate = match &args { - RevokeArgs::AuthorityV1 => Some(MetadataDelegateRole::Authority), - RevokeArgs::DataV1 => Some(MetadataDelegateRole::Data), RevokeArgs::CollectionV1 => Some(MetadataDelegateRole::Collection), - RevokeArgs::CollectionItemV1 => Some(MetadataDelegateRole::CollectionItem), + RevokeArgs::DataV1 => Some(MetadataDelegateRole::Data), RevokeArgs::ProgrammableConfigV1 => Some(MetadataDelegateRole::ProgrammableConfig), + RevokeArgs::AuthorityV1 => Some(MetadataDelegateRole::Authority), + RevokeArgs::CollectionItemV1 => Some(MetadataDelegateRole::CollectionItem), RevokeArgs::ProgrammableConfigItemV1 => Some(MetadataDelegateRole::ProgrammableConfigItem), // we don't need to fail if did not find a match at this point _ => None, diff --git a/token-metadata/program/src/processor/metadata/update.rs b/token-metadata/program/src/processor/metadata/update.rs index fe59412f04..55d130081c 100644 --- a/token-metadata/program/src/processor/metadata/update.rs +++ b/token-metadata/program/src/processor/metadata/update.rs @@ -188,6 +188,7 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro { token_standard } else { + // TODO: What if they have an edition account but choose not to pass it in? check_token_standard(ctx.accounts.mint_info, ctx.accounts.edition_info)? }; @@ -225,7 +226,7 @@ fn update_v1(program_id: &Pubkey, ctx: Context, args: UpdateArgs) -> Pro ctx.accounts.authority_info, ctx.accounts.metadata_info, token, - Some(token_standard), + token_standard, authority_type, metadata_delegate_role, )?; @@ -256,9 +257,7 @@ fn validate_update( // support for delegate update msg!("Auth type: Delegate"); } - _ => { - return Err(MetadataError::InvalidAuthorityType.into()); - } + _ => return Err(MetadataError::InvalidAuthorityType.into()), } // Destructure args. @@ -302,13 +301,13 @@ fn validate_update( return Err(MetadataError::InvalidUpdateArgs.into()); } } - MetadataDelegateRole::Data => { - // Fields allowed for `Data`: - // `data` + MetadataDelegateRole::Collection | MetadataDelegateRole::CollectionItem => { + // Fields allowed for `Collection` and `CollectionItem`: + // `collection` if new_update_authority.is_some() + || data.is_some() || primary_sale_happened.is_some() || is_mutable.is_some() - || collection.is_some() || collection_details.is_some() || uses.is_some() || rule_set.is_some() @@ -317,14 +316,13 @@ fn validate_update( return Err(MetadataError::InvalidUpdateArgs.into()); } } - - MetadataDelegateRole::Collection | MetadataDelegateRole::CollectionItem => { - // Fields allowed for `Collection` and `CollectionItem`: - // `collection` + MetadataDelegateRole::Data => { + // Fields allowed for `Data`: + // `data` if new_update_authority.is_some() - || data.is_some() || primary_sale_happened.is_some() || is_mutable.is_some() + || collection.is_some() || collection_details.is_some() || uses.is_some() || rule_set.is_some() @@ -361,8 +359,8 @@ fn check_desired_token_standard( existing_or_inferred_token_standard: TokenStandard, desired_token_standard: TokenStandard, ) -> ProgramResult { - // This code only allows switching between Fungible and FungibleAsset, and only when - // mint decimals is zero. + // This function only allows switching between Fungible and FungibleAsset. Mint decimals must + // be zero. if !mint_decimals_is_zero { return Err(MetadataError::InvalidTokenStandard.into()); } diff --git a/token-metadata/program/src/processor/verification/collection.rs b/token-metadata/program/src/processor/verification/collection.rs index aea56548cd..811aecb0d4 100644 --- a/token-metadata/program/src/processor/verification/collection.rs +++ b/token-metadata/program/src/processor/verification/collection.rs @@ -160,8 +160,9 @@ pub(crate) fn unverify_collection_v1(program_id: &Pubkey, ctx: Context let authority_response = if parent_burned { // If the collection parent is burned, we need to use an authority for the item rather than - // the collection. The required authority is either the item's metadata update authority, - // or an update delegate for the item. This call fails if no valid authority is present. + // the collection. The required authority is either the item's metadata update authority + // or a delegate for the item that can update the item's collection field. This call fails + // if no valid authority is present. auth_request.mint = &metadata.mint; auth_request.update_authority = &metadata.update_authority; auth_request.metadata_delegate_roles = vec![ @@ -185,6 +186,10 @@ pub(crate) fn unverify_collection_v1(program_id: &Pubkey, ctx: Context // If the collection parent is not burned, the required authority is either the collection // parent's metadata update authority, or a collection delegate for the collection parent. // This call fails if no valid authority is present. + // + // Note that this is sending the delegate in the `metadata_delegate_roles` vec and NOT the + // `collection_metadata_delegate_roles` vec because in this case we are authorizing using + // the collection parent's update authority. auth_request.mint = collection_mint_info.key; auth_request.update_authority = &collection_metadata.update_authority; auth_request.metadata_delegate_roles = vec![MetadataDelegateRole::Collection]; diff --git a/token-metadata/program/src/state/metadata.rs b/token-metadata/program/src/state/metadata.rs index a357998bea..2e6e12a82a 100644 --- a/token-metadata/program/src/state/metadata.rs +++ b/token-metadata/program/src/state/metadata.rs @@ -88,7 +88,7 @@ impl Metadata { update_authority: &AccountInfo<'a>, metadata: &AccountInfo<'a>, token: Option, - token_standard: Option, + token_standard: TokenStandard, authority_type: AuthorityType, delegate_role: Option, ) -> ProgramResult { @@ -114,18 +114,8 @@ impl Metadata { rule_set ); - // updates the token standard only if the current value is None - let token_standard = match self.token_standard { - Some(ts) => ts, - None => { - if let Some(ts) = token_standard { - self.token_standard = Some(ts); - ts - } else { - return Err(MetadataError::InvalidTokenStandard.into()); - } - } - }; + // Update the token standard. + self.token_standard = Some(token_standard); if matches!(authority_type, AuthorityType::Metadata) || matches!(delegate_role, Some(MetadataDelegateRole::Data)) diff --git a/token-metadata/program/tests/utils/digital_asset.rs b/token-metadata/program/tests/utils/digital_asset.rs index 4fb2860c1a..b6e7a0e772 100644 --- a/token-metadata/program/tests/utils/digital_asset.rs +++ b/token-metadata/program/tests/utils/digital_asset.rs @@ -561,10 +561,23 @@ impl DigitalAsset { .spl_token_program(spl_token::ID); match args { - DelegateArgs::AuthorityV1 { .. } => { + // Token delegates. + DelegateArgs::SaleV1 { .. } + | DelegateArgs::TransferV1 { .. } + | DelegateArgs::UtilityV1 { .. } + | DelegateArgs::StakingV1 { .. } + | DelegateArgs::LockedTransferV1 { .. } => { + let (token_record, _) = + find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); + builder.token_record(token_record); + } + DelegateArgs::StandardV1 { .. } => { /* nothing to add */ } + + // Metadata delegates. + DelegateArgs::CollectionV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::Authority, + MetadataDelegateRole::Collection, &payer.pubkey(), &delegate, ); @@ -579,29 +592,28 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } - - DelegateArgs::CollectionV1 { .. } => { + DelegateArgs::ProgrammableConfigV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::Collection, + MetadataDelegateRole::ProgrammableConfig, &payer.pubkey(), &delegate, ); builder.delegate_record(delegate_record); } - DelegateArgs::CollectionItemV1 { .. } => { + DelegateArgs::AuthorityV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::CollectionItem, + MetadataDelegateRole::Authority, &payer.pubkey(), &delegate, ); builder.delegate_record(delegate_record); } - DelegateArgs::ProgrammableConfigV1 { .. } => { + DelegateArgs::CollectionItemV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::ProgrammableConfig, + MetadataDelegateRole::CollectionItem, &payer.pubkey(), &delegate, ); @@ -616,17 +628,6 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } - - DelegateArgs::SaleV1 { .. } - | DelegateArgs::TransferV1 { .. } - | DelegateArgs::UtilityV1 { .. } - | DelegateArgs::StakingV1 { .. } - | DelegateArgs::LockedTransferV1 { .. } => { - let (token_record, _) = - find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); - builder.token_record(token_record); - } - DelegateArgs::StandardV1 { .. } => { /* nothing to add */ } } if let Some(edition) = self.edition { @@ -791,10 +792,24 @@ impl DigitalAsset { .spl_token_program(spl_token::ID); match args { - RevokeArgs::AuthorityV1 => { + // Token delegates. + RevokeArgs::SaleV1 + | RevokeArgs::TransferV1 + | RevokeArgs::UtilityV1 + | RevokeArgs::StakingV1 + | RevokeArgs::LockedTransferV1 + | RevokeArgs::MigrationV1 => { + let (token_record, _) = + find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); + builder.token_record(token_record); + } + RevokeArgs::StandardV1 { .. } => { /* nothing to add */ } + + // Metadata delegates. + RevokeArgs::CollectionV1 => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::Authority, + MetadataDelegateRole::Collection, &payer.pubkey(), &delegate, ); @@ -809,33 +824,34 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } - RevokeArgs::CollectionV1 => { + RevokeArgs::ProgrammableConfigV1 => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::Collection, + MetadataDelegateRole::ProgrammableConfig, &payer.pubkey(), &delegate, ); builder.delegate_record(delegate_record); } - RevokeArgs::CollectionItemV1 => { + RevokeArgs::AuthorityV1 => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::CollectionItem, + MetadataDelegateRole::Authority, &payer.pubkey(), &delegate, ); builder.delegate_record(delegate_record); } - RevokeArgs::ProgrammableConfigV1 => { + RevokeArgs::CollectionItemV1 => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), - MetadataDelegateRole::ProgrammableConfig, + MetadataDelegateRole::CollectionItem, &payer.pubkey(), &delegate, ); builder.delegate_record(delegate_record); } + RevokeArgs::ProgrammableConfigItemV1 => { let (delegate_record, _) = find_metadata_delegate_record_account( &self.mint.pubkey(), @@ -845,17 +861,6 @@ impl DigitalAsset { ); builder.delegate_record(delegate_record); } - RevokeArgs::SaleV1 - | RevokeArgs::TransferV1 - | RevokeArgs::UtilityV1 - | RevokeArgs::StakingV1 - | RevokeArgs::LockedTransferV1 - | RevokeArgs::MigrationV1 => { - let (token_record, _) = - find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); - builder.token_record(token_record); - } - RevokeArgs::StandardV1 { .. } => { /* nothing to add */ } } if let Some(edition) = self.edition { From 5789cfb7cc1dfa383ec9bd067770d219299f4d3f Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 01:51:40 -0700 Subject: [PATCH 06/13] Change unverify test on collection delegate behavior change --- token-metadata/program/tests/unverify.rs | 91 +++++++++++++++++++----- 1 file changed, 75 insertions(+), 16 deletions(-) diff --git a/token-metadata/program/tests/unverify.rs b/token-metadata/program/tests/unverify.rs index d2d5460f1b..6122a8de4a 100644 --- a/token-metadata/program/tests/unverify.rs +++ b/token-metadata/program/tests/unverify.rs @@ -2030,6 +2030,81 @@ mod unverify_collection { .await; } + #[tokio::test] + async fn pass_unverify_burned_pnft_parent_using_item_collection_delegate() { + let mut context = program_test().start_with_context().await; + + let mut test_items = create_mint_verify_collection_check( + &mut context, + DEFAULT_COLLECTION_DETAILS, + TokenStandard::ProgrammableNonFungible, + TokenStandard::ProgrammableNonFungible, + ) + .await; + + // Burn collection parent. + let args = BurnArgs::V1 { amount: 1 }; + let payer = context.payer.dirty_clone(); + test_items + .collection_parent_da + .burn(&mut context, payer, args, None, None) + .await + .unwrap(); + + // Assert that metadata, edition, token and token record accounts are closed. + test_items + .collection_parent_da + .assert_burned(&mut context) + .await + .unwrap(); + + // Create a metadata update delegate for the item. + let delegate = Keypair::new(); + airdrop(&mut context, &delegate.pubkey(), LAMPORTS_PER_SOL) + .await + .unwrap(); + + let payer = context.payer.dirty_clone(); + let payer_pubkey = payer.pubkey(); + let delegate_args = DelegateArgs::CollectionV1 { + authorization_data: None, + }; + test_items + .da + .delegate(&mut context, payer, delegate.pubkey(), delegate_args) + .await + .unwrap(); + + // Find delegate record PDA. + let (delegate_record, _) = find_metadata_delegate_record_account( + &test_items.da.mint.pubkey(), + MetadataDelegateRole::Collection, + &payer_pubkey, + &delegate.pubkey(), + ); + + // Unverify. + let args = VerificationArgs::CollectionV1; + test_items + .da + .unverify( + &mut context, + delegate, + args, + None, + Some(delegate_record), + Some(test_items.collection_parent_da.mint.pubkey()), + Some(test_items.collection_parent_da.metadata), + ) + .await + .unwrap(); + + test_items + .da + .assert_item_collection_matches_on_chain(&mut context, &test_items.collection) + .await; + } + #[tokio::test] async fn pass_unverify_burned_pnft_parent_using_item_collection_item_delegate() { let mut context = program_test().start_with_context().await; @@ -2153,22 +2228,6 @@ mod unverify_collection { .await; } - #[tokio::test] - async fn items_collection_delegate_cannot_unverify_burned_pnft_parent() { - let delegate_args = DelegateArgs::CollectionV1 { - authorization_data: None, - }; - - let delegate_role = MetadataDelegateRole::Collection; - - other_metadata_delegates_cannot_unverify_burned_pnft_parent( - AssetToDelegate::Item, - delegate_args, - delegate_role, - ) - .await; - } - #[tokio::test] async fn items_prgm_config_delegate_cannot_unverify_burned_pnft_parent() { let delegate_args = DelegateArgs::ProgrammableConfigV1 { From 23cfe4338d91d3919bfbebd936e15bb7fd1702c2 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 02:14:54 -0700 Subject: [PATCH 07/13] Fix JS Update test --- token-metadata/js/test/update.test.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/token-metadata/js/test/update.test.ts b/token-metadata/js/test/update.test.ts index f627969d0a..abe03ca355 100644 --- a/token-metadata/js/test/update.test.ts +++ b/token-metadata/js/test/update.test.ts @@ -1201,7 +1201,8 @@ test('Update: Delegate Authority Type Not Supported', async (t) => { amman.addr.addLabel('Delegate Record', delegateRecord); const args: DelegateArgs = { - __kind: 'UpdateV1', + __kind: 'SaleV1', + amount: 1, authorizationData: null, }; From 71db91ac01e3912c5eb43d7cb65d1063bc0b4824 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 03:12:15 -0700 Subject: [PATCH 08/13] Modify DA test util object to return delegate or token record * Return the value derived in the delegate method. * Also add a test for Authority delegate. --- token-metadata/program/tests/update.rs | 78 ++++++++++++++++++- .../program/tests/utils/digital_asset.rs | 14 +++- 2 files changed, 89 insertions(+), 3 deletions(-) diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index b3bc167514..78343bd6a0 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -29,7 +29,7 @@ mod update { use super::*; #[tokio::test] - async fn success_update() { + async fn success_update_by_update_authority() { let context = &mut program_test().start_with_context().await; let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); @@ -101,6 +101,82 @@ mod update { assert_eq!(metadata.data.uri, new_uri); } + #[tokio::test] + async fn success_update_by_authority_delegate() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create(context, TokenStandard::NonFungible, None) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.update_authority, update_authority.pubkey()); + assert!(!metadata.primary_sale_happened); + assert!(metadata.is_mutable); + + // Create `Authority` metadata delegate. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_record = da + .delegate( + context, + update_authority, + delegate.pubkey(), + DelegateArgs::AuthorityV1 { + authorization_data: None, + }, + ) + .await + .unwrap() + .unwrap(); + + // Change a few values that this delegate is allowed to change. + let mut update_args = UpdateArgs::default(); + let (new_update_authority, primary_sale_happened, is_mutable) = get_update_args_fields!( + &mut update_args, + new_update_authority, + primary_sale_happened, + is_mutable + ); + *new_update_authority = Some(delegate.pubkey()); + *primary_sale_happened = Some(true); + *is_mutable = Some(false); + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + //let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + + assert_eq!(metadata.update_authority, delegate.pubkey()); + assert!(metadata.primary_sale_happened); + assert!(!metadata.is_mutable); + } + #[tokio::test] async fn update_pfnt_config() { let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); diff --git a/token-metadata/program/tests/utils/digital_asset.rs b/token-metadata/program/tests/utils/digital_asset.rs index b6e7a0e772..fae02f5136 100644 --- a/token-metadata/program/tests/utils/digital_asset.rs +++ b/token-metadata/program/tests/utils/digital_asset.rs @@ -550,7 +550,7 @@ impl DigitalAsset { payer: Keypair, delegate: Pubkey, args: DelegateArgs, - ) -> Result<(), BanksClientError> { + ) -> Result, BanksClientError> { let mut builder = DelegateBuilder::new(); builder .delegate(delegate) @@ -560,6 +560,8 @@ impl DigitalAsset { .authority(payer.pubkey()) .spl_token_program(spl_token::ID); + let mut delegate_or_token_record = None; + match args { // Token delegates. DelegateArgs::SaleV1 { .. } @@ -570,6 +572,7 @@ impl DigitalAsset { let (token_record, _) = find_token_record_account(&self.mint.pubkey(), &self.token.unwrap()); builder.token_record(token_record); + delegate_or_token_record = Some(token_record); } DelegateArgs::StandardV1 { .. } => { /* nothing to add */ } @@ -582,6 +585,7 @@ impl DigitalAsset { &delegate, ); builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); } DelegateArgs::DataV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( @@ -591,6 +595,7 @@ impl DigitalAsset { &delegate, ); builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); } DelegateArgs::ProgrammableConfigV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( @@ -600,6 +605,7 @@ impl DigitalAsset { &delegate, ); builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); } DelegateArgs::AuthorityV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( @@ -609,6 +615,7 @@ impl DigitalAsset { &delegate, ); builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); } DelegateArgs::CollectionItemV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( @@ -618,6 +625,7 @@ impl DigitalAsset { &delegate, ); builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); } DelegateArgs::ProgrammableConfigItemV1 { .. } => { let (delegate_record, _) = find_metadata_delegate_record_account( @@ -627,6 +635,7 @@ impl DigitalAsset { &delegate, ); builder.delegate_record(delegate_record); + delegate_or_token_record = Some(delegate_record); } } @@ -661,7 +670,8 @@ impl DigitalAsset { context.last_blockhash, ); - context.banks_client.process_transaction(tx).await + context.banks_client.process_transaction(tx).await?; + Ok(delegate_or_token_record) } pub async fn migrate( From 8cf0e57ccf18e661f3ef0003b9d2d7dbf12da0b3 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 03:35:00 -0700 Subject: [PATCH 09/13] Add collection delegate test --- token-metadata/program/tests/update.rs | 73 +++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 2 deletions(-) diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index 78343bd6a0..60c7ad0db0 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -20,8 +20,8 @@ mod update { use mpl_token_metadata::{ error::MetadataError, - instruction::{DelegateArgs, RuleSetToggle, UpdateArgs}, - state::{Creator, Data, ProgrammableConfig, TokenStandard}, + instruction::{CollectionToggle, DelegateArgs, RuleSetToggle, UpdateArgs}, + state::{Collection, Creator, Data, ProgrammableConfig, TokenStandard}, }; use solana_program::pubkey::Pubkey; use solana_sdk::signature::Keypair; @@ -177,6 +177,75 @@ mod update { assert!(!metadata.is_mutable); } + #[tokio::test] + async fn success_update_by_collection_delegate() { + let context = &mut program_test().start_with_context().await; + + let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + + let mut da = DigitalAsset::new(); + da.create(context, TokenStandard::NonFungible, None) + .await + .unwrap(); + + let metadata = da.get_metadata(context).await; + assert_eq!(metadata.collection, None); + + // Create `Collection` metadata delegate. + let delegate = Keypair::new(); + delegate.airdrop(context, 1_000_000_000).await.unwrap(); + let delegate_record = da + .delegate( + context, + update_authority, + delegate.pubkey(), + DelegateArgs::CollectionV1 { + authorization_data: None, + }, + ) + .await + .unwrap() + .unwrap(); + + // Change a value that this delegate is allowed to change. + let mut update_args = UpdateArgs::default(); + let collection_toggle = get_update_args_fields!(&mut update_args, collection); + let new_collection = Collection { + verified: false, + key: Keypair::new().pubkey(), + }; + *collection_toggle.0 = CollectionToggle::Set(new_collection.clone()); + + let mut builder = UpdateBuilder::new(); + builder + .authority(delegate.pubkey()) + .delegate_record(delegate_record) + .metadata(da.metadata) + .mint(da.mint.pubkey()) + .payer(delegate.pubkey()); + + if let Some(edition) = da.edition { + builder.edition(edition); + } + + let update_ix = builder.build(update_args).unwrap().instruction(); + + //let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); + let tx = Transaction::new_signed_with_payer( + &[update_ix], + Some(&delegate.pubkey()), + &[&delegate], + context.last_blockhash, + ); + + context.banks_client.process_transaction(tx).await.unwrap(); + + // checks the created metadata values + let metadata = da.get_metadata(context).await; + + assert_eq!(metadata.collection, Some(new_collection)); + } + #[tokio::test] async fn update_pfnt_config() { let mut program_test = ProgramTest::new("mpl_token_metadata", mpl_token_metadata::ID, None); From 2920b21dec6247805b70bd10b2845c33dce4b4f6 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 03:35:39 -0700 Subject: [PATCH 10/13] Comment out JS test where the delegate is no longer available --- token-metadata/js/test/update.test.ts | 152 +++++++++++++------------- 1 file changed, 76 insertions(+), 76 deletions(-) diff --git a/token-metadata/js/test/update.test.ts b/token-metadata/js/test/update.test.ts index abe03ca355..e09b0eee11 100644 --- a/token-metadata/js/test/update.test.ts +++ b/token-metadata/js/test/update.test.ts @@ -1177,82 +1177,82 @@ test('Update: Invalid Update Authority Fails', async (t) => { await updateTx.assertError(t, /Invalid authority type/); }); -test('Update: Delegate Authority Type Not Supported', async (t) => { - const API = new InitTransactions(); - const { fstTxHandler: handler, payerPair: payer, connection } = await API.payer(); - - const daManager = await createDefaultAsset(t, connection, API, handler, payer); - - // creates a delegate - - const [, delegate] = await API.getKeypair('Delegate'); - // delegate PDA - const [delegateRecord] = PublicKey.findProgramAddressSync( - [ - Buffer.from('metadata'), - PROGRAM_ID.toBuffer(), - daManager.mint.toBuffer(), - Buffer.from('update_delegate'), - payer.publicKey.toBuffer(), - delegate.publicKey.toBuffer(), - ], - PROGRAM_ID, - ); - amman.addr.addLabel('Delegate Record', delegateRecord); - - const args: DelegateArgs = { - __kind: 'SaleV1', - amount: 1, - authorizationData: null, - }; - - const { tx: delegateTx } = await API.delegate( - delegate.publicKey, - daManager.mint, - daManager.metadata, - payer.publicKey, - payer, - args, - handler, - delegateRecord, - daManager.masterEdition, - ); - await delegateTx.assertSuccess(t); - - const assetData = await daManager.getAssetData(connection); - const authority = delegate; - - // Change some values and run update. - const data: Data = { - name: 'DigitalAsset2', - symbol: 'DA2', - uri: 'uri2', - sellerFeeBasisPoints: 10, - creators: assetData.creators, - }; - const authorizationData = daManager.emptyAuthorizationData(); - - const updateData = new UpdateTestData(); - updateData.data = data; - updateData.authorizationData = authorizationData; - - const { tx: updateTx } = await API.update( - t, - handler, - daManager.mint, - daManager.metadata, - authority, - updateData, - delegateRecord, - daManager.masterEdition, - ); - updateTx.then((x) => - x.assertLogs(t, [/Invalid authority type/i], { - txLabel: 'tx: Update', - }), - ); - await updateTx.assertError(t); -}); +// test('Update: Delegate Authority Type Not Supported', async (t) => { +// const API = new InitTransactions(); +// const { fstTxHandler: handler, payerPair: payer, connection } = await API.payer(); + +// const daManager = await createDefaultAsset(t, connection, API, handler, payer); + +// // creates a delegate + +// const [, delegate] = await API.getKeypair('Delegate'); +// // delegate PDA +// const [delegateRecord] = PublicKey.findProgramAddressSync( +// [ +// Buffer.from('metadata'), +// PROGRAM_ID.toBuffer(), +// daManager.mint.toBuffer(), +// Buffer.from('update_delegate'), +// payer.publicKey.toBuffer(), +// delegate.publicKey.toBuffer(), +// ], +// PROGRAM_ID, +// ); +// amman.addr.addLabel('Delegate Record', delegateRecord); + +// const args: DelegateArgs = { +// __kind: 'UpdateV1', +// amount: 1, +// authorizationData: null, +// }; + +// const { tx: delegateTx } = await API.delegate( +// delegate.publicKey, +// daManager.mint, +// daManager.metadata, +// payer.publicKey, +// payer, +// args, +// handler, +// delegateRecord, +// daManager.masterEdition, +// ); +// await delegateTx.assertSuccess(t); + +// const assetData = await daManager.getAssetData(connection); +// const authority = delegate; + +// // Change some values and run update. +// const data: Data = { +// name: 'DigitalAsset2', +// symbol: 'DA2', +// uri: 'uri2', +// sellerFeeBasisPoints: 10, +// creators: assetData.creators, +// }; +// const authorizationData = daManager.emptyAuthorizationData(); + +// const updateData = new UpdateTestData(); +// updateData.data = data; +// updateData.authorizationData = authorizationData; + +// const { tx: updateTx } = await API.update( +// t, +// handler, +// daManager.mint, +// daManager.metadata, +// authority, +// updateData, +// delegateRecord, +// daManager.masterEdition, +// ); +// updateTx.then((x) => +// x.assertLogs(t, [/Invalid authority type/i], { +// txLabel: 'tx: Update', +// }), +// ); +// await updateTx.assertError(t); +// }); test('Update: Holder Authority Type Not Supported', async (t) => { const API = new InitTransactions(); From dc4852c76ef1d533043e0d46370d57d30b9f4dce Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 03:43:17 -0700 Subject: [PATCH 11/13] Add test for collection item delegate --- token-metadata/program/tests/update.rs | 28 +++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/token-metadata/program/tests/update.rs b/token-metadata/program/tests/update.rs index 60c7ad0db0..1a6a0f977c 100644 --- a/token-metadata/program/tests/update.rs +++ b/token-metadata/program/tests/update.rs @@ -178,7 +178,24 @@ mod update { } #[tokio::test] - async fn success_update_by_collection_delegate() { + async fn success_update_by_items_collection_delegate() { + let args = DelegateArgs::CollectionItemV1 { + authorization_data: None, + }; + + success_update_collection_by_items_delegate(args).await; + } + + #[tokio::test] + async fn success_update_by_items_collection_item_delegate() { + let args = DelegateArgs::CollectionItemV1 { + authorization_data: None, + }; + + success_update_collection_by_items_delegate(args).await; + } + + async fn success_update_collection_by_items_delegate(delegate_args: DelegateArgs) { let context = &mut program_test().start_with_context().await; let update_authority = Keypair::from_bytes(&context.payer.to_bytes()).unwrap(); @@ -195,14 +212,7 @@ mod update { let delegate = Keypair::new(); delegate.airdrop(context, 1_000_000_000).await.unwrap(); let delegate_record = da - .delegate( - context, - update_authority, - delegate.pubkey(), - DelegateArgs::CollectionV1 { - authorization_data: None, - }, - ) + .delegate(context, update_authority, delegate.pubkey(), delegate_args) .await .unwrap() .unwrap(); From 5fd48e7172d5960f04e7374cea6b2b0e891329c8 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 03:46:05 -0700 Subject: [PATCH 12/13] Regenerate JS API --- token-metadata/js/idl/mpl_token_metadata.json | 131 +++++++++++++++++- .../js/src/generated/types/DelegateArgs.ts | 50 ++++++- .../generated/types/MetadataDelegateRole.ts | 4 +- .../js/src/generated/types/RevokeArgs.ts | 5 +- .../js/src/generated/types/UpdateArgs.ts | 34 +++++ 5 files changed, 212 insertions(+), 12 deletions(-) diff --git a/token-metadata/js/idl/mpl_token_metadata.json b/token-metadata/js/idl/mpl_token_metadata.json index 2526a52b2f..5ba20bfd41 100644 --- a/token-metadata/js/idl/mpl_token_metadata.json +++ b/token-metadata/js/idl/mpl_token_metadata.json @@ -4671,7 +4671,7 @@ ] }, { - "name": "UpdateV1", + "name": "DataV1", "fields": [ { "name": "authorization_data", @@ -4759,6 +4759,45 @@ } } ] + }, + { + "name": "AuthorityV1", + "fields": [ + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "CollectionItemV1", + "fields": [ + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] + }, + { + "name": "ProgrammableConfigItemV1", + "fields": [ + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] } ] } @@ -4778,7 +4817,7 @@ "name": "TransferV1" }, { - "name": "UpdateV1" + "name": "DataV1" }, { "name": "UtilityV1" @@ -4797,6 +4836,15 @@ }, { "name": "MigrationV1" + }, + { + "name": "AuthorityV1" + }, + { + "name": "CollectionItemV1" + }, + { + "name": "ProgrammableConfigItemV1" } ] } @@ -4816,10 +4864,16 @@ "name": "Use" }, { - "name": "Update" + "name": "Data" }, { "name": "ProgrammableConfig" + }, + { + "name": "CollectionItem" + }, + { + "name": "ProgrammableConfigItem" } ] } @@ -4974,6 +5028,77 @@ } } ] + }, + { + "name": "V2", + "fields": [ + { + "name": "new_update_authority", + "type": { + "option": "publicKey" + } + }, + { + "name": "data", + "type": { + "option": { + "defined": "Data" + } + } + }, + { + "name": "primary_sale_happened", + "type": { + "option": "bool" + } + }, + { + "name": "is_mutable", + "type": { + "option": "bool" + } + }, + { + "name": "collection", + "type": { + "defined": "CollectionToggle" + } + }, + { + "name": "collection_details", + "type": { + "defined": "CollectionDetailsToggle" + } + }, + { + "name": "uses", + "type": { + "defined": "UsesToggle" + } + }, + { + "name": "rule_set", + "type": { + "defined": "RuleSetToggle" + } + }, + { + "name": "token_standard", + "type": { + "option": { + "defined": "TokenStandard" + } + } + }, + { + "name": "authorization_data", + "type": { + "option": { + "defined": "AuthorizationData" + } + } + } + ] } ] } diff --git a/token-metadata/js/src/generated/types/DelegateArgs.ts b/token-metadata/js/src/generated/types/DelegateArgs.ts index 395df7c152..df9a9538a6 100644 --- a/token-metadata/js/src/generated/types/DelegateArgs.ts +++ b/token-metadata/js/src/generated/types/DelegateArgs.ts @@ -22,7 +22,7 @@ export type DelegateArgsRecord = { CollectionV1: { authorizationData: beet.COption }; SaleV1: { amount: beet.bignum; authorizationData: beet.COption }; TransferV1: { amount: beet.bignum; authorizationData: beet.COption }; - UpdateV1: { authorizationData: beet.COption }; + DataV1: { authorizationData: beet.COption }; UtilityV1: { amount: beet.bignum; authorizationData: beet.COption }; StakingV1: { amount: beet.bignum; authorizationData: beet.COption }; StandardV1: { amount: beet.bignum }; @@ -32,6 +32,9 @@ export type DelegateArgsRecord = { authorizationData: beet.COption; }; ProgrammableConfigV1: { authorizationData: beet.COption }; + AuthorityV1: { authorizationData: beet.COption }; + CollectionItemV1: { authorizationData: beet.COption }; + ProgrammableConfigItemV1: { authorizationData: beet.COption }; }; /** @@ -55,9 +58,8 @@ export const isDelegateArgsSaleV1 = (x: DelegateArgs): x is DelegateArgs & { __k export const isDelegateArgsTransferV1 = ( x: DelegateArgs, ): x is DelegateArgs & { __kind: 'TransferV1' } => x.__kind === 'TransferV1'; -export const isDelegateArgsUpdateV1 = ( - x: DelegateArgs, -): x is DelegateArgs & { __kind: 'UpdateV1' } => x.__kind === 'UpdateV1'; +export const isDelegateArgsDataV1 = (x: DelegateArgs): x is DelegateArgs & { __kind: 'DataV1' } => + x.__kind === 'DataV1'; export const isDelegateArgsUtilityV1 = ( x: DelegateArgs, ): x is DelegateArgs & { __kind: 'UtilityV1' } => x.__kind === 'UtilityV1'; @@ -73,6 +75,16 @@ export const isDelegateArgsLockedTransferV1 = ( export const isDelegateArgsProgrammableConfigV1 = ( x: DelegateArgs, ): x is DelegateArgs & { __kind: 'ProgrammableConfigV1' } => x.__kind === 'ProgrammableConfigV1'; +export const isDelegateArgsAuthorityV1 = ( + x: DelegateArgs, +): x is DelegateArgs & { __kind: 'AuthorityV1' } => x.__kind === 'AuthorityV1'; +export const isDelegateArgsCollectionItemV1 = ( + x: DelegateArgs, +): x is DelegateArgs & { __kind: 'CollectionItemV1' } => x.__kind === 'CollectionItemV1'; +export const isDelegateArgsProgrammableConfigItemV1 = ( + x: DelegateArgs, +): x is DelegateArgs & { __kind: 'ProgrammableConfigItemV1' } => + x.__kind === 'ProgrammableConfigItemV1'; /** * @category userTypes @@ -110,10 +122,10 @@ export const delegateArgsBeet = beet.dataEnum([ ], [ - 'UpdateV1', - new beet.FixableBeetArgsStruct( + 'DataV1', + new beet.FixableBeetArgsStruct( [['authorizationData', beet.coption(authorizationDataBeet)]], - 'DelegateArgsRecord["UpdateV1"]', + 'DelegateArgsRecord["DataV1"]', ), ], @@ -166,4 +178,28 @@ export const delegateArgsBeet = beet.dataEnum([ 'DelegateArgsRecord["ProgrammableConfigV1"]', ), ], + + [ + 'AuthorityV1', + new beet.FixableBeetArgsStruct( + [['authorizationData', beet.coption(authorizationDataBeet)]], + 'DelegateArgsRecord["AuthorityV1"]', + ), + ], + + [ + 'CollectionItemV1', + new beet.FixableBeetArgsStruct( + [['authorizationData', beet.coption(authorizationDataBeet)]], + 'DelegateArgsRecord["CollectionItemV1"]', + ), + ], + + [ + 'ProgrammableConfigItemV1', + new beet.FixableBeetArgsStruct( + [['authorizationData', beet.coption(authorizationDataBeet)]], + 'DelegateArgsRecord["ProgrammableConfigItemV1"]', + ), + ], ]) as beet.FixableBeet; diff --git a/token-metadata/js/src/generated/types/MetadataDelegateRole.ts b/token-metadata/js/src/generated/types/MetadataDelegateRole.ts index e103d46880..5ca7165221 100644 --- a/token-metadata/js/src/generated/types/MetadataDelegateRole.ts +++ b/token-metadata/js/src/generated/types/MetadataDelegateRole.ts @@ -14,8 +14,10 @@ export enum MetadataDelegateRole { Authority, Collection, Use, - Update, + Data, ProgrammableConfig, + CollectionItem, + ProgrammableConfigItem, } /** diff --git a/token-metadata/js/src/generated/types/RevokeArgs.ts b/token-metadata/js/src/generated/types/RevokeArgs.ts index 9ba7c3b1aa..fed9bd445d 100644 --- a/token-metadata/js/src/generated/types/RevokeArgs.ts +++ b/token-metadata/js/src/generated/types/RevokeArgs.ts @@ -14,13 +14,16 @@ export enum RevokeArgs { CollectionV1, SaleV1, TransferV1, - UpdateV1, + DataV1, UtilityV1, StakingV1, StandardV1, LockedTransferV1, ProgrammableConfigV1, MigrationV1, + AuthorityV1, + CollectionItemV1, + ProgrammableConfigItemV1, } /** diff --git a/token-metadata/js/src/generated/types/UpdateArgs.ts b/token-metadata/js/src/generated/types/UpdateArgs.ts index 7b731d9f0c..694c5a0328 100644 --- a/token-metadata/js/src/generated/types/UpdateArgs.ts +++ b/token-metadata/js/src/generated/types/UpdateArgs.ts @@ -14,6 +14,7 @@ import { CollectionDetailsToggle, collectionDetailsToggleBeet } from './Collecti import { UsesToggle, usesToggleBeet } from './UsesToggle'; import { RuleSetToggle, ruleSetToggleBeet } from './RuleSetToggle'; import { AuthorizationData, authorizationDataBeet } from './AuthorizationData'; +import { TokenStandard, tokenStandardBeet } from './TokenStandard'; /** * This type is used to derive the {@link UpdateArgs} type as well as the de/serializer. * However don't refer to it in your code but use the {@link UpdateArgs} type instead. @@ -35,6 +36,18 @@ export type UpdateArgsRecord = { ruleSet: RuleSetToggle; authorizationData: beet.COption; }; + V2: { + newUpdateAuthority: beet.COption; + data: beet.COption; + primarySaleHappened: beet.COption; + isMutable: beet.COption; + collection: CollectionToggle; + collectionDetails: CollectionDetailsToggle; + uses: UsesToggle; + ruleSet: RuleSetToggle; + tokenStandard: beet.COption; + authorizationData: beet.COption; + }; }; /** @@ -52,6 +65,8 @@ export type UpdateArgs = beet.DataEnumKeyAsKind; export const isUpdateArgsV1 = (x: UpdateArgs): x is UpdateArgs & { __kind: 'V1' } => x.__kind === 'V1'; +export const isUpdateArgsV2 = (x: UpdateArgs): x is UpdateArgs & { __kind: 'V2' } => + x.__kind === 'V2'; /** * @category userTypes @@ -75,4 +90,23 @@ export const updateArgsBeet = beet.dataEnum([ 'UpdateArgsRecord["V1"]', ), ], + + [ + 'V2', + new beet.FixableBeetArgsStruct( + [ + ['newUpdateAuthority', beet.coption(beetSolana.publicKey)], + ['data', beet.coption(dataBeet)], + ['primarySaleHappened', beet.coption(beet.bool)], + ['isMutable', beet.coption(beet.bool)], + ['collection', collectionToggleBeet], + ['collectionDetails', collectionDetailsToggleBeet], + ['uses', usesToggleBeet], + ['ruleSet', ruleSetToggleBeet], + ['tokenStandard', beet.coption(tokenStandardBeet)], + ['authorizationData', beet.coption(authorizationDataBeet)], + ], + 'UpdateArgsRecord["V2"]', + ), + ], ]) as beet.FixableBeet; From fcb5418f9739f5a49da8f268fbc0a7adb0a09797 Mon Sep 17 00:00:00 2001 From: Michael Danenberg <56533526+danenbm@users.noreply.github.com> Date: Thu, 6 Apr 2023 15:24:46 -0700 Subject: [PATCH 13/13] Adding expiration for delegates * Creation time set when PDA is created. * Expiration checked during get_authority_type. * This is a draft idea that passes existing tests, but expiration functionality has not been thoroughly tested. --- token-metadata/program/src/error.rs | 8 +++ .../program/src/instruction/delegate.rs | 20 ++++++ .../src/processor/delegate/delegate.rs | 19 +++-- token-metadata/program/src/state/delegate.rs | 72 +++++++++++++++++++ token-metadata/program/src/state/mod.rs | 1 + .../program/src/state/programmable.rs | 68 +++++++++++++++--- token-metadata/program/tests/delegate.rs | 22 +++++- token-metadata/program/tests/revoke.rs | 22 +++++- 8 files changed, 211 insertions(+), 21 deletions(-) diff --git a/token-metadata/program/src/error.rs b/token-metadata/program/src/error.rs index 0645c9ddf5..77493037fc 100644 --- a/token-metadata/program/src/error.rs +++ b/token-metadata/program/src/error.rs @@ -741,6 +741,14 @@ pub enum MetadataError { /// 187 #[error("Invalid token record account")] InvalidTokenRecord, + + /// 188 + #[error("Invalid metadata delegate record account")] + InvalidDelegateRecord, + + /// 189 + #[error("Delegate has expired")] + DelegateExpired, } impl PrintProgramError for MetadataError { diff --git a/token-metadata/program/src/instruction/delegate.rs b/token-metadata/program/src/instruction/delegate.rs index 8f4f31b3a2..ed76f4ced8 100644 --- a/token-metadata/program/src/instruction/delegate.rs +++ b/token-metadata/program/src/instruction/delegate.rs @@ -120,6 +120,26 @@ impl fmt::Display for MetadataDelegateRole { } } +pub trait MetadataDelegateExpiration { + fn expiration_time_secs(&self) -> i64; +} + +impl MetadataDelegateExpiration for MetadataDelegateRole { + fn expiration_time_secs(&self) -> i64 { + let one_week: i64 = 3600 * 24 * 7; + let four_weeks = one_week * 4; + match self { + Self::Authority => one_week, + Self::Collection => four_weeks, + Self::Use => four_weeks, + Self::Data => four_weeks, + Self::ProgrammableConfig => four_weeks, + Self::CollectionItem => four_weeks, + Self::ProgrammableConfigItem => four_weeks, + } + } +} + /// Delegates an action over an asset to a specific account. /// /// # Accounts: diff --git a/token-metadata/program/src/processor/delegate/delegate.rs b/token-metadata/program/src/processor/delegate/delegate.rs index c3be34ceae..ba0914131b 100644 --- a/token-metadata/program/src/processor/delegate/delegate.rs +++ b/token-metadata/program/src/processor/delegate/delegate.rs @@ -4,8 +4,13 @@ use borsh::BorshSerialize; use mpl_token_auth_rules::utils::get_latest_revision; use mpl_utils::{assert_signer, create_or_allocate_account_raw}; use solana_program::{ - account_info::AccountInfo, entrypoint::ProgramResult, program::invoke, program_pack::Pack, - pubkey::Pubkey, system_program, sysvar, + account_info::AccountInfo, + entrypoint::ProgramResult, + program::invoke, + program_pack::Pack, + pubkey::Pubkey, + system_program, + sysvar::{self, Sysvar}, }; use spl_token::{instruction::AuthorityType as SplAuthorityType, state::Account}; @@ -19,7 +24,7 @@ use crate::{ pda::{find_token_record_account, PREFIX}, processor::AuthorizationData, state::{ - Metadata, MetadataDelegateRecord, Operation, ProgrammableConfig, Resizable, + Metadata, MetadataDelegateRecordV2, Operation, ProgrammableConfig, Resizable, TokenDelegateRole, TokenMetadataAccount, TokenRecord, TokenStandard, TokenState, }, utils::{auth_rules_validate, freeze, thaw, AuthRulesValidateParams}, @@ -500,15 +505,19 @@ fn create_pda_account<'a>( delegate_record_info, system_program_info, payer_info, - MetadataDelegateRecord::size(), + MetadataDelegateRecordV2::size(), &signer_seeds, )?; - let pda = MetadataDelegateRecord { + // Get the current time. + let current_time = solana_program::clock::Clock::get()?; + + let pda = MetadataDelegateRecordV2 { bump: bump[0], mint: *mint_info.key, delegate: *delegate_info.key, update_authority: *authority_info.key, + creation_time: current_time.unix_timestamp, ..Default::default() }; pda.serialize(&mut *delegate_record_info.try_borrow_mut_data()?)?; diff --git a/token-metadata/program/src/state/delegate.rs b/token-metadata/program/src/state/delegate.rs index 40d795aefa..69d2c1df34 100644 --- a/token-metadata/program/src/state/delegate.rs +++ b/token-metadata/program/src/state/delegate.rs @@ -1,4 +1,6 @@ use super::*; +use crate::instruction::{MetadataDelegateExpiration, MetadataDelegateRole}; +use solana_program::sysvar::Sysvar; const SIZE: usize = 98; @@ -53,3 +55,73 @@ impl MetadataDelegateRecord { Ok(delegate) } } + +// V2 + +#[repr(C)] +#[cfg_attr(feature = "serde-feature", derive(Serialize, Deserialize))] +#[derive(BorshSerialize, BorshDeserialize, PartialEq, Eq, Debug, Clone, ShankAccount)] +/// SEEDS = [ +/// "metadata", +/// program id, +/// mint id, +/// delegate role, +/// update authority id, +/// delegate id +/// ] +pub struct MetadataDelegateRecordV2 { + pub key: Key, // 1 + pub bump: u8, // 1 + #[cfg_attr(feature = "serde-feature", serde(with = "As::"))] + pub mint: Pubkey, // 32 + #[cfg_attr(feature = "serde-feature", serde(with = "As::"))] + pub delegate: Pubkey, // 32 + #[cfg_attr(feature = "serde-feature", serde(with = "As::"))] + pub update_authority: Pubkey, // 32 + pub creation_time: i64, // 8 +} + +impl Default for MetadataDelegateRecordV2 { + fn default() -> Self { + Self { + key: Key::MetadataDelegateV2, + bump: 255, + mint: Pubkey::default(), + delegate: Pubkey::default(), + update_authority: Pubkey::default(), + creation_time: 0, + } + } +} + +impl TokenMetadataAccount for MetadataDelegateRecordV2 { + fn key() -> Key { + Key::MetadataDelegateV2 + } + + fn size() -> usize { + SIZE + 8 + } +} + +impl MetadataDelegateRecordV2 { + pub fn from_bytes(data: &[u8]) -> Result { + let delegate: MetadataDelegateRecordV2 = try_from_slice_checked( + data, + Key::MetadataDelegateV2, + MetadataDelegateRecordV2::size(), + )?; + Ok(delegate) + } + + pub fn check_expiration(&self, role: MetadataDelegateRole) -> ProgramResult { + // Get the current time. + let current_time = solana_program::clock::Clock::get()?; + + if current_time.unix_timestamp > self.creation_time + role.expiration_time_secs() { + Err(MetadataError::DelegateExpired.into()) + } else { + Ok(()) + } + } +} diff --git a/token-metadata/program/src/state/mod.rs b/token-metadata/program/src/state/mod.rs index f9c2340abf..ed06b9fe28 100644 --- a/token-metadata/program/src/state/mod.rs +++ b/token-metadata/program/src/state/mod.rs @@ -137,6 +137,7 @@ pub enum Key { TokenOwnedEscrow, TokenRecord, MetadataDelegate, + MetadataDelegateV2, } #[cfg(feature = "serde-feature")] diff --git a/token-metadata/program/src/state/programmable.rs b/token-metadata/program/src/state/programmable.rs index 91023135b3..70c1c1457f 100644 --- a/token-metadata/program/src/state/programmable.rs +++ b/token-metadata/program/src/state/programmable.rs @@ -338,11 +338,34 @@ impl AuthorityType { ); if cmp_pubkeys(&pda_key, metadata_delegate_record_info.key) { - let delegate_record = MetadataDelegateRecord::from_account_info( - metadata_delegate_record_info, - )?; + // let delegate_record = MetadataDelegateRecord::from_account_info( + // metadata_delegate_record_info, + // )?; + + let data = metadata_delegate_record_info.data.borrow(); + + let key_byte = + data.first().ok_or(MetadataError::InvalidDelegateRecord)?; + let account_key = FromPrimitive::from_u8(*key_byte) + .ok_or(MetadataError::InvalidDelegateRecord)?; + + let delegate = match account_key { + Key::MetadataDelegate => { + let delegate_record = + MetadataDelegateRecord::from_bytes(&data)?; + delegate_record.delegate + } + Key::MetadataDelegateV2 => { + let delegate_record = + MetadataDelegateRecordV2::from_bytes(&data)?; + + delegate_record.check_expiration(*role)?; + delegate_record.delegate + } + _ => return Err(MetadataError::InvalidDelegateRecord.into()), + }; - if delegate_record.delegate == *request.authority { + if delegate == *request.authority { return Ok(AuthorityResponse { authority_type: AuthorityType::MetadataDelegate, metadata_delegate_role: Some(*role), @@ -364,12 +387,37 @@ impl AuthorityType { ); if cmp_pubkeys(&pda_key, metadata_delegate_record_info.key) { - let delegate_record = - MetadataDelegateRecord::from_account_info( - metadata_delegate_record_info, - )?; - - if delegate_record.delegate == *request.authority { + // let delegate_record = + // MetadataDelegateRecord::from_account_info( + // metadata_delegate_record_info, + // )?; + + let data = metadata_delegate_record_info.data.borrow(); + + let key_byte = + data.first().ok_or(MetadataError::InvalidDelegateRecord)?; + let account_key = FromPrimitive::from_u8(*key_byte) + .ok_or(MetadataError::InvalidDelegateRecord)?; + + let delegate = match account_key { + Key::MetadataDelegate => { + let delegate_record = + MetadataDelegateRecord::from_bytes(&data)?; + delegate_record.delegate + } + Key::MetadataDelegateV2 => { + let delegate_record = + MetadataDelegateRecordV2::from_bytes(&data)?; + + delegate_record.check_expiration(*role)?; + delegate_record.delegate + } + _ => { + return Err(MetadataError::InvalidDelegateRecord.into()) + } + }; + + if delegate == *request.authority { return Ok(AuthorityResponse { authority_type: AuthorityType::MetadataDelegate, metadata_delegate_role: Some(*role), diff --git a/token-metadata/program/tests/delegate.rs b/token-metadata/program/tests/delegate.rs index 3a5c30db71..0ba31657b1 100644 --- a/token-metadata/program/tests/delegate.rs +++ b/token-metadata/program/tests/delegate.rs @@ -17,7 +17,7 @@ mod delegate { instruction::{DelegateArgs, MetadataDelegateRole}, pda::{find_metadata_delegate_record_account, find_token_record_account}, state::{ - Key, Metadata, MetadataDelegateRecord, TokenDelegateRole, TokenRecord, TokenStandard, + Key, Metadata, MetadataDelegateRecordV2, TokenDelegateRole, TokenRecord, TokenStandard, }, }; use num_traits::FromPrimitive; @@ -146,8 +146,24 @@ mod delegate { ); let pda = get_account(&mut context, &pda_key).await; - let delegate_record = MetadataDelegateRecord::from_bytes(&pda.data).unwrap(); - assert_eq!(delegate_record.key, Key::MetadataDelegate); + let delegate_record = MetadataDelegateRecordV2::from_bytes(&pda.data).unwrap(); + assert_eq!(delegate_record.key, Key::MetadataDelegateV2); + + // let pda = get_account(&mut context, &pda_key).await; + // let key_byte = pda.data.first().unwrap(); + // let account_key = FromPrimitive::from_u8(*key_byte).unwrap(); + + // match account_key { + // Key::MetadataDelegate => { + // let delegate_record = MetadataDelegateRecord::from_bytes(&pda.data).unwrap(); + // assert_eq!(delegate_record.key, Key::MetadataDelegate); + // } + // Key::MetadataDelegateV2 => { + // let delegate_record = MetadataDelegateRecordV2::from_bytes(&pda.data).unwrap(); + // assert_eq!(delegate_record.key, Key::MetadataDelegateV2); + // } + // _ => panic!("Unexpected key"), + // } } #[tokio::test] diff --git a/token-metadata/program/tests/revoke.rs b/token-metadata/program/tests/revoke.rs index 9dc1ca7b70..9abf5b30a9 100644 --- a/token-metadata/program/tests/revoke.rs +++ b/token-metadata/program/tests/revoke.rs @@ -17,7 +17,7 @@ mod revoke { instruction::{DelegateArgs, MetadataDelegateRole, RevokeArgs}, pda::{find_metadata_delegate_record_account, find_token_record_account}, state::{ - Key, Metadata, MetadataDelegateRecord, TokenDelegateRole, TokenRecord, TokenStandard, + Key, Metadata, MetadataDelegateRecordV2, TokenDelegateRole, TokenRecord, TokenStandard, TOKEN_RECORD_SIZE, }, }; @@ -163,8 +163,24 @@ mod revoke { ); let pda = get_account(&mut context, &pda_key).await; - let delegate_record = MetadataDelegateRecord::from_bytes(&pda.data).unwrap(); - assert_eq!(delegate_record.key, Key::MetadataDelegate); + let delegate_record = MetadataDelegateRecordV2::from_bytes(&pda.data).unwrap(); + assert_eq!(delegate_record.key, Key::MetadataDelegateV2); + + // let pda = get_account(&mut context, &pda_key).await; + // let key_byte = pda.data.first().unwrap(); + // let account_key = FromPrimitive::from_u8(*key_byte).unwrap(); + + // match account_key { + // Key::MetadataDelegate => { + // let delegate_record = MetadataDelegateRecord::from_bytes(&pda.data).unwrap(); + // assert_eq!(delegate_record.key, Key::MetadataDelegate); + // } + // Key::MetadataDelegateV2 => { + // let delegate_record = MetadataDelegateRecordV2::from_bytes(&pda.data).unwrap(); + // assert_eq!(delegate_record.key, Key::MetadataDelegateV2); + // } + // _ => panic!("Unexpected key"), + // } // revokes the delegate let payer = Keypair::from_bytes(&context.payer.to_bytes()).unwrap();