-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Bug
register_agent fails with Cross-program invocation with unauthorized signer or writable account when both conditions are true:
owner != payer(minting an agent NFT on behalf of a different wallet)additionalMetadatais provided (non-null, non-empty)
The default SDK path (owner defaults to payer) is unaffected.
Root cause
In step 2g of register_agent.rs (lines 282-296), the update_field CPI uses owner as the metadata update authority:
let update_field_ix = spl_token_metadata_interface::instruction::update_field(
&anchor_spl::token_2022::ID,
&ctx.accounts.agent_mint.key(),
&ctx.accounts.owner.key(), // update_authority
...
);
anchor_lang::solana_program::program::invoke(
&update_field_ix,
&[
ctx.accounts.agent_mint.to_account_info(),
ctx.accounts.owner.to_account_info(),
],
)?;The SPL update_field instruction marks update_authority as AccountMeta::new_readonly(*update_authority, true) - a required signer. But owner is defined as UncheckedAccount (line 30), not Signer, so its AccountInfo has is_signer = false. The runtime detects signer privilege escalation and rejects the CPI before it dispatches.
Note: the ATA creation (step 2i) is NOT the cause - create_associated_token_account does not require the wallet/owner as a signer.
Reproduction
The simulation logs show:
'Program log: TokenMetadataInstruction: Initialize', // step 2f succeeds
'Program TokenzQd... success',
"Fo2EYEYbnJTNnBnbAgnjnG1c2fixpFn1vSUUHSeoHhRP's signer privilege escalated", // step 2g fails immediately
'Program satiRkxEiwZ51cv8PRu8UMzuaqeaNU9jABo6oAFMsLe failed: Cross-program invocation with unauthorized signer or writable account'
No intermediate CPI invocation log between TokenMetadata Initialize and the escalation error - the runtime rejects update_field before dispatching.
Impact
- Criticality: Low. Only affects
owner != payer+additionalMetadatacombo. No data corruption, fund loss, or security issue. - Default SDK usage is fine -
ownerdefaults topayer.address, so the signer requirement is satisfied implicitly. - Blocks use case: a hot wallet (payer) minting an agent NFT for a different owner with metadata fields.
Fix options
Option A: Payer as temporary update authority (recommended)
Use payer as the initial metadata update authority during registration, then transfer authority to owner at the end. No SDK breaking change - owner stays Address.
initialize_metadata(line 252): change&ctx.accounts.owner.key()to&ctx.accounts.payer.key()update_fieldcalls (line 285): change&ctx.accounts.owner.key()to&ctx.accounts.payer.key()- After all metadata writes, add a CPI to transfer update authority from payer to owner:
let transfer_authority_ix = spl_token_metadata_interface::instruction::update_authority(
&anchor_spl::token_2022::ID,
&ctx.accounts.agent_mint.key(),
&ctx.accounts.payer.key(), // current authority (signer)
ctx.accounts.owner.key(), // new authority
);
anchor_lang::solana_program::program::invoke(
&transfer_authority_ix,
&[
ctx.accounts.agent_mint.to_account_info(),
ctx.accounts.payer.to_account_info(),
],
)?;Pros: No SDK-level changes, owner never needs to sign, owner ends up as update authority post-registration.
Cons: Verify the update_authority CPI accounts are correct (same runtime-panic risk as SAS CPIs).
Option B: Make owner a Signer
Change owner from UncheckedAccount to Signer in the accounts struct.
Pros: Simplest code change.
Cons: Breaking SDK change (owner becomes TransactionSigner instead of Address). Blocks the hot-wallet-mints-for-user pattern entirely since the owner's private key would be required.
Option C: Skip additionalMetadata during registration
Remove the update_field loop from register_agent. Metadata fields would be set post-registration via updateAgentMetadata (which already requires owner to sign).
Pros: Simplest change, removes the problematic code path entirely.
Cons: Changes the API contract (registration no longer sets metadata atomically). Requires two transactions for the full flow.