Skip to content

Commit 2608880

Browse files
Add Quasar port of allow-block-list-token transfer hook example
Port the ABL (Allow/Block List) token transfer hook program from Anchor to Quasar. This program enforces allow/block lists on Token-2022 transfers. All 7 instructions ported: - init_mint: Creates Token-2022 mint with transfer hook, permanent delegate, metadata pointer, and embedded metadata (AB mode + threshold) - init_config: Creates config PDA with authority - attach_to_mint: Attaches transfer hook to an existing mint - tx_hook: SPL transfer hook execute handler with allow/block/threshold logic - init_wallet: Creates per-wallet allow/block entries - remove_wallet: Removes wallet entries (closes PDA) - change_mode: Changes mode via Token-2022 metadata update Key Quasar patterns: - Raw CPI for Token-2022 extension init (TransferHook, PermanentDelegate, MetadataPointer, InitializeMint2, TokenMetadata) - Manual TLV parsing of Token-2022 metadata in tx_hook for mode detection - ExtraAccountMetaList with AccountData seed (destination owner lookup) - Direct lamport manipulation for account closing (remove_wallet) - 8-byte instruction discriminators (required by SPL transfer hook interface) Builds with quasar build (54.5 KB). State unit tests pass.
1 parent e0fb78f commit 2608880

File tree

15 files changed

+1419
-0
lines changed

15 files changed

+1419
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "quasar-abl-token"
3+
version = "0.1.0"
4+
edition = "2021"
5+
6+
[workspace]
7+
8+
[lints.rust.unexpected_cfgs]
9+
level = "warn"
10+
check-cfg = ['cfg(target_os, values("solana"))']
11+
12+
[lib]
13+
crate-type = ["cdylib", "lib"]
14+
15+
[features]
16+
alloc = ["quasar-lang/alloc"]
17+
client = []
18+
debug = []
19+
20+
[dependencies]
21+
quasar-lang = "0.0"
22+
quasar-spl = "0.0"
23+
solana-instruction = { version = "3.2.0" }
24+
25+
[dev-dependencies]
26+
quasar-svm = { version = "0.1" }
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
[project]
2+
name = "quasar_abl_token"
3+
4+
[toolchain]
5+
type = "solana"
6+
7+
[testing]
8+
language = "rust"
9+
10+
[testing.rust]
11+
framework = "quasar-svm"
12+
13+
[testing.rust.test]
14+
program = "cargo"
15+
args = ["test", "tests::"]
16+
17+
[clients]
18+
languages = ["rust"]
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
pub const META_LIST_ACCOUNT_SEED: &[u8] = b"extra-account-metas";
2+
pub const CONFIG_SEED: &[u8] = b"config";
3+
pub const AB_WALLET_SEED: &[u8] = b"ab_wallet";
4+
5+
/// SHA-256("spl-transfer-hook-interface:execute")[:8]
6+
pub const EXECUTE_DISCRIMINATOR: [u8; 8] = [105, 37, 101, 197, 75, 251, 102, 26];
7+
8+
/// Maximum lengths for metadata fields.
9+
pub const MAX_NAME: usize = 32;
10+
pub const MAX_SYMBOL: usize = 10;
11+
pub const MAX_URI: usize = 128;
12+
13+
/// Maximum buffer size for Token-2022 metadata CPI instructions.
14+
pub const MAX_META_IX: usize = 512;
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
use quasar_lang::prelude::ProgramError;
2+
3+
/// Custom error codes for the allow/block list program.
4+
/// Encoded as ProgramError::Custom(N).
5+
6+
pub const ERROR_INVALID_METADATA: u32 = 6000;
7+
pub const ERROR_WALLET_NOT_ALLOWED: u32 = 6001;
8+
pub const ERROR_AMOUNT_NOT_ALLOWED: u32 = 6002;
9+
pub const ERROR_WALLET_BLOCKED: u32 = 6003;
10+
pub const ERROR_UNAUTHORIZED: u32 = 6004;
11+
12+
pub fn invalid_metadata() -> ProgramError {
13+
ProgramError::Custom(ERROR_INVALID_METADATA)
14+
}
15+
16+
pub fn wallet_not_allowed() -> ProgramError {
17+
ProgramError::Custom(ERROR_WALLET_NOT_ALLOWED)
18+
}
19+
20+
pub fn amount_not_allowed() -> ProgramError {
21+
ProgramError::Custom(ERROR_AMOUNT_NOT_ALLOWED)
22+
}
23+
24+
pub fn wallet_blocked() -> ProgramError {
25+
ProgramError::Custom(ERROR_WALLET_BLOCKED)
26+
}
27+
28+
pub fn unauthorized() -> ProgramError {
29+
ProgramError::Custom(ERROR_UNAUTHORIZED)
30+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use quasar_lang::cpi::{CpiCall, InstructionAccount, Seed};
2+
use quasar_lang::prelude::*;
3+
use quasar_lang::sysvars::Sysvar;
4+
5+
use crate::constants::*;
6+
use crate::instructions::init_mint::Token2022;
7+
8+
#[derive(Accounts)]
9+
pub struct AttachToMint<'info> {
10+
#[account(mut)]
11+
pub payer: &'info Signer,
12+
#[account(mut)]
13+
pub mint: &'info UncheckedAccount,
14+
#[account(mut)]
15+
pub extra_metas_account: &'info mut UncheckedAccount,
16+
pub system_program: &'info Program<System>,
17+
pub token_program: &'info Program<Token2022>,
18+
}
19+
20+
impl AttachToMint<'_> {
21+
#[inline(always)]
22+
pub fn attach_to_mint(&self) -> Result<(), ProgramError> {
23+
let mint_key = self.mint.to_account_view().address();
24+
let payer_key = self.payer.to_account_view().address();
25+
let token_prog = self.token_program.to_account_view().address();
26+
27+
// TransferHookUpdate: opcode 36, sub-opcode 1
28+
// Sets the transfer hook program_id on the mint.
29+
let mut update_data = [0u8; 37];
30+
update_data[0] = 36;
31+
update_data[1] = 1; // Update sub-instruction
32+
// COption<Pubkey>: 4 bytes discriminator (1 = Some) + 32 bytes pubkey
33+
update_data[2..6].copy_from_slice(&1u32.to_le_bytes()); // Some
34+
update_data[6..38 - 1].copy_from_slice(&crate::ID.as_ref()[..31]);
35+
// Actually, COption encoding is: [1u8 if Some, 0 if None] but SPL uses 4 bytes
36+
// Let me use the right encoding: just 1 byte bool then 32 byte address? No.
37+
// SPL token uses: 1 byte discriminator (1=Some) followed by 32 bytes.
38+
// But wait - the TransferHookUpdate instruction encoding for the optional program_id is:
39+
// COption: 4 bytes (0 = None, 1 = Some), then 32 bytes if Some.
40+
// Total ix data = 2 (opcode + sub) + 4 + 32 = 38
41+
// Let me redo this properly.
42+
let mut update_data = [0u8; 38];
43+
update_data[0] = 36; // TransferHookExtension opcode
44+
update_data[1] = 1; // Update sub-instruction
45+
update_data[2..6].copy_from_slice(&1u32.to_le_bytes()); // COption::Some
46+
update_data[6..38].copy_from_slice(crate::ID.as_ref());
47+
48+
CpiCall::new(
49+
token_prog,
50+
[
51+
InstructionAccount::writable(mint_key),
52+
InstructionAccount::readonly_signer(payer_key),
53+
],
54+
[
55+
self.mint.to_account_view(),
56+
self.payer.to_account_view(),
57+
],
58+
update_data,
59+
)
60+
.invoke()?;
61+
62+
// Initialize the ExtraAccountMetaList PDA (same as in init_mint).
63+
let meta_list_size: u64 = 51;
64+
let lamports = Rent::get()?.try_minimum_balance(meta_list_size as usize)?;
65+
66+
let (expected_pda, bump) = Address::find_program_address(
67+
&[META_LIST_ACCOUNT_SEED, mint_key.as_ref()],
68+
&crate::ID,
69+
);
70+
if self.extra_metas_account.to_account_view().address() != &expected_pda {
71+
return Err(ProgramError::InvalidSeeds);
72+
}
73+
74+
let bump_bytes = [bump];
75+
let seeds = [
76+
Seed::from(META_LIST_ACCOUNT_SEED),
77+
Seed::from(mint_key.as_ref()),
78+
Seed::from(&bump_bytes as &[u8]),
79+
];
80+
81+
self.system_program
82+
.create_account(
83+
self.payer,
84+
&*self.extra_metas_account,
85+
lamports,
86+
meta_list_size,
87+
&crate::ID,
88+
)
89+
.invoke_signed(&seeds)?;
90+
91+
// Write ExtraAccountMeta TLV data
92+
let view = unsafe {
93+
&mut *(self.extra_metas_account as *const UncheckedAccount as *mut UncheckedAccount
94+
as *mut AccountView)
95+
};
96+
let mut data = view.try_borrow_mut()?;
97+
98+
data[0..8].copy_from_slice(&EXECUTE_DISCRIMINATOR);
99+
data[8..12].copy_from_slice(&39u32.to_le_bytes());
100+
data[12..16].copy_from_slice(&1u32.to_le_bytes());
101+
102+
// ABWallet PDA: seeds = [Literal("ab_wallet"), AccountData(2, 32, 32)]
103+
data[16] = 1; // PDA from seeds
104+
let mut config = [0u8; 32];
105+
config[0] = 2; // 2 seeds
106+
config[1] = 0; // literal
107+
config[2] = 9; // length
108+
config[3..12].copy_from_slice(AB_WALLET_SEED);
109+
config[12] = 1; // account data
110+
config[13] = 2; // account_index (destination token account)
111+
config[14] = 32; // data_index
112+
config[15] = 32; // length
113+
data[17..49].copy_from_slice(&config);
114+
data[49] = 0; // not signer
115+
data[50] = 0; // not writable
116+
117+
log("Transfer hook attached to mint");
118+
Ok(())
119+
}
120+
}

0 commit comments

Comments
 (0)