What is ERC3643 on Solana?


ERC-3643 هو معيار رمزي لإيثيريوم مصمم خصيصًا للامتثال التنظيمي (اعرف عميلك، مكافحة غسل الأموال، إلخ) وإصدار الرموز المصرح بها. مثل ERC-20، يحدد الوظائف الأساسية للرموز القابلة للاستبدال ولكنه يضمن إمكانية تطبيق متطلبات الامتثال الأساسية على السلسلة.

Key Characteristics

  • Eligibility Verification: The token contract verifies whether both the holder and the recipient have completed KYC (or AML) checks before allowing a transfer.
  • Regulatory Compliance: Implementing features such as whitelists/blacklists, investor-count or jurisdictional limits, and other rules at the smart contract level.
  • Enforcement & Control: If an unauthorized transfer is attempted, the contract blocks it, and issuers can forcibly transfer or burn tokens to fulfill real-world regulatory obligations.

Token Extensions

Solana's Token Extensions (also referred to as Token-2022) are a set of enhancements to Solana's native SPL Token program, introducing built-in support for advanced token features without requiring new standalone contracts. The motivation for Token Extensions was to provide a more flexible, extensible token standard that caters to complex and regulated use cases, all while avoiding the fragmentation seen in ecosystems with many bespoke token contracts.

Key Features (You can check other features here)

  • Transfer Hooks: يسمح بتشغيل منطق مخصص على السلسلة (فرض الإتاوات، فحوصات الامتثال في الوقت الفعلي) عند نقل الرمز.
  • Confidential Transfers: Hides the transfer amount from the public, preserving user privacy while allowing authorized parties (e.g., auditors) to access the data if needed.
  • Transfer Fees: Automatically deducts a specified fee (or tax) during each transfer and routes it to a designated recipient address.
  • Permanent Delegates: Grants a special authority unlimited privileges to override or reclaim tokens, enabling administrative actions like forced transfers and burns.

What Does a Token Extension Program for ERC3643 Look Like?

Below is a Solidity-style pseudocode that illustrates how key features of Solana's Token Extensions, such as KYC enforcement, account freezing, transfer hooks, and permanent delegate functionality could be conceptually represented in an Ethereum-like contract.

Additionally, this code builds on SPL-20 (the standard Solana token program, SPL-Token) and layers the Token Extension functionality on top of it using Solidity syntax. If you'd like to review the baseline Solana token program (SPL-Token), please refer to this link.

javascript
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);
}

KYC Whitelisting

The isKYCApproved mapping ensures that only KYC-verified users can send or receive tokens. Before a transfer proceeds, both the sender and recipient must be flagged as approved. This mimics Solana's capability to restrict transfers to verified identities via identity-gated token accounts or transfer hooks configured at the mint level.

Freeze Enforcement

The frozen mapping lets the compliance authority lock any account from sending or receiving tokens. In Solana, token accounts can be frozen either by a freeze authority, a native feature that provides immediate enforcement without contract logic.

Transfer Hook

The transferHookProgram variable represents an external contract that implements custom logic. It is called during each token transfer, allowing dynamic enforcement like:

  • Rejecting transfers over a certain size
  • Charging royalties
  • Checking external allowlists or oracles

يؤدي امتداد Transfer Hook في سولانا هذه الوظيفة على مستوى البروتوكول، مما يمنح المطورين تحكمًا قويًا على السلسلة في عمليات النقل دون الحاجة إلى نشر عقد مخصص.

Permanent Delegate (Override Authority)

The complianceAuthority has the power to forcibly transfer tokens between any two addresses. This is comparable to Solana's Permanent Delegate feature, which allows authorized entities to bypass user permissions in regulated contexts as required, for example, to comply with legal orders or reclaim tokens during fraud mitigation.

How to do ERC3643 on Solana

1. Conceptual Map

ERC-3643 FeatureToken-2022 Counterpart / Actor
KYC whitelistCustom transfer-hook program
Account freezefreeze_authority
Force / claw-back transferpermanent_delegate::transfer
On-transfer custom logicTransfer-hook callback (execute)
Compliance adminPDA signer or multisig that owns the mint

2. End-to-End Call Flow (simplified)

latex
User → Token‑2022 (transfer) ↘
                               Transfer Hook (KYC check) → OK / ERR
                               ↘
                        Token‑2022 (actual balance change)

تحتوي معاملة سولانا الواحدة على كل من تعليمة الرمز واستدعاء الخطاف. إذا أرجع الخطاف خطأ، يتم التراجع عن المعاملة بأكملها - وهو ما يعكس التنفيذ على السلسلة في ERC-3643.

3. Minimal Transfer-Hook Program

This program adds a KYC gate to any Token 2022 mint. During setup the one-time init_meta instruction creates the extra_account_meta_list PDA whose fixed seeds bind it to the mint, stores the whitelist PDA inside that list, and immediately inserts the initializer (payer) into whitelist.allowed. That means the mint authority can move the first tokens without an extra whitelist update call. Later, whenever someone calls transfer_checked, the Token 2022 program CPI-invokes the hook's execute function, passing the five fixed accounts (source token, mint, destination token, owner, meta list) plus the forwarded whitelist PDA. The hook checks that the owners of both token accounts appear in whitelist.allowed; if either is missing it raises SrcNotAllowed or DstNotAllowed, rolling back the whole transaction, otherwise it returns Ok(()) and the transfer finalizes. All other administrative powers such as freezing, claw back, and supply changes remain with the mint's native authorities, so roughly ninety lines of code deliver ERC 3643 style compliance without breaking standard SPL tooling or wallet UX.

rust
// 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,
}

4. CLI Quickstart (Solana v1.18+, spl-token-cli v3.0.0+, Anchor v0.30+)

أولاً تختار محفظة ستعمل كمسؤول امتثال وتحفظ pubkey الخاص بها في COMP_AUTH، ثم تحسب مسبقًا PDA للقائمة البيضاء التي ستخزنها في الخطاف وتسميها WL_PDA. بمجرد جاهزية المفاتيح تقوم بتجميع kyc_hook باستخدام anchor build ونشره، مع تدوين program-id المطبوع حديثًا كـ HOOK_ID. بعد ذلك تنشئ عملة Token-2022 جديدة تمامًا تشير إلى هذا الخطاف عبر تمرير --transfer-hook $HOOK_ID، مع تسمية محفظة المسؤول أيضًا كسلطة تجميد واسترداد؛ تطبع سولانا MINT_ADDRESS الناتج. نظرًا لأن Token-2022 يحتاج إلى معرفة الحسابات الإضافية التي سيمررها إلى الخطاف، فإنك تقوم فورًا بتشغيل anchor run init-meta، والذي يستدعي مهيئ الخطاف ويخزن WL_PDA في قائمة extra-account-meta الخاصة بالعملة. تتم هذه الخطوة مرة واحدة فقط. الآن العملة مجهزة لاعرف عميلك، حتى تتمكن من سك الإمداد الأولي لنفسك بحرية؛ لا يتم استدعاء الخطاف في عمليات السك. أخيرًا تقوم بنقل الرموز إلى محفظة أخرى: في تلك اللحظة يستدعي Token-2022 الخطاف، الذي يفحص كلاً من المرسل والمستقبل مقابل القائمة البيضاء على السلسلة. إذا كان كلاهما مدرجًا يتم النقل؛ إذا كان أي منهما مفقودًا يطرح الخطاف خطأ ويتم التراجع عن المعاملة بأكملها، مما يمنحك امتثالًا بأسلوب ERC-3643 في معاملة سولانا واحدة.

bash
# 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
EVM TO SVM

Start building on Solana

Node storing all data and participating in consensus

  • Ethereum: Archive Node
  • Solana: [n/a]

Node storing some data and participating in consensus

  • Ethereum: Full Node
  • Solana: Consensus Node

Node storing some data and not participating in consensus

  • Ethereum: Light Node
  • Solana: RPC Node

تدار بواسطة

© 2026 مؤسسة سولانا.
جميع الحقوق محفوظة.
تواصل معنا
What is ERC3643 on Solana? | Solana