diff --git a/clients/js/src/generated/identity/errors/mplAgentIdentity.ts b/clients/js/src/generated/identity/errors/mplAgentIdentity.ts index 7871958..c55e956 100644 --- a/clients/js/src/generated/identity/errors/mplAgentIdentity.ts +++ b/clients/js/src/generated/identity/errors/mplAgentIdentity.ts @@ -177,6 +177,23 @@ export class GenesisNotMintFundedError extends ProgramError { codeToErrorMap.set(0xb, GenesisNotMintFundedError); nameToErrorMap.set('GenesisNotMintFunded', GenesisNotMintFundedError); +/** GenesisAuthorityMismatch: Genesis account authority does not match the agent wallet */ +export class GenesisAuthorityMismatchError extends ProgramError { + override readonly name: string = 'GenesisAuthorityMismatch'; + + readonly code: number = 0xc; // 12 + + constructor(program: Program, cause?: Error) { + super( + 'Genesis account authority does not match the agent wallet', + program, + cause + ); + } +} +codeToErrorMap.set(0xc, GenesisAuthorityMismatchError); +nameToErrorMap.set('GenesisAuthorityMismatch', GenesisAuthorityMismatchError); + /** * Attempts to resolve a custom program error from the provided error code. * @category Errors diff --git a/clients/js/test/identity/setAgentToken.test.ts b/clients/js/test/identity/setAgentToken.test.ts index 6a30ac8..9e34ff4 100644 --- a/clients/js/test/identity/setAgentToken.test.ts +++ b/clients/js/test/identity/setAgentToken.test.ts @@ -26,7 +26,8 @@ import { } from '../../src/generated/identity'; import { createCollectionAndAsset, createUmi } from '../_setup'; -/** Create a Genesis account via initializeV2 with the given funding mode. */ +/** Create a Genesis account via initializeV2 with the given funding mode. + * Authority defaults to the umi payer. */ async function createGenesisAccount( umi: Awaited>, fundingMode: number @@ -51,6 +52,57 @@ async function createGenesisAccount( return { baseMint: baseMint.publicKey, genesisAccount: genesisAccountPda }; } +/** Create a Genesis account via Execute CPI so the asset signer PDA is the authority. */ +async function createGenesisAccountViaExecute( + umi: Awaited>, + asset: ReturnType, + collection: ReturnType, + fundingMode: number +) { + const baseMint = generateSigner(umi); + const genesisAccountPda = findGenesisAccountV2Pda(umi, { + baseMint: baseMint.publicKey, + genesisIndex: 0, + }); + const assetSignerPda = findAssetSignerPda(umi, { asset }); + + // Build the inner initializeV2 instruction. + const innerTx = initializeV2(umi, { + baseMint, + authority: createNoopSigner(publicKey(assetSignerPda)), + fundingMode, + totalSupplyBaseToken: 1_000_000_000n, + name: 'Test Token', + uri: 'https://example.com/metadata.json', + symbol: 'TST', + }); + + // Wrap in execute CPI. + const executeTx = execute(umi, { + asset: { publicKey: asset }, + collection: { publicKey: collection }, + instructions: innerTx, + }); + + // The mpl-core execute() helper doesn't propagate inner TransactionBuilder + // signers. Manually add them (excluding the asset signer which signs via CPI). + const assetSignerKey = publicKey(assetSignerPda); + const innerSigners = innerTx.items.flatMap((item) => item.signers); + const items = executeTx.items; + for (const item of items) { + item.signers.push( + ...innerSigners.filter((s) => s.publicKey !== assetSignerKey) + ); + } + + await executeTx + .setItems(items) + .prepend(setComputeUnitLimit(umi, { units: 600_000 })) + .sendAndConfirm(umi); + + return { baseMint: baseMint.publicKey, genesisAccount: genesisAccountPda }; +} + test('it can set an agent token', async (t) => { const umi = await createUmi(); const { collection, asset } = await createCollectionAndAsset(umi); @@ -62,8 +114,13 @@ test('it can set an agent token', async (t) => { agentRegistrationUri: 'https://example.com/agent.json', }).sendAndConfirm(umi); - // Create a Genesis account with funding_mode = Mint (0). - const { baseMint, genesisAccount } = await createGenesisAccount(umi, 0); + // Create a Genesis account via Execute CPI so asset signer is the authority. + const { baseMint, genesisAccount } = await createGenesisAccountViaExecute( + umi, + asset, + collection, + 0 + ); // Set agent token via Execute CPI. const assetSignerPda = findAssetSignerPda(umi, { asset }); @@ -116,14 +173,10 @@ test('it cannot set agent token twice', async (t) => { agentRegistrationUri: 'https://example.com/agent.json', }).sendAndConfirm(umi); - const { genesisAccount: genesisAccount1 } = await createGenesisAccount( - umi, - 0 - ); - const { genesisAccount: genesisAccount2 } = await createGenesisAccount( - umi, - 0 - ); + const { genesisAccount: genesisAccount1 } = + await createGenesisAccountViaExecute(umi, asset, collection, 0); + const { genesisAccount: genesisAccount2 } = + await createGenesisAccountViaExecute(umi, asset, collection, 0); const assetSignerPda = findAssetSignerPda(umi, { asset }); diff --git a/clients/rust-identity/src/generated/errors/mpl_agent_identity.rs b/clients/rust-identity/src/generated/errors/mpl_agent_identity.rs index 8679000..0ece4f7 100644 --- a/clients/rust-identity/src/generated/errors/mpl_agent_identity.rs +++ b/clients/rust-identity/src/generated/errors/mpl_agent_identity.rs @@ -47,6 +47,9 @@ pub enum MplAgentIdentityError { /// 11 (0xB) - Genesis account is not mint-funded #[error("Genesis account is not mint-funded")] GenesisNotMintFunded, + /// 12 (0xC) - Genesis account authority does not match the agent wallet + #[error("Genesis account authority does not match the agent wallet")] + GenesisAuthorityMismatch, } impl From for ProgramError { @@ -71,6 +74,7 @@ impl TryFrom for MplAgentIdentityError { 9 => Ok(MplAgentIdentityError::AgentIdentityAlreadyRegistered), 10 => Ok(MplAgentIdentityError::InvalidGenesisAccount), 11 => Ok(MplAgentIdentityError::GenesisNotMintFunded), + 12 => Ok(MplAgentIdentityError::GenesisAuthorityMismatch), _ => Err(ProgramError::InvalidArgument), } } @@ -95,6 +99,9 @@ impl ToStr for MplAgentIdentityError { } MplAgentIdentityError::InvalidGenesisAccount => "Invalid Genesis Account", MplAgentIdentityError::GenesisNotMintFunded => "Genesis account is not mint-funded", + MplAgentIdentityError::GenesisAuthorityMismatch => { + "Genesis account authority does not match the agent wallet" + } } } } diff --git a/clients/rust-identity/tests/set_agent_token.rs b/clients/rust-identity/tests/set_agent_token.rs index 2d15493..850d4f7 100644 --- a/clients/rust-identity/tests/set_agent_token.rs +++ b/clients/rust-identity/tests/set_agent_token.rs @@ -60,8 +60,14 @@ async fn set_agent_token() { let (collection, asset) = setup::create_collection_and_asset(&mut context).await; let agent_identity_pda = setup::register_identity(&mut context, asset, collection).await; + let (asset_signer_pda, _) = Pubkey::find_program_address( + &["mpl-core-execute".as_bytes(), asset.as_ref()], + &setup::MPL_CORE_ID, + ); + let base_mint = Keypair::new().pubkey(); - let genesis_account = create_genesis_account(&mut context, base_mint, 0, 0).await; + let genesis_account = + create_genesis_account(&mut context, base_mint, 0, 0, asset_signer_pda).await; let ix = build_set_agent_token_via_execute( asset, @@ -101,7 +107,8 @@ async fn cannot_set_agent_token_without_asset_signer() { let agent_identity_pda = setup::register_identity(&mut context, asset, collection).await; let base_mint = Keypair::new().pubkey(); - let genesis_account = create_genesis_account(&mut context, base_mint, 0, 0).await; + let payer_key = context.payer.pubkey(); + let genesis_account = create_genesis_account(&mut context, base_mint, 0, 0, payer_key).await; // Call SetAgentTokenV1 directly (not via Execute), so payer is the authority. let ix = SetAgentTokenV1Builder::new() @@ -136,8 +143,14 @@ async fn cannot_set_agent_token_twice() { let (collection, asset) = setup::create_collection_and_asset(&mut context).await; let agent_identity_pda = setup::register_identity(&mut context, asset, collection).await; + let (asset_signer_pda, _) = Pubkey::find_program_address( + &["mpl-core-execute".as_bytes(), asset.as_ref()], + &setup::MPL_CORE_ID, + ); + let base_mint = Keypair::new().pubkey(); - let genesis_account = create_genesis_account(&mut context, base_mint, 0, 0).await; + let genesis_account = + create_genesis_account(&mut context, base_mint, 0, 0, asset_signer_pda).await; // First set succeeds. let ix = build_set_agent_token_via_execute( @@ -158,7 +171,8 @@ async fn cannot_set_agent_token_twice() { // Second set with a different genesis account should fail. let base_mint_2 = Keypair::new().pubkey(); - let genesis_account_2 = create_genesis_account(&mut context, base_mint_2, 0, 0).await; + let genesis_account_2 = + create_genesis_account(&mut context, base_mint_2, 0, 0, asset_signer_pda).await; let ix = build_set_agent_token_via_execute( asset, @@ -229,7 +243,8 @@ async fn cannot_set_agent_token_with_transfer_funded_genesis() { // Create a genesis account with funding_mode = Transfer (1). let base_mint = Keypair::new().pubkey(); - let genesis_account = create_genesis_account(&mut context, base_mint, 0, 1).await; + let payer_key = context.payer.pubkey(); + let genesis_account = create_genesis_account(&mut context, base_mint, 0, 1, payer_key).await; let ix = build_set_agent_token_via_execute( asset, @@ -254,6 +269,42 @@ async fn cannot_set_agent_token_with_transfer_funded_genesis() { setup::assert_custom_error(err, MplAgentIdentityError::GenesisNotMintFunded as u32); } +#[tokio::test] +async fn cannot_set_agent_token_with_wrong_genesis_authority() { + let mut context = setup::setup().start_with_context().await; + + let (collection, asset) = setup::create_collection_and_asset(&mut context).await; + let agent_identity_pda = setup::register_identity(&mut context, asset, collection).await; + + // Create a genesis account whose authority is NOT the asset signer PDA. + let base_mint = Keypair::new().pubkey(); + let wrong_authority = Keypair::new().pubkey(); + let genesis_account = + create_genesis_account(&mut context, base_mint, 0, 0, wrong_authority).await; + + let ix = build_set_agent_token_via_execute( + asset, + collection, + agent_identity_pda, + genesis_account, + context.payer.pubkey(), + ); + + let tx = Transaction::new_signed_with_payer( + &[ix], + Some(&context.payer.pubkey()), + &[&context.payer], + context.last_blockhash, + ); + let err = context + .banks_client + .process_transaction(tx) + .await + .unwrap_err(); + + setup::assert_custom_error(err, MplAgentIdentityError::GenesisAuthorityMismatch as u32); +} + #[tokio::test] async fn cannot_set_agent_token_with_invalid_genesis_discriminator() { let mut context = setup::setup().start_with_context().await; @@ -305,7 +356,8 @@ async fn cannot_set_agent_token_on_unregistered_identity() { // Don't register identity — PDA won't be initialized. let (agent_identity_pda, _) = AgentIdentityV2::find_pda(&asset); let base_mint = Keypair::new().pubkey(); - let genesis_account = create_genesis_account(&mut context, base_mint, 0, 0).await; + let payer_key = context.payer.pubkey(); + let genesis_account = create_genesis_account(&mut context, base_mint, 0, 0, payer_key).await; let ix = build_set_agent_token_via_execute( asset, @@ -383,8 +435,14 @@ async fn set_agent_token_migrates_v1_to_v2() { assert_eq!(account.data[0], 1); // Key::AgentIdentityV1 // Set agent token via Execute CPI — this should migrate V1 -> V2. + let (asset_signer_pda, _) = Pubkey::find_program_address( + &["mpl-core-execute".as_bytes(), asset.as_ref()], + &setup::MPL_CORE_ID, + ); + let base_mint = Keypair::new().pubkey(); - let genesis_account = create_genesis_account(&mut context, base_mint, 0, 0).await; + let genesis_account = + create_genesis_account(&mut context, base_mint, 0, 0, asset_signer_pda).await; let ix = build_set_agent_token_via_execute( asset, @@ -427,9 +485,15 @@ async fn set_agent_token_on_v1_cannot_set_twice() { // Downgrade to V1. downgrade_to_v1(&mut context, agent_identity_pda, asset).await; + let (asset_signer_pda, _) = Pubkey::find_program_address( + &["mpl-core-execute".as_bytes(), asset.as_ref()], + &setup::MPL_CORE_ID, + ); + // First set migrates V1 -> V2 and sets the token. let base_mint = Keypair::new().pubkey(); - let genesis_account = create_genesis_account(&mut context, base_mint, 0, 0).await; + let genesis_account = + create_genesis_account(&mut context, base_mint, 0, 0, asset_signer_pda).await; let ix = build_set_agent_token_via_execute( asset, @@ -449,7 +513,8 @@ async fn set_agent_token_on_v1_cannot_set_twice() { // Second set should fail (now it's V2 with token already set). let base_mint_2 = Keypair::new().pubkey(); - let genesis_account_2 = create_genesis_account(&mut context, base_mint_2, 0, 0).await; + let genesis_account_2 = + create_genesis_account(&mut context, base_mint_2, 0, 0, asset_signer_pda).await; let ix = build_set_agent_token_via_execute( asset, diff --git a/clients/rust-identity/tests/setup/mod.rs b/clients/rust-identity/tests/setup/mod.rs index c2002fa..a96ab13 100644 --- a/clients/rust-identity/tests/setup/mod.rs +++ b/clients/rust-identity/tests/setup/mod.rs @@ -118,6 +118,7 @@ pub async fn create_genesis_account( base_mint: Pubkey, finalized: u8, funding_mode: u8, + authority: Pubkey, ) -> Pubkey { let genesis_keypair = Keypair::new(); let genesis_address = genesis_keypair.pubkey(); @@ -127,8 +128,8 @@ pub async fn create_genesis_account( data[0] = 18; // finalized at offset 4 data[4] = finalized; - // authority at offset 8 (use payer as authority) - data[8..40].copy_from_slice(context.payer.pubkey().as_ref()); + // authority at offset 8 + data[8..40].copy_from_slice(authority.as_ref()); // base_mint at offset 40 data[40..72].copy_from_slice(base_mint.as_ref()); // funding_mode at offset 128 diff --git a/idls/mpl_agent_identity.json b/idls/mpl_agent_identity.json index 45c0d73..95f0edc 100644 --- a/idls/mpl_agent_identity.json +++ b/idls/mpl_agent_identity.json @@ -400,6 +400,11 @@ "code": 11, "name": "GenesisNotMintFunded", "msg": "Genesis account is not mint-funded" + }, + { + "code": 12, + "name": "GenesisAuthorityMismatch", + "msg": "Genesis account authority does not match the agent wallet" } ], "metadata": { diff --git a/programs/mpl-agent-identity/src/error.rs b/programs/mpl-agent-identity/src/error.rs index d486604..6cfcde7 100644 --- a/programs/mpl-agent-identity/src/error.rs +++ b/programs/mpl-agent-identity/src/error.rs @@ -55,6 +55,10 @@ pub enum MplAgentIdentityError { /// 11 - Genesis account is not mint-funded #[error("Genesis account is not mint-funded")] GenesisNotMintFunded, + + /// 12 - Genesis account authority does not match the agent wallet + #[error("Genesis account authority does not match the agent wallet")] + GenesisAuthorityMismatch, } impl PrintProgramError for MplAgentIdentityError { diff --git a/programs/mpl-agent-identity/src/processor/set_agent_token_v1.rs b/programs/mpl-agent-identity/src/processor/set_agent_token_v1.rs index 7013b4f..a4fdf5e 100644 --- a/programs/mpl-agent-identity/src/processor/set_agent_token_v1.rs +++ b/programs/mpl-agent-identity/src/processor/set_agent_token_v1.rs @@ -28,6 +28,9 @@ const GENESIS_FUNDING_MODE_OFFSET: usize = 128; /// Minimum size of a GenesisAccountV2 account. const GENESIS_MIN_SIZE: usize = 136; +/// Offset of `authority` (Pubkey) in GenesisAccountV2. +const GENESIS_AUTHORITY_OFFSET: usize = 8; + /// FundingMode::Mint variant value. const FUNDING_MODE_MINT: u8 = 0; @@ -133,6 +136,20 @@ pub fn set_agent_token_v1<'a>( return Err(MplAgentIdentityError::OnlyAssetSignerCanSetAgentToken.into()); } + // Assert that the genesis account authority matches the agent's wallet (asset signer PDA). + { + let genesis_data = ctx.accounts.genesis_account.try_borrow_data()?; + let genesis_authority = Pubkey::from( + <[u8; 32]>::try_from( + &genesis_data[GENESIS_AUTHORITY_OFFSET..GENESIS_AUTHORITY_OFFSET + 32], + ) + .unwrap(), + ); + if genesis_authority != asset_signer_pda.0 { + return Err(MplAgentIdentityError::GenesisAuthorityMismatch.into()); + } + } + /****************************************************/ /***************** Argument Guards ******************/ /****************************************************/