From bc87c186ec94d70a119a0bce875557974774c0aa Mon Sep 17 00:00:00 2001 From: febo Date: Sun, 18 Jan 2026 15:16:53 +0000 Subject: [PATCH 1/2] Add self-revoke delegate --- pinocchio/interface/src/instruction.rs | 6 +++--- pinocchio/program/src/processor/revoke.rs | 22 ++++++++++++++++++---- 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/pinocchio/interface/src/instruction.rs b/pinocchio/interface/src/instruction.rs index 80b44c15..8ef3dbd8 100644 --- a/pinocchio/interface/src/instruction.rs +++ b/pinocchio/interface/src/instruction.rs @@ -124,11 +124,11 @@ pub enum TokenInstruction { /// /// * Single owner /// 0. `[writable]` The source account. - /// 1. `[signer]` The source account owner. + /// 1. `[signer]` The source account's owner/delegate. /// - /// * Multisignature owner + /// * Multisignature owner/delegate /// 0. `[writable]` The source account. - /// 1. `[]` The source account's multisignature owner. + /// 1. `[]` The source account's multisignature owner/delegate. /// 2. `..+M` `[signer]` M signer accounts. Revoke, diff --git a/pinocchio/program/src/processor/revoke.rs b/pinocchio/program/src/processor/revoke.rs index 20ddc648..4b85c958 100644 --- a/pinocchio/program/src/processor/revoke.rs +++ b/pinocchio/program/src/processor/revoke.rs @@ -18,9 +18,9 @@ pub fn process_revoke(accounts: &[AccountInfo]) -> ProgramResult { let source_account = unsafe { load_mut::(source_account_info.borrow_mut_data_unchecked())? }; - // Unpacking the remaining accounts to get the owner account at this point + // Unpacking the remaining accounts to get the authority account at this point // to maintain the same order as SPL Token. - let [owner_info, remaining @ ..] = remaining else { + let [authority_info, remaining @ ..] = remaining else { return Err(ProgramError::NotEnoughAccountKeys); }; @@ -28,8 +28,22 @@ pub fn process_revoke(accounts: &[AccountInfo]) -> ProgramResult { return Err(TokenError::AccountFrozen.into()); } - // SAFETY: `owner_info` is not currently borrowed. - unsafe { validate_owner(&source_account.owner, owner_info, remaining)? } + // Validates the owner or delegate. + + // SAFETY: `authority_info` is not currently borrowed; in the case + // `authority_info` is the same as `source_account_info`, then it cannot be + // a multisig. + unsafe { + validate_owner( + if source_account.delegate() == Some(authority_info.key()) { + authority_info.key() + } else { + &source_account.owner + }, + authority_info, + remaining, + )? + }; source_account.clear_delegate(); source_account.set_delegated_amount(0); From a997ffd3f5876b373785f75baf635dab7a82c68a Mon Sep 17 00:00:00 2001 From: febo Date: Sun, 18 Jan 2026 15:18:52 +0000 Subject: [PATCH 2/2] Add test --- pinocchio/program/tests/revoke.rs | 83 +++++++++++++++++++++++++++++++ 1 file changed, 83 insertions(+) diff --git a/pinocchio/program/tests/revoke.rs b/pinocchio/program/tests/revoke.rs index 14ed9566..8c14c84c 100644 --- a/pinocchio/program/tests/revoke.rs +++ b/pinocchio/program/tests/revoke.rs @@ -92,3 +92,86 @@ async fn revoke() { assert!(account.delegate.is_none()); assert!(account.delegated_amount == 0); } + +#[tokio::test] +async fn revoke_with_delegate() { + let mut context = ProgramTest::new("pinocchio_token_program", TOKEN_PROGRAM_ID, None) + .start_with_context() + .await; + + // Given a mint account. + + let mint_authority = Keypair::new(); + let freeze_authority = Pubkey::new_unique(); + + let mint = mint::initialize( + &mut context, + mint_authority.pubkey(), + Some(freeze_authority), + &TOKEN_PROGRAM_ID, + ) + .await + .unwrap(); + + // And a token account with 100 tokens. + + let owner = Keypair::new(); + + let account = + account::initialize(&mut context, &mint, &owner.pubkey(), &TOKEN_PROGRAM_ID).await; + + mint::mint( + &mut context, + &mint, + &account, + &mint_authority, + 100, + &TOKEN_PROGRAM_ID, + ) + .await + .unwrap(); + + // And 50 tokens delegated. + + let delegate = Keypair::new(); + + account::approve( + &mut context, + &account, + &delegate.pubkey(), + &owner, + 50, + &TOKEN_PROGRAM_ID, + ) + .await; + + // When we revoke the delegation with the delegate as authority. + + let revoke_ix = spl_token_interface::instruction::revoke( + &spl_token_interface::ID, + &account, + &delegate.pubkey(), + &[], + ) + .unwrap(); + + let tx = Transaction::new_signed_with_payer( + &[revoke_ix], + Some(&context.payer.pubkey()), + &[&context.payer, &delegate], + context.last_blockhash, + ); + context.banks_client.process_transaction(tx).await.unwrap(); + + // Then the account should not have a delegate nor delegated amount. + + let account = context.banks_client.get_account(account).await.unwrap(); + + assert!(account.is_some()); + + let account = account.unwrap(); + let account = spl_token_interface::state::Account::unpack(&account.data).unwrap(); + + assert!(account.delegate.is_none()); + assert!(account.delegated_amount == 0); +}