Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions clients/js/src/generated/identity/errors/mplAgentIdentity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
75 changes: 64 additions & 11 deletions clients/js/test/identity/setAgentToken.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<ReturnType<typeof createUmi>>,
fundingMode: number
Expand All @@ -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<ReturnType<typeof createUmi>>,
asset: ReturnType<typeof publicKey>,
collection: ReturnType<typeof publicKey>,
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);
Expand All @@ -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 });
Expand Down Expand Up @@ -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 });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MplAgentIdentityError> for ProgramError {
Expand All @@ -71,6 +74,7 @@ impl TryFrom<u32> for MplAgentIdentityError {
9 => Ok(MplAgentIdentityError::AgentIdentityAlreadyRegistered),
10 => Ok(MplAgentIdentityError::InvalidGenesisAccount),
11 => Ok(MplAgentIdentityError::GenesisNotMintFunded),
12 => Ok(MplAgentIdentityError::GenesisAuthorityMismatch),
_ => Err(ProgramError::InvalidArgument),
}
}
Expand All @@ -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"
}
}
}
}
83 changes: 74 additions & 9 deletions clients/rust-identity/tests/set_agent_token.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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(
Expand All @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down
5 changes: 3 additions & 2 deletions clients/rust-identity/tests/setup/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions idls/mpl_agent_identity.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
4 changes: 4 additions & 0 deletions programs/mpl-agent-identity/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
17 changes: 17 additions & 0 deletions programs/mpl-agent-identity/src/processor/set_agent_token_v1.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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 ******************/
/****************************************************/
Expand Down
Loading