ERC-3643 è uno standard di token Ethereum progettato specificamente per la conformità normativa (KYC, AML, ecc.) e l'emissione di token autorizzati. Come ERC-20, delinea le funzionalità principali dei token fungibili, ma garantisce che i requisiti di conformità essenziali possano essere applicati onchain.
Token Extensions estende il Token Program con funzionalità avanzate per la conformità.
Funzionalità chiave:documentazione completaper ulteriori dettagli.
Il Token Extension Program fornisce estensioni modulari per i token.
Queste estensioni includono controlli di trasferimento, hook personalizzati e altro.
pragma solidity ^0.8.28;
interface ISpl20 {
function transfer(address to, address mintAddress, uint256 amount) external;
function getTokenAccount(address owner, address token) external view returns (uint256 balance, bool isFrozen);
function mintTokens(address to, address mintAddress, uint256 amount) external;
}
contract SPL3643 {
ISpl20 public immutable spl20;
address public immutable mintAddress;
mapping(address => bool) public isKYCApproved;
mapping(address => bool) public frozen;
address public complianceAuthority;
address public transferHookProgram;
event KYCApproved(address indexed user, bool status);
event AccountFrozen(address indexed user, bool status);
event TransferHookSet(address indexed hookProgram);
event ForcedTransfer(address indexed from, address indexed to, uint256 value);
event Transfer(address indexed from, address indexed to, uint256 value);
constructor(address _spl20, address _mint, address authority) {
spl20 = ISpl20(_spl20);
mintAddress = _mint;
complianceAuthority = authority;
}
modifier onlyComplianceAuth() {
require(msg.sender == complianceAuthority, "not compliance auth");
_;
}
function approveKYC(address user, bool approved) external onlyComplianceAuth {
isKYCApproved[user] = approved;
emit KYCApproved(user, approved);
}
function freezeAccount(address user, bool freeze) external onlyComplianceAuth {
frozen[user] = freeze;
emit AccountFrozen(user, freeze);
}
function setTransferHook(address hookProgram) external onlyComplianceAuth {
transferHookProgram = hookProgram;
emit TransferHookSet(hookProgram);
}
function setComplianceAuthority(address newAuthority) external onlyComplianceAuth {
complianceAuthority = newAuthority;
}
function transfer(address to, uint256 amount) external {
require(!frozen[msg.sender] && !frozen[to], "account frozen");
require(isKYCApproved[msg.sender] && isKYCApproved[to], "KYC required");
if (transferHookProgram != address(0)) {
bool ok = ITransferHook(transferHookProgram).onTransfer(msg.sender, to, amount);
require(ok, "blocked by hook");
}
spl20.transfer(to, mintAddress, amount);
emit Transfer(msg.sender, to, amount);
}
function forceTransfer(address from, address to, uint256 amount) external onlyComplianceAuth {
// temporarily unfreeze to bypass Spl20.transfer require(msg.sender == owner)
frozen[from] = false;
spl20.transfer(to, mintAddress, amount);
frozen[from] = true;
emit ForcedTransfer(from, to, amount);
}
}
interface ITransferHook {
function onTransfer(address from, address to, uint256 amount) external returns (bool);
}
Implementa verifiche KYC e whitelist utilizzando Transfer Hooks.
Congela account specifici per la conformità normativa.
I Transfer Hook permettono logica personalizzata durante i trasferimenti.
L'estensione Transfer Hook di Solana esegue questa funzione a livello di protocollo, offrendo agli sviluppatori un potente controllo onchain sui trasferimenti senza richiedere il deployment di contratti personalizzati.
Designa un delegato permanente per gestire i token conformi.
User → Token‑2022 (transfer) ↘
Transfer Hook (KYC check) → OK / ERR
↘
Token‑2022 (actual balance change)
Una singola transazione Solana contiene sia l'istruzione del token che la chiamata all'hook. Se l'hook restituisce un errore, l'intera transazione viene annullata, rispecchiando l'applicazione onchain di ERC-3643.
Implementazione base di un Transfer Hook
// programs/kyc_hook/src/lib.rs
use anchor_lang::prelude::*;
use anchor_spl::token_interface::{Mint, TokenAccount, TokenInterface};
use spl_tlv_account_resolution::{seeds::Seed, state::*};
use spl_transfer_hook_interface::instruction::TransferHookInstruction;
declare_id!("KycHook1111111111111111111111111111111111111");
#[program]
pub mod kyc_hook {
use super::*;
/// One-time initializer: create extra_account_meta_list PDA,
/// register the whitelist PDA, and automatically whitelist the payer.
#[interface(spl_transfer_hook_interface::initialize_extra_account_meta_list)]
pub fn init_meta(ctx: Context<InitMeta>) -> Result<()> {
/* -- 1. Seed the whitelist with the payer / mint authority -- */
let wl = &mut ctx.accounts.whitelist;
let payer_key = ctx.accounts.payer.key();
if !wl.allowed.contains(&payer_key) {
wl.allowed.push(payer_key);
}
/* -- 2. Build ExtraAccountMeta so Token-2022 forwards whitelist -- */
let metas = vec![ExtraAccountMeta::new_with_pubkey(
&ctx.accounts.whitelist.key(),
/* is_signer */ false,
/* is_writable*/ true,
)?];
/* -- 3. Create extra_account_meta_list PDA -- */
let size = ExtraAccountMetaList::size_of(metas.len())? as u64;
let lamports = Rent::get()?.minimum_balance(size as usize);
let seeds = &[
b"extra-account-metas",
ctx.accounts.mint.key().as_ref(),
&[ctx.bumps.extra_metas],
];
anchor_lang::system_program::create_account(
CpiContext::new_with_signer(
ctx.accounts.system_program.to_account_info(),
anchor_lang::system_program::CreateAccount {
from: ctx.accounts.payer.to_account_info(),
to: ctx.accounts.extra_metas.to_account_info(),
},
&[seeds],
),
lamports,
size,
ctx.program_id,
)?;
/* -- 4. Initialise TLV data inside the PDA -- */
ExtraAccountMetaList::init::<TransferHookInstruction>(
&mut ctx.accounts.extra_metas.try_borrow_mut_data()?,
&metas,
)?;
Ok(())
}
/// Called automatically on every Token-2022 transfer.
#[interface(spl_transfer_hook_interface::execute)]
pub fn execute(ctx: Context<Hook>, _amount: u64) -> Result<()> {
let wl = &ctx.accounts.whitelist;
let src = ctx.accounts.source_token.owner;
let dst = ctx.accounts.dest_token.owner;
require!(wl.allowed.contains(&src), ComplianceError::SrcNotAllowed);
require!(wl.allowed.contains(&dst), ComplianceError::DstNotAllowed);
Ok(())
}
}
/* ---------------- Data & account structs ---------------- */
#[account] // simple demo whitelist
pub struct Whitelist {
pub allowed: Vec<Pubkey>,
}
/* init_meta accounts */
#[derive(Accounts)]
pub struct InitMeta<'info> {
#[account(mut)]
payer: Signer<'info>,
/// CHECK: PDA = ["extra-account-metas", mint]
#[account(mut, seeds = [b"extra-account-metas", mint.key().as_ref()], bump)]
extra_metas: AccountInfo<'info>,
/// CHECK: verified by interface macro
#[account(mut)]
mint: InterfaceAccount<'info, Mint>,
#[account(mut)]
whitelist: Account<'info, Whitelist>,
system_program: Program<'info, System>,
}
/* execute accounts (fixed order!) */
#[derive(Accounts)]
pub struct Hook<'info> {
// 0 source token
#[account(token::mint = mint, token::authority = owner)]
source_token: InterfaceAccount<'info, TokenAccount>,
// 1 mint
mint: InterfaceAccount<'info, Mint>,
// 2 destination token
#[account(token::mint = mint)]
dest_token: InterfaceAccount<'info, TokenAccount>,
// 3 owner (source wallet)
/// CHECK:
owner: UncheckedAccount<'info>,
// 4 extra_account_meta_list
/// CHECK:
#[account(seeds = [b"extra-account-metas", mint.key().as_ref()], bump)]
extra_account_meta_list: UncheckedAccount<'info>,
// 5 whitelist (forwarded via meta list)
whitelist: Account<'info, Whitelist>,
}
#[error_code]
pub enum ComplianceError {
#[msg("source wallet not allowed")]
SrcNotAllowed,
#[msg("destination wallet not allowed")]
DstNotAllowed,
}
Per prima cosa scegli un wallet che fungerà da amministratore della conformità e salvi la sua pubkey in COMP_AUTH, poi pre-calcoli il PDA della whitelist che memorizzerai nell'hook e lo chiami WL_PDA. Con le chiavi pronte compili kyc_hook usando anchor build e lo distribuisci, annotando il program-id appena stampato come HOOK_ID. Successivamente crei un mint Token-2022 completamente nuovo che punta a questo hook passando --transfer-hook $HOOK_ID, nominando anche il wallet admin come autorità di freeze e claw-back; Solana stampa il MINT_ADDRESS risultante. Poiché Token-2022 deve sapere quali account extra passare all'hook, esegui immediatamente anchor run init-meta, che chiama l'inizializzatore dell'hook e memorizza WL_PDA nell'elenco extra-account-meta del mint. Questo passaggio viene eseguito una sola volta. Ora il mint è configurato per il KYC, quindi puoi liberamente coniare la fornitura iniziale a te stesso; l'hook non viene invocato nelle operazioni di mint. Infine trasferisci i token a un altro wallet: in quel momento Token-2022 invoca l'hook, che controlla sia il mittente che il destinatario rispetto alla whitelist onchain. Se entrambi sono nell'elenco il trasferimento va a buon fine; se uno dei due manca l'hook genera un errore e l'intera transazione viene annullata, offrendoti una conformità in stile ERC-3643 in un'unica transazione Solana.
# 0. Keys & PDAs
COMP_AUTH=$(solana address) # compliance admin key
WL_PDA=<derived whitelist PDA> # used in step 2
# 1. Build & deploy the hook
anchor build
solana program deploy target/deploy/kyc_hook.so # save as HOOK_ID
# 2. Create a KYC-enabled mint (Token-2022 CLI)
spl-token --program-id TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb create-token \
--transfer-hook $HOOK_ID \
--enable-permanent-delegate \
--freeze-authority $COMP_AUTH # returns MINT_ADDRESS
# 3. Initialize extra_account_meta_list (one-time)
anchor run init-meta -- --mint $MINT_ADDRESS --whitelist $WL_PDA
# 4. Mint & transfer
spl-token mint $MINT_ADDRESS 100 $(solana address) # minting bypasses hook
spl-token transfer $MINT_ADDRESS 10 <RECIPIENT> # hook executes, KYC enforced