Revert "token-2022: implement CpiGuard (#3712)"

This reverts commit 126fb93be2.
This commit is contained in:
Trent Nelson 2022-10-25 00:46:00 -06:00
parent 126fb93be2
commit 2c28eaa036
No known key found for this signature in database
GPG Key ID: 9A4ACDAE36292B58
13 changed files with 30 additions and 1208 deletions

1
Cargo.lock generated
View File

@ -6196,7 +6196,6 @@ dependencies = [
"solana-program-test", "solana-program-test",
"solana-sdk", "solana-sdk",
"spl-associated-token-account 1.1.1", "spl-associated-token-account 1.1.1",
"spl-instruction-padding",
"spl-memo 3.0.1", "spl-memo 3.0.1",
"spl-token-2022 0.4.3", "spl-token-2022 0.4.3",
"spl-token-client", "spl-token-client",

View File

@ -20,8 +20,8 @@ use {
}, },
spl_token_2022::{ spl_token_2022::{
extension::{ extension::{
confidential_transfer, cpi_guard, default_account_state, interest_bearing_mint, confidential_transfer, default_account_state, interest_bearing_mint, memo_transfer,
memo_transfer, transfer_fee, ExtensionType, StateWithExtensionsOwned, transfer_fee, ExtensionType, StateWithExtensionsOwned,
}, },
instruction, instruction,
solana_zk_token_sdk::{ solana_zk_token_sdk::{
@ -1388,50 +1388,6 @@ where
.await .await
} }
/// Prevent unsafe usage of token account through CPI
pub async fn enable_cpi_guard<S: Signers>(
&self,
account: &Pubkey,
authority: &Pubkey,
signing_keypairs: &S,
) -> TokenResult<T::Output> {
let signing_pubkeys = signing_keypairs.pubkeys();
let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys);
self.process_ixs(
&[cpi_guard::instruction::enable_cpi_guard(
&self.program_id,
account,
authority,
&multisig_signers,
)?],
signing_keypairs,
)
.await
}
/// Stop preventing unsafe usage of token account through CPI
pub async fn disable_cpi_guard<S: Signers>(
&self,
account: &Pubkey,
authority: &Pubkey,
signing_keypairs: &S,
) -> TokenResult<T::Output> {
let signing_pubkeys = signing_keypairs.pubkeys();
let multisig_signers = self.get_multisig_signers(authority, &signing_pubkeys);
self.process_ixs(
&[cpi_guard::instruction::disable_cpi_guard(
&self.program_id,
account,
authority,
&multisig_signers,
)?],
signing_keypairs,
)
.await
}
/// Update interest rate /// Update interest rate
pub async fn update_interest_rate<S: Signers>( pub async fn update_interest_rate<S: Signers>(
&self, &self,

View File

@ -21,5 +21,4 @@ solana-sdk = "=1.14.4"
spl-associated-token-account = { version = "1.1", path = "../../associated-token-account/program" } spl-associated-token-account = { version = "1.1", path = "../../associated-token-account/program" }
spl-memo = { version = "3.0.1", path = "../../memo/program", features = ["no-entrypoint"] } spl-memo = { version = "3.0.1", path = "../../memo/program", features = ["no-entrypoint"] }
spl-token-2022 = { version = "0.4", path="../program-2022", features = ["no-entrypoint"] } spl-token-2022 = { version = "0.4", path="../program-2022", features = ["no-entrypoint"] }
spl-instruction-padding = { version = "0.1.0", path="../../instruction-padding/program", features = ["no-entrypoint"] }
spl-token-client = { version = "0.2.1", path = "../client" } spl-token-client = { version = "0.2.1", path = "../client" }

View File

@ -31,37 +31,24 @@ fn rerun_if_changed(directory: &Path) {
fn main() { fn main() {
let cwd = env::current_dir().expect("Unable to get current working directory"); let cwd = env::current_dir().expect("Unable to get current working directory");
let spl_token_2022_dir = cwd let spl_token_2022_dir = cwd
.parent() .parent()
.expect("Unable to get parent directory of current working dir") .expect("Unable to get parent directory of current working dir")
.join("program-2022"); .join("program-2022");
rerun_if_changed(&spl_token_2022_dir); rerun_if_changed(&spl_token_2022_dir);
let instruction_padding_dir = cwd
.parent()
.expect("Unable to get parent directory of current working dir")
.parent()
.expect("Unable to get grandparent directory of current working dir")
.join("instruction-padding")
.join("program");
rerun_if_changed(&instruction_padding_dir);
println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-changed=build.rs");
for program_dir in [spl_token_2022_dir, instruction_padding_dir] { let spl_token_2022_toml = spl_token_2022_dir.join("Cargo.toml");
let program_toml = program_dir.join("Cargo.toml"); let spl_token_2022_toml = format!("{}", spl_token_2022_toml.display());
let program_toml = format!("{}", program_toml.display()); let args = vec!["build-sbf", "--manifest-path", &spl_token_2022_toml];
let args = vec!["build-sbf", "--manifest-path", &program_toml]; let output = Command::new("cargo")
let output = Command::new("cargo") .args(&args)
.args(&args) .output()
.output() .expect("Error running cargo build-sbf");
.expect("Error running cargo build-sbf"); if let Ok(output_str) = std::str::from_utf8(&output.stdout) {
if let Ok(output_str) = std::str::from_utf8(&output.stdout) { let subs = output_str.split('\n');
let subs = output_str.split('\n'); for sub in subs {
for sub in subs { println!("cargo:warning=(not a warning) {}", sub);
println!("cargo:warning=(not a warning) {}", sub);
}
} }
} }

View File

@ -1,767 +0,0 @@
#![cfg(feature = "test-sbf")]
mod program_test;
use {
program_test::{keypair_clone, TestContext, TokenContext},
solana_program_test::{
processor,
tokio::{self, sync::Mutex},
ProgramTest,
},
solana_sdk::{
instruction::InstructionError, pubkey::Pubkey, signature::Signer, signer::keypair::Keypair,
transaction::TransactionError, transport::TransportError,
},
spl_instruction_padding::instruction::wrap_instruction,
spl_token_2022::{
error::TokenError,
extension::{
cpi_guard::{self, CpiGuard},
ExtensionType,
},
instruction::{self, AuthorityType},
processor::Processor as SplToken2022Processor,
},
spl_token_client::{
client::ProgramBanksClientProcessTransaction,
token::{Token, TokenError as TokenClientError},
},
std::sync::Arc,
};
// set up a bank and bank client with spl token 2022 and the instruction padder
// also creates a token with no extensions and inits two token accounts
async fn make_context() -> TestContext {
// TODO this may be removed when we upgrade to a solana version with a fixed `get_stack_height()` stub
if std::env::var("BPF_OUT_DIR").is_err() && std::env::var("SBF_OUT_DIR").is_err() {
panic!("CpiGuard tests MUST be invoked with `cargo test-sbf`, NOT `cargo test --feature test-sbf`. \
In a non-BPF context, `get_stack_height()` always returns 0, and all tests WILL fail.");
}
let mut program_test = ProgramTest::new(
"spl_token_2022",
spl_token_2022::id(),
processor!(SplToken2022Processor::process),
);
program_test.add_program(
"spl_instruction_padding",
spl_instruction_padding::id(),
processor!(spl_instruction_padding::processor::process),
);
let program_context = program_test.start_with_context().await;
let program_context = Arc::new(Mutex::new(program_context));
let mut test_context = TestContext {
context: program_context,
token_context: None,
};
test_context.init_token_with_mint(vec![]).await.unwrap();
let token_context = test_context.token_context.as_ref().unwrap();
token_context
.token
.create_auxiliary_token_account_with_extension_space(
&token_context.alice,
&token_context.alice.pubkey(),
vec![ExtensionType::CpiGuard],
)
.await
.unwrap();
token_context
.token
.create_auxiliary_token_account(&token_context.bob, &token_context.bob.pubkey())
.await
.unwrap();
test_context
}
fn client_error(token_error: TokenError) -> TokenClientError {
TokenClientError::Client(Box::new(TransportError::TransactionError(
TransactionError::InstructionError(0, InstructionError::Custom(token_error as u32)),
)))
}
#[tokio::test]
async fn test_cpi_guard_enable_disable() {
let context = make_context().await;
let TokenContext {
token, alice, bob, ..
} = context.token_context.unwrap();
// enable guard properly
token
.enable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice])
.await
.unwrap();
// guard is enabled
let alice_state = token.get_account_info(&alice.pubkey()).await.unwrap();
let extension = alice_state.get_extension::<CpiGuard>().unwrap();
assert!(bool::from(extension.lock_cpi));
// attempt to disable through cpi. this fails
let error = token
.process_ixs(
&[wrap_instruction(
spl_instruction_padding::id(),
cpi_guard::instruction::disable_cpi_guard(
&spl_token_2022::id(),
&alice.pubkey(),
&alice.pubkey(),
&[],
)
.unwrap(),
vec![],
0,
)
.unwrap()],
&[&alice],
)
.await
.unwrap_err();
assert_eq!(error, client_error(TokenError::CpiGuardSettingsLocked));
// guard remains enabled
let alice_state = token.get_account_info(&alice.pubkey()).await.unwrap();
let extension = alice_state.get_extension::<CpiGuard>().unwrap();
assert!(bool::from(extension.lock_cpi));
// disable guard properly
token
.disable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice])
.await
.unwrap();
// guard is disabled
let alice_state = token.get_account_info(&alice.pubkey()).await.unwrap();
let extension = alice_state.get_extension::<CpiGuard>().unwrap();
assert!(!bool::from(extension.lock_cpi));
// attempt to enable through cpi. this fails
let error = token
.process_ixs(
&[wrap_instruction(
spl_instruction_padding::id(),
cpi_guard::instruction::enable_cpi_guard(
&spl_token_2022::id(),
&alice.pubkey(),
&alice.pubkey(),
&[],
)
.unwrap(),
vec![],
0,
)
.unwrap()],
&[&alice],
)
.await
.unwrap_err();
assert_eq!(error, client_error(TokenError::CpiGuardSettingsLocked));
// guard remains disabled
let alice_state = token.get_account_info(&alice.pubkey()).await.unwrap();
let extension = alice_state.get_extension::<CpiGuard>().unwrap();
assert!(!bool::from(extension.lock_cpi));
// enable works with realloc
token
.reallocate(
&bob.pubkey(),
&bob.pubkey(),
&[ExtensionType::CpiGuard],
&[&bob],
)
.await
.unwrap();
token
.enable_cpi_guard(&bob.pubkey(), &bob.pubkey(), &[&bob])
.await
.unwrap();
let bob_state = token.get_account_info(&bob.pubkey()).await.unwrap();
let extension = bob_state.get_extension::<CpiGuard>().unwrap();
assert!(bool::from(extension.lock_cpi));
}
#[tokio::test]
async fn test_cpi_guard_transfer() {
let context = make_context().await;
let TokenContext {
token,
token_unchecked,
mint_authority,
alice,
bob,
..
} = context.token_context.unwrap();
let mk_transfer = |authority, do_checked| {
wrap_instruction(
spl_instruction_padding::id(),
if do_checked {
instruction::transfer_checked(
&spl_token_2022::id(),
&alice.pubkey(),
token.get_address(),
&bob.pubkey(),
&authority,
&[],
1,
9,
)
.unwrap()
} else {
#[allow(deprecated)]
instruction::transfer(
&spl_token_2022::id(),
&alice.pubkey(),
&bob.pubkey(),
&authority,
&[],
1,
)
.unwrap()
},
vec![],
0,
)
.unwrap()
};
let mut amount = 100;
token
.mint_to(
&alice.pubkey(),
&mint_authority.pubkey(),
amount,
&[&mint_authority],
)
.await
.unwrap();
for do_checked in [true, false] {
let token_obj = if do_checked { &token } else { &token_unchecked };
token_obj
.enable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice])
.await
.unwrap();
// transfer works normally with cpi guard enabled
token_obj
.transfer(
&alice.pubkey(),
&bob.pubkey(),
&alice.pubkey(),
1,
&[&alice],
)
.await
.unwrap();
amount -= 1;
let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap();
assert_eq!(alice_state.base.amount, amount);
// user-auth cpi transfer with cpi guard doesnt work
let error = token_obj
.process_ixs(&[mk_transfer(alice.pubkey(), do_checked)], &[&alice])
.await
.unwrap_err();
assert_eq!(error, client_error(TokenError::CpiGuardTransferBlocked));
let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap();
assert_eq!(alice_state.base.amount, amount);
// delegate-auth cpi transfer with cpi guard works
token_obj
.approve(
&alice.pubkey(),
&bob.pubkey(),
&alice.pubkey(),
1,
&[&alice],
)
.await
.unwrap();
token_obj
.process_ixs(&[mk_transfer(bob.pubkey(), do_checked)], &[&bob])
.await
.unwrap();
amount -= 1;
let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap();
assert_eq!(alice_state.base.amount, amount);
// transfer still works through cpi with cpi guard off
token_obj
.disable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice])
.await
.unwrap();
token_obj
.process_ixs(&[mk_transfer(alice.pubkey(), do_checked)], &[&alice])
.await
.unwrap();
amount -= 1;
let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap();
assert_eq!(alice_state.base.amount, amount);
}
}
#[tokio::test]
async fn test_cpi_guard_burn() {
let context = make_context().await;
let TokenContext {
token,
token_unchecked,
mint_authority,
alice,
bob,
..
} = context.token_context.unwrap();
let mk_burn = |authority, do_checked| {
wrap_instruction(
spl_instruction_padding::id(),
if do_checked {
instruction::burn_checked(
&spl_token_2022::id(),
&alice.pubkey(),
token.get_address(),
&authority,
&[],
1,
9,
)
.unwrap()
} else {
instruction::burn(
&spl_token_2022::id(),
&alice.pubkey(),
token.get_address(),
&authority,
&[],
1,
)
.unwrap()
},
vec![],
0,
)
.unwrap()
};
let mut amount = 100;
token
.mint_to(
&alice.pubkey(),
&mint_authority.pubkey(),
amount,
&[&mint_authority],
)
.await
.unwrap();
for do_checked in [true, false] {
let token_obj = if do_checked { &token } else { &token_unchecked };
token_obj
.enable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice])
.await
.unwrap();
// burn works normally with cpi guard enabled
token_obj
.burn(&alice.pubkey(), &alice.pubkey(), 1, &[&alice])
.await
.unwrap();
amount -= 1;
let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap();
assert_eq!(alice_state.base.amount, amount);
// user-auth cpi burn with cpi guard doesnt work
let error = token_obj
.process_ixs(&[mk_burn(alice.pubkey(), do_checked)], &[&alice])
.await
.unwrap_err();
assert_eq!(error, client_error(TokenError::CpiGuardBurnBlocked));
let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap();
assert_eq!(alice_state.base.amount, amount);
// delegate-auth cpi burn with cpi guard works
token_obj
.approve(
&alice.pubkey(),
&bob.pubkey(),
&alice.pubkey(),
1,
&[&alice],
)
.await
.unwrap();
token_obj
.process_ixs(&[mk_burn(bob.pubkey(), do_checked)], &[&bob])
.await
.unwrap();
amount -= 1;
let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap();
assert_eq!(alice_state.base.amount, amount);
// burn still works through cpi with cpi guard off
token_obj
.disable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice])
.await
.unwrap();
token_obj
.process_ixs(&[mk_burn(alice.pubkey(), do_checked)], &[&alice])
.await
.unwrap();
amount -= 1;
let alice_state = token_obj.get_account_info(&alice.pubkey()).await.unwrap();
assert_eq!(alice_state.base.amount, amount);
}
}
#[tokio::test]
async fn test_cpi_guard_approve() {
let context = make_context().await;
let TokenContext {
token,
token_unchecked,
alice,
bob,
..
} = context.token_context.unwrap();
let mk_approve = |do_checked| {
wrap_instruction(
spl_instruction_padding::id(),
if do_checked {
instruction::approve_checked(
&spl_token_2022::id(),
&alice.pubkey(),
token.get_address(),
&bob.pubkey(),
&alice.pubkey(),
&[],
1,
9,
)
.unwrap()
} else {
instruction::approve(
&spl_token_2022::id(),
&alice.pubkey(),
&bob.pubkey(),
&alice.pubkey(),
&[],
1,
)
.unwrap()
},
vec![],
0,
)
.unwrap()
};
for do_checked in [true, false] {
let token_obj = if do_checked { &token } else { &token_unchecked };
token_obj
.enable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice])
.await
.unwrap();
// approve works normally with cpi guard enabled
token_obj
.approve(
&alice.pubkey(),
&bob.pubkey(),
&alice.pubkey(),
1,
&[&alice],
)
.await
.unwrap();
token_obj
.revoke(&alice.pubkey(), &alice.pubkey(), &[&alice])
.await
.unwrap();
// approve doesnt work through cpi
let error = token_obj
.process_ixs(&[mk_approve(do_checked)], &[&alice])
.await
.unwrap_err();
assert_eq!(error, client_error(TokenError::CpiGuardApproveBlocked));
// approve still works through cpi with cpi guard off
token_obj
.disable_cpi_guard(&alice.pubkey(), &alice.pubkey(), &[&alice])
.await
.unwrap();
token_obj
.process_ixs(&[mk_approve(do_checked)], &[&alice])
.await
.unwrap();
token_obj
.revoke(&alice.pubkey(), &alice.pubkey(), &[&alice])
.await
.unwrap();
}
}
async fn make_close_test_account<S: Signer>(
token: &Token<ProgramBanksClientProcessTransaction>,
owner: &S,
authority: Option<Pubkey>,
) -> Pubkey {
let account = Keypair::new();
token
.create_auxiliary_token_account_with_extension_space(
&account,
&owner.pubkey(),
vec![ExtensionType::CpiGuard],
)
.await
.unwrap();
if authority.is_some() {
token
.set_authority(
&account.pubkey(),
&owner.pubkey(),
authority.as_ref(),
AuthorityType::CloseAccount,
&[owner],
)
.await
.unwrap();
}
token
.enable_cpi_guard(&account.pubkey(), &owner.pubkey(), &[owner])
.await
.unwrap();
account.pubkey()
}
#[tokio::test]
async fn test_cpi_guard_close_account() {
let context = make_context().await;
let TokenContext {
token, alice, bob, ..
} = context.token_context.unwrap();
let mk_close = |account, destination, authority| {
wrap_instruction(
spl_instruction_padding::id(),
instruction::close_account(
&spl_token_2022::id(),
&account,
&destination,
&authority,
&[],
)
.unwrap(),
vec![],
0,
)
.unwrap()
};
// test closing through owner and closing through close authority
// the result should be the same eitehr way
for maybe_close_authority in [None, Some(bob.pubkey())] {
let authority = if maybe_close_authority.is_none() {
&alice
} else {
&bob
};
// closing normally works
let account = make_close_test_account(&token, &alice, maybe_close_authority).await;
token
.close_account(&account, &bob.pubkey(), &authority.pubkey(), &[authority])
.await
.unwrap();
// cpi close with guard enabled fails if lamports diverted to third party
let account = make_close_test_account(&token, &alice, maybe_close_authority).await;
let error = token
.process_ixs(
&[mk_close(account, bob.pubkey(), authority.pubkey())],
&[authority],
)
.await
.unwrap_err();
assert_eq!(error, client_error(TokenError::CpiGuardCloseAccountBlocked));
// but close suceeds if lamports are returned to owner
token
.process_ixs(
&[mk_close(account, alice.pubkey(), authority.pubkey())],
&[authority],
)
.await
.unwrap();
// close still works through cpi when guard disabled
let account = make_close_test_account(&token, &alice, maybe_close_authority).await;
token
.disable_cpi_guard(&account, &alice.pubkey(), &[&alice])
.await
.unwrap();
token
.process_ixs(
&[mk_close(account, bob.pubkey(), authority.pubkey())],
&[authority],
)
.await
.unwrap();
}
}
#[derive(Copy, Clone, Debug, PartialEq)]
enum SetAuthTest {
ChangeOwner,
AddCloseAuth,
ChangeCloseAuth,
RemoveCloseAuth,
}
#[tokio::test]
async fn test_cpi_guard_set_authority() {
let context = make_context().await;
let TokenContext {
token, alice, bob, ..
} = context.token_context.unwrap();
// the behavior of cpi guard and close authority is so complicated that its best to test all cases exhaustively
let mut states = vec![];
for action in [
SetAuthTest::ChangeOwner,
SetAuthTest::AddCloseAuth,
SetAuthTest::ChangeCloseAuth,
SetAuthTest::RemoveCloseAuth,
] {
for enable_cpi_guard in [true, false] {
for do_in_cpi in [true, false] {
states.push((action, enable_cpi_guard, do_in_cpi));
}
}
}
for state in states {
let (action, enable_cpi_guard, do_in_cpi) = state;
// make a new account
let account = Keypair::new();
token
.create_auxiliary_token_account_with_extension_space(
&account,
&alice.pubkey(),
vec![ExtensionType::CpiGuard],
)
.await
.unwrap();
// turn on cpi guard if we are testing that case
// all actions with cpi guard off should succeed unconditionally
// so half of these tests are backwards compat checks
if enable_cpi_guard {
token
.enable_cpi_guard(&account.pubkey(), &alice.pubkey(), &[&alice])
.await
.unwrap();
}
// if we are changing or removing close auth, we need to have one to change/remove
if action == SetAuthTest::ChangeCloseAuth || action == SetAuthTest::RemoveCloseAuth {
token
.set_authority(
&account.pubkey(),
&alice.pubkey(),
Some(&bob.pubkey()),
AuthorityType::CloseAccount,
&[&alice],
)
.await
.unwrap();
}
// this produces the token instruction we want to execute
let (current_authority, new_authority) = match action {
SetAuthTest::ChangeOwner | SetAuthTest::AddCloseAuth => {
(keypair_clone(&alice), Some(bob.pubkey()))
}
SetAuthTest::ChangeCloseAuth => (keypair_clone(&bob), Some(alice.pubkey())),
SetAuthTest::RemoveCloseAuth => (keypair_clone(&bob), None),
};
let token_instruction = instruction::set_authority(
&spl_token_2022::id(),
&account.pubkey(),
new_authority.as_ref(),
if action == SetAuthTest::ChangeOwner {
AuthorityType::AccountOwner
} else {
AuthorityType::CloseAccount
},
&current_authority.pubkey(),
&[],
)
.unwrap();
// this wraps it or doesnt based on the test case
let instruction = if do_in_cpi {
wrap_instruction(spl_instruction_padding::id(), token_instruction, vec![], 0).unwrap()
} else {
token_instruction
};
// and here we go
let result = token
.process_ixs(&[instruction], &[&current_authority])
.await;
// truth table for our cases
match (action, enable_cpi_guard, do_in_cpi) {
// all actions succeed with cpi guard off
(_, false, _) => result.unwrap(),
// ownership cannot be transferred with guard
(SetAuthTest::ChangeOwner, true, false) => assert_eq!(
result.unwrap_err(),
client_error(TokenError::CpiGuardOwnerChangeBlocked)
),
// all other actions succeed outside cpi with guard
(_, true, false) => result.unwrap(),
// removing a close authority succeeds in cpi with guard
(SetAuthTest::RemoveCloseAuth, true, true) => result.unwrap(),
// changing owner, adding close, or changing close all fail in cpi with guard
(_, true, true) => assert_eq!(
result.unwrap_err(),
client_error(TokenError::CpiGuardSetAuthorityBlocked)
),
}
}
}

View File

@ -139,6 +139,7 @@ pub enum TokenError {
/// mint and try again /// mint and try again
#[error("An account can only be closed if its withheld fee balance is zero, harvest fees to the mint and try again")] #[error("An account can only be closed if its withheld fee balance is zero, harvest fees to the mint and try again")]
AccountHasWithheldTransferFees, AccountHasWithheldTransferFees,
/// No memo in previous instruction; required for recipient to receive a transfer /// No memo in previous instruction; required for recipient to receive a transfer
#[error("No memo in previous instruction; required for recipient to receive a transfer")] #[error("No memo in previous instruction; required for recipient to receive a transfer")]
NoMemo, NoMemo,
@ -155,38 +156,9 @@ pub enum TokenError {
the associated `maximum_pending_balance_credit_counter`" the associated `maximum_pending_balance_credit_counter`"
)] )]
MaximumPendingBalanceCreditCounterExceeded, MaximumPendingBalanceCreditCounterExceeded,
// 40
/// The deposit amount for the confidential extension exceeds the maximum limit /// The deposit amount for the confidential extension exceeds the maximum limit
#[error("Deposit amount exceeds maximum limit")] #[error("Deposit amount exceeds maximum limit")]
MaximumDepositAmountExceeded, MaximumDepositAmountExceeded,
/// CPI Guard cannot be enabled or disabled in CPI
#[error("CPI Guard cannot be enabled or disabled in CPI")]
CpiGuardSettingsLocked,
/// CPI Guard is enabled, and a program attempted to transfer user funds without using a delegate
#[error("CPI Guard is enabled, and a program attempted to transfer user funds via CPI without using a delegate")]
CpiGuardTransferBlocked,
/// CPI Guard is enabled, and a program attempted to burn user funds without using a delegate
#[error(
"CPI Guard is enabled, and a program attempted to burn user funds via CPI without using a delegate"
)]
CpiGuardBurnBlocked,
/// CPI Guard is enabled, and a program attempted to close an account without returning lamports to owner
#[error("CPI Guard is enabled, and a program attempted to close an account via CPI without returning lamports to owner")]
CpiGuardCloseAccountBlocked,
// 45
/// CPI Guard is enabled, and a program attempted to approve a delegate
#[error("CPI Guard is enabled, and a program attempted to approve a delegate via CPI")]
CpiGuardApproveBlocked,
/// CPI Guard is enabled, and a program attempted to add or replace an authority
#[error(
"CPI Guard is enabled, and a program attempted to add or replace an authority via CPI"
)]
CpiGuardSetAuthorityBlocked,
/// Account ownership cannot be changed while CPI Guard is enabled
#[error("Account ownership cannot be changed while CPI Guard is enabled")]
CpiGuardOwnerChangeBlocked,
} }
impl From<TokenError> for ProgramError { impl From<TokenError> for ProgramError {
fn from(e: TokenError) -> Self { fn from(e: TokenError) -> Self {
@ -302,27 +274,6 @@ impl PrintProgramError for TokenError {
TokenError::MaximumDepositAmountExceeded => { TokenError::MaximumDepositAmountExceeded => {
msg!("Deposit amount exceeds maximum limit") msg!("Deposit amount exceeds maximum limit")
} }
TokenError::CpiGuardSettingsLocked => {
msg!("CPI Guard status cannot be changed in CPI")
}
TokenError::CpiGuardTransferBlocked => {
msg!("CPI Guard is enabled, and a program attempted to transfer user funds without using a delegate")
}
TokenError::CpiGuardBurnBlocked => {
msg!("CPI Guard is enabled, and a program attempted to burn user funds without using a delegate")
}
TokenError::CpiGuardCloseAccountBlocked => {
msg!("CPI Guard is enabled, and a program attempted to close an account without returning lamports to owner")
}
TokenError::CpiGuardApproveBlocked => {
msg!("CPI Guard is enabled, and a program attempted to approve a delegate")
}
TokenError::CpiGuardSetAuthorityBlocked => {
msg!("CPI Guard is enabled, and a program attempted to add or change an authority")
}
TokenError::CpiGuardOwnerChangeBlocked => {
msg!("Account ownership cannot be changed while CPI Guard is enabled")
}
} }
} }
} }

View File

@ -1,100 +0,0 @@
use {
crate::{
check_program_account,
instruction::{encode_instruction, TokenInstruction},
},
num_enum::{IntoPrimitive, TryFromPrimitive},
solana_program::{
instruction::{AccountMeta, Instruction},
program_error::ProgramError,
pubkey::Pubkey,
},
};
/// CPI Guard extension instructions
#[derive(Clone, Copy, Debug, PartialEq, IntoPrimitive, TryFromPrimitive)]
#[repr(u8)]
pub enum CpiGuardInstruction {
/// Lock certain token operations from taking place within CPI for this Account, namely:
/// * Transfer and Burn must go through a delegate.
/// * CloseAccount can only return lamports to owner.
/// * SetAuthority can only be used to remove an existing close authority.
/// * Approve is disallowed entirely.
///
/// In addition, CPI Guard cannot be enabled or disabled via CPI.
///
/// Accounts expected by this instruction:
///
/// 0. `[writable]` The account to update.
/// 1. `[signer]` The account's owner.
///
/// * Multisignature authority
/// 0. `[writable]` The account to update.
/// 1. `[]` The account's multisignature owner.
/// 2. ..2+M `[signer]` M signer accounts.
///
Enable,
/// Allow all token operations to happen via CPI as normal.
///
/// Implicitly initializes the extension in the case where it is not present.
///
/// Accounts expected by this instruction:
///
/// 0. `[writable]` The account to update.
/// 1. `[signer]` The account's owner.
///
/// * Multisignature authority
/// 0. `[writable]` The account to update.
/// 1. `[]` The account's multisignature owner.
/// 2. ..2+M `[signer]` M signer accounts.
///
Disable,
}
/// Create an `Enable` instruction
pub fn enable_cpi_guard(
token_program_id: &Pubkey,
account: &Pubkey,
owner: &Pubkey,
signers: &[&Pubkey],
) -> Result<Instruction, ProgramError> {
check_program_account(token_program_id)?;
let mut accounts = vec![
AccountMeta::new(*account, false),
AccountMeta::new_readonly(*owner, signers.is_empty()),
];
for signer_pubkey in signers.iter() {
accounts.push(AccountMeta::new_readonly(**signer_pubkey, true));
}
Ok(encode_instruction(
token_program_id,
accounts,
TokenInstruction::CpiGuardExtension,
CpiGuardInstruction::Enable,
&(),
))
}
/// Create a `Disable` instruction
pub fn disable_cpi_guard(
token_program_id: &Pubkey,
account: &Pubkey,
owner: &Pubkey,
signers: &[&Pubkey],
) -> Result<Instruction, ProgramError> {
check_program_account(token_program_id)?;
let mut accounts = vec![
AccountMeta::new(*account, false),
AccountMeta::new_readonly(*owner, signers.is_empty()),
];
for signer_pubkey in signers.iter() {
accounts.push(AccountMeta::new_readonly(**signer_pubkey, true));
}
Ok(encode_instruction(
token_program_id,
accounts,
TokenInstruction::CpiGuardExtension,
CpiGuardInstruction::Disable,
&(),
))
}

View File

@ -1,39 +0,0 @@
use {
crate::{
extension::{Extension, ExtensionType, StateWithExtensionsMut},
pod::PodBool,
state::Account,
},
bytemuck::{Pod, Zeroable},
solana_program::instruction::{get_stack_height, TRANSACTION_LEVEL_STACK_HEIGHT},
};
/// CPI Guard extension instructions
pub mod instruction;
/// CPI Guard extension processor
pub mod processor;
/// CPI Guard extension for Accounts
#[repr(C)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
pub struct CpiGuard {
/// Lock privileged token operations from happening via CPI
pub lock_cpi: PodBool,
}
impl Extension for CpiGuard {
const TYPE: ExtensionType = ExtensionType::CpiGuard;
}
/// Determine if CPI Guard is enabled for this account
pub fn cpi_guard_enabled(account_state: &StateWithExtensionsMut<Account>) -> bool {
if let Ok(extension) = account_state.get_extension::<CpiGuard>() {
return extension.lock_cpi.into();
}
false
}
/// Determine if we are in CPI
pub fn in_cpi() -> bool {
get_stack_height() > TRANSACTION_LEVEL_STACK_HEIGHT
}

View File

@ -1,98 +0,0 @@
use {
crate::{
check_program_account,
error::TokenError,
extension::{
cpi_guard::{in_cpi, instruction::CpiGuardInstruction, CpiGuard},
StateWithExtensionsMut,
},
instruction::decode_instruction_type,
processor::Processor,
state::Account,
},
solana_program::{
account_info::{next_account_info, AccountInfo},
entrypoint::ProgramResult,
msg,
pubkey::Pubkey,
},
};
fn process_enable_cpi_guard(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let token_account_info = next_account_info(account_info_iter)?;
let owner_info = next_account_info(account_info_iter)?;
let owner_info_data_len = owner_info.data_len();
let mut account_data = token_account_info.data.borrow_mut();
let mut account = StateWithExtensionsMut::<Account>::unpack(&mut account_data)?;
Processor::validate_owner(
program_id,
&account.base.owner,
owner_info,
owner_info_data_len,
account_info_iter.as_slice(),
)?;
if in_cpi() {
return Err(TokenError::CpiGuardSettingsLocked.into());
}
let extension = if let Ok(extension) = account.get_extension_mut::<CpiGuard>() {
extension
} else {
account.init_extension::<CpiGuard>(true)?
};
extension.lock_cpi = true.into();
Ok(())
}
fn process_diasble_cpi_guard(program_id: &Pubkey, accounts: &[AccountInfo]) -> ProgramResult {
let account_info_iter = &mut accounts.iter();
let token_account_info = next_account_info(account_info_iter)?;
let owner_info = next_account_info(account_info_iter)?;
let owner_info_data_len = owner_info.data_len();
let mut account_data = token_account_info.data.borrow_mut();
let mut account = StateWithExtensionsMut::<Account>::unpack(&mut account_data)?;
Processor::validate_owner(
program_id,
&account.base.owner,
owner_info,
owner_info_data_len,
account_info_iter.as_slice(),
)?;
if in_cpi() {
return Err(TokenError::CpiGuardSettingsLocked.into());
}
let extension = if let Ok(extension) = account.get_extension_mut::<CpiGuard>() {
extension
} else {
account.init_extension::<CpiGuard>(true)?
};
extension.lock_cpi = false.into();
Ok(())
}
pub(crate) fn process_instruction(
program_id: &Pubkey,
accounts: &[AccountInfo],
input: &[u8],
) -> ProgramResult {
check_program_account(program_id)?;
match decode_instruction_type(input)? {
CpiGuardInstruction::Enable => {
msg!("CpiGuardInstruction::Enable");
process_enable_cpi_guard(program_id, accounts)
}
CpiGuardInstruction::Disable => {
msg!("CpiGuardInstruction::Disable");
process_diasble_cpi_guard(program_id, accounts)
}
}
}

View File

@ -31,7 +31,7 @@ pub enum RequiredMemoTransfersInstruction {
Enable, Enable,
/// Stop requiring memos for transfers into this Account. /// Stop requiring memos for transfers into this Account.
/// ///
/// Implicitly initializes the extension in the case where it is not present. /// Fails if the account does not have the extension present.
/// ///
/// Accounts expected by this instruction: /// Accounts expected by this instruction:
/// ///

View File

@ -5,7 +5,6 @@ use {
error::TokenError, error::TokenError,
extension::{ extension::{
confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint}, confidential_transfer::{ConfidentialTransferAccount, ConfidentialTransferMint},
cpi_guard::CpiGuard,
default_account_state::DefaultAccountState, default_account_state::DefaultAccountState,
immutable_owner::ImmutableOwner, immutable_owner::ImmutableOwner,
interest_bearing_mint::InterestBearingConfig, interest_bearing_mint::InterestBearingConfig,
@ -34,8 +33,6 @@ use serde::{Deserialize, Serialize};
/// Confidential Transfer extension /// Confidential Transfer extension
pub mod confidential_transfer; pub mod confidential_transfer;
/// CPI Guard extension
pub mod cpi_guard;
/// Default Account State extension /// Default Account State extension
pub mod default_account_state; pub mod default_account_state;
/// Immutable Owner extension /// Immutable Owner extension
@ -632,8 +629,6 @@ pub enum ExtensionType {
NonTransferable, NonTransferable,
/// Tokens accrue interest over time, /// Tokens accrue interest over time,
InterestBearingConfig, InterestBearingConfig,
/// Locks privileged token operations from happening via CPI
CpiGuard,
/// Padding extension used to make an account exactly Multisig::LEN, used for testing /// Padding extension used to make an account exactly Multisig::LEN, used for testing
#[cfg(test)] #[cfg(test)]
AccountPaddingTest = u16::MAX - 1, AccountPaddingTest = u16::MAX - 1,
@ -674,7 +669,6 @@ impl ExtensionType {
ExtensionType::MemoTransfer => pod_get_packed_len::<MemoTransfer>(), ExtensionType::MemoTransfer => pod_get_packed_len::<MemoTransfer>(),
ExtensionType::NonTransferable => pod_get_packed_len::<NonTransferable>(), ExtensionType::NonTransferable => pod_get_packed_len::<NonTransferable>(),
ExtensionType::InterestBearingConfig => pod_get_packed_len::<InterestBearingConfig>(), ExtensionType::InterestBearingConfig => pod_get_packed_len::<InterestBearingConfig>(),
ExtensionType::CpiGuard => pod_get_packed_len::<CpiGuard>(),
#[cfg(test)] #[cfg(test)]
ExtensionType::AccountPaddingTest => pod_get_packed_len::<AccountPaddingTest>(), ExtensionType::AccountPaddingTest => pod_get_packed_len::<AccountPaddingTest>(),
#[cfg(test)] #[cfg(test)]
@ -735,8 +729,7 @@ impl ExtensionType {
ExtensionType::ImmutableOwner ExtensionType::ImmutableOwner
| ExtensionType::TransferFeeAmount | ExtensionType::TransferFeeAmount
| ExtensionType::ConfidentialTransferAccount | ExtensionType::ConfidentialTransferAccount
| ExtensionType::MemoTransfer | ExtensionType::MemoTransfer => AccountType::Account,
| ExtensionType::CpiGuard => AccountType::Account,
#[cfg(test)] #[cfg(test)]
ExtensionType::AccountPaddingTest => AccountType::Account, ExtensionType::AccountPaddingTest => AccountType::Account,
#[cfg(test)] #[cfg(test)]

View File

@ -605,11 +605,6 @@ pub enum TokenInstruction<'a> {
/// See `extension::interest_bearing_mint::instruction::InterestBearingMintInstruction` for /// See `extension::interest_bearing_mint::instruction::InterestBearingMintInstruction` for
/// further details about the extended instructions that share this instruction prefix /// further details about the extended instructions that share this instruction prefix
InterestBearingMintExtension, InterestBearingMintExtension,
/// The common instruction prefix for CPI Guard account extension instructions.
///
/// See `extension::cpi_guard::instruction::CpiGuardInstruction` for
/// further details about the extended instructions that share this instruction prefix
CpiGuardExtension,
} }
impl<'a> TokenInstruction<'a> { impl<'a> TokenInstruction<'a> {
/// Unpacks a byte buffer into a [TokenInstruction](enum.TokenInstruction.html). /// Unpacks a byte buffer into a [TokenInstruction](enum.TokenInstruction.html).
@ -740,7 +735,6 @@ impl<'a> TokenInstruction<'a> {
31 => Self::CreateNativeMint, 31 => Self::CreateNativeMint,
32 => Self::InitializeNonTransferableMint, 32 => Self::InitializeNonTransferableMint,
33 => Self::InterestBearingMintExtension, 33 => Self::InterestBearingMintExtension,
34 => Self::CpiGuardExtension,
_ => return Err(TokenError::InvalidInstruction.into()), _ => return Err(TokenError::InvalidInstruction.into()),
}) })
} }
@ -893,9 +887,6 @@ impl<'a> TokenInstruction<'a> {
&Self::InterestBearingMintExtension => { &Self::InterestBearingMintExtension => {
buf.push(33); buf.push(33);
} }
&Self::CpiGuardExtension => {
buf.push(34);
}
}; };
buf buf
} }

View File

@ -6,7 +6,6 @@ use {
error::TokenError, error::TokenError,
extension::{ extension::{
confidential_transfer::{self, ConfidentialTransferAccount}, confidential_transfer::{self, ConfidentialTransferAccount},
cpi_guard::{self, in_cpi, CpiGuard},
default_account_state::{self, DefaultAccountState}, default_account_state::{self, DefaultAccountState},
immutable_owner::ImmutableOwner, immutable_owner::ImmutableOwner,
interest_bearing_mint::{self, InterestBearingConfig}, interest_bearing_mint::{self, InterestBearingConfig},
@ -349,21 +348,13 @@ impl Processor {
} }
} }
} }
_ => { _ => Self::validate_owner(
Self::validate_owner( program_id,
program_id, &source_account.base.owner,
&source_account.base.owner, authority_info,
authority_info, authority_info_data_len,
authority_info_data_len, account_info_iter.as_slice(),
account_info_iter.as_slice(), )?,
)?;
if let Ok(cpi_guard) = source_account.get_extension::<CpiGuard>() {
if cpi_guard.lock_cpi.into() && in_cpi() {
return Err(TokenError::CpiGuardTransferBlocked.into());
}
}
}
}; };
// Revisit this later to see if it's worth adding a check to reduce // Revisit this later to see if it's worth adding a check to reduce
@ -485,12 +476,6 @@ impl Processor {
account_info_iter.as_slice(), account_info_iter.as_slice(),
)?; )?;
if let Ok(cpi_guard) = source_account.get_extension::<CpiGuard>() {
if cpi_guard.lock_cpi.into() && in_cpi() {
return Err(TokenError::CpiGuardApproveBlocked.into());
}
}
source_account.base.delegate = COption::Some(*delegate_info.key); source_account.base.delegate = COption::Some(*delegate_info.key);
source_account.base.delegated_amount = amount; source_account.base.delegated_amount = amount;
source_account.pack_base(); source_account.pack_base();
@ -564,14 +549,6 @@ impl Processor {
return Err(TokenError::ImmutableOwner.into()); return Err(TokenError::ImmutableOwner.into());
} }
if let Ok(cpi_guard) = account.get_extension::<CpiGuard>() {
if cpi_guard.lock_cpi.into() && in_cpi() {
return Err(TokenError::CpiGuardSetAuthorityBlocked.into());
} else if cpi_guard.lock_cpi.into() {
return Err(TokenError::CpiGuardOwnerChangeBlocked.into());
}
}
if let COption::Some(authority) = new_authority { if let COption::Some(authority) = new_authority {
account.base.owner = authority; account.base.owner = authority;
} else { } else {
@ -594,13 +571,6 @@ impl Processor {
authority_info_data_len, authority_info_data_len,
account_info_iter.as_slice(), account_info_iter.as_slice(),
)?; )?;
if let Ok(cpi_guard) = account.get_extension::<CpiGuard>() {
if cpi_guard.lock_cpi.into() && in_cpi() && new_authority != COption::None {
return Err(TokenError::CpiGuardSetAuthorityBlocked.into());
}
}
account.base.close_authority = new_authority; account.base.close_authority = new_authority;
} }
_ => { _ => {
@ -859,21 +829,13 @@ impl Processor {
source_account.base.delegate = COption::None; source_account.base.delegate = COption::None;
} }
} }
_ => { _ => Self::validate_owner(
Self::validate_owner( program_id,
program_id, &source_account.base.owner,
&source_account.base.owner, authority_info,
authority_info, authority_info_data_len,
authority_info_data_len, account_info_iter.as_slice(),
account_info_iter.as_slice(), )?,
)?;
if let Ok(cpi_guard) = source_account.get_extension::<CpiGuard>() {
if cpi_guard.lock_cpi.into() && in_cpi() {
return Err(TokenError::CpiGuardBurnBlocked.into());
}
}
}
} }
} }
@ -927,15 +889,6 @@ impl Processor {
.base .base
.is_owned_by_system_program_or_incinerator() .is_owned_by_system_program_or_incinerator()
{ {
if let Ok(cpi_guard) = source_account.get_extension::<CpiGuard>() {
if cpi_guard.lock_cpi.into()
&& in_cpi()
&& !cmp_pubkeys(destination_account_info.key, &source_account.base.owner)
{
return Err(TokenError::CpiGuardCloseAccountBlocked.into());
}
}
Self::validate_owner( Self::validate_owner(
program_id, program_id,
&authority, &authority,
@ -1371,9 +1324,6 @@ impl Processor {
&input[1..], &input[1..],
) )
} }
TokenInstruction::CpiGuardExtension => {
cpi_guard::processor::process_instruction(program_id, accounts, &input[1..])
}
} }
} }