Las transferencias de SPL mueven saldos de tokens entre cuentas de token de la misma emisión. Los delegados permiten a un tercero transferir hasta una cantidad aprobada en nombre del propietario; este es el patrón detrás de los enrutadores DEX y los programas de depósito en garantía.
# Las cantidades de la CLI son cantidades de UI (tokens completos), nunca unidades base brutasspl-token transfer <MINT> <UI_AMOUNT> <RECIPIENT> --fund-recipient# approve toma la CUENTA DE TOKEN DE ORIGEN, no la emisiónspl-token approve <TOKEN_ACCOUNT> <UI_AMOUNT> <DELEGATE_PUBKEY>
use anchor_spl::token::{self, TransferChecked, Approve};token::transfer_checked(cpi_ctx, amount, decimals)?;token::approve(cpi_ctx, amount)?;
Cuándo recurrir a esto:
Pagos de usuario a usuario en tokens fungibles
Aprobar que un programa de intercambio retire tokens
Crear depósitos en garantía que mueven fondos al liquidarse
# Alice envía 2.5 tokens - la CLI toma la cantidad de UI y la escala por los decimales de la emisiónspl-token transfer "$MINT" 2.5 <BOB_WALLET> --fund-recipient# Alice aprueba al enrutador para gastar hasta 1 token de SU cuenta de token.# El argumento 1 es la cuenta de token que se delega, no la emisión.ALICE_ATA=$(spl-token address --token "$MINT" --verbose | awk '/Associated token address/{print $NF}')spl-token approve "$ALICE_ATA" 1 <ROUTER_DELEGATE_PUBKEY>
use anchor_lang::prelude::*;use anchor_spl::token::{self, Approve, Mint, Token, TokenAccount, TransferChecked};#[derive(Accounts)]pub struct TransferPayment<'info> { pub mint: Account<'info, Mint>, #[account(mut, token::mint = mint, token::authority = authority)] pub from: Account<'info, TokenAccount>, #[account(mut, token::mint = mint)] pub to: Account<'info, TokenAccount>, pub authority: Signer<'info>, pub token_program: Program<'info, Token>,}pub fn transfer_payment(ctx: Context<TransferPayment>, amount: u64) -> Result<()> { let decimals = ctx.accounts.mint.decimals; let cpi = CpiContext::new( ctx.accounts.token_program.to_account_info(), TransferChecked { mint: ctx.accounts.mint.to_account_info(), from: ctx.accounts.from.to_account_info(), to: ctx.accounts.to.to_account_info(), authority: ctx.accounts.authority.to_account_info(), }, ); token::transfer_checked(cpi, amount, decimals)?; Ok(())}#[derive(Accounts)]pub struct ApproveRouter<'info> { #[account(mut, token::authority = owner)] pub owner_token: Account<'info, TokenAccount>, /// CHECK: la clave pública a la que se otorgan los derechos de delegado; el programa Token la almacena textualmente. pub delegate: UncheckedAccount<'info>, pub owner: Signer<'info>, pub token_program: Program<'info, Token>,}pub fn approve_router(ctx: Context<ApproveRouter>, amount: u64) -> Result<()> { let cpi = CpiContext::new( ctx.accounts.token_program.to_account_info(), Approve { to: ctx.accounts.owner_token.to_account_info(), delegate: ctx.accounts.delegate.to_account_info(), authority: ctx.accounts.owner.to_account_info(), }, ); token::approve(cpi, amount)?; Ok(())}
Lo que esto demuestra:
token::mint = mint fija ambos lados al mismo activo antes de cualquier CPI
transfer_checked pasa la emisión y verifica los decimales en la cadena; transfer simple está obsoleto
El delegado es una cuenta real pasada en la instrucción; nunca reutilices otra cuenta para esa ranura
El propietario firma la transferencia; el delegado puede firmar después de approve; revoke restablece la cantidad delegada a cero
import { createSolanaRpc, address } from "@solana/kit";const rpc = createSolanaRpc("https://api.devnet.solana.com");// Construir instrucciones de transferencia a través de los ayudantes de token de @solana-program/// Siempre derivar el ATA del destinatario; financiar si falta
Desajuste de emisión - la transferencia entre diferentes emisiones falla. Solución: validar from.mint == to.mint en la cadena y en los clientes.
Cantidad delegada insuficiente - la transferencia del delegado excede la aprobación. Solución: volver a aprobar o dividir entre transacciones.
Aprobaciones ilimitadas obsoletas - riesgo de seguridad en DeFi. Solución: aprobar un gasto exacto; revocar después del intercambio.
Transferencia a una dirección que no es ATA - funciona pero confunde a los usuarios. Solución: derivar y mostrar direcciones ATA.
Cuenta de origen congelada - todas las transferencias bloqueadas. Solución: verificar el campo state antes de hacer promesas en la UX.
transfer sin verificar obsoleto - spl_token::instruction::transfer y anchor_spl::token::{transfer, Transfer} están obsoletos, y Token-2022 rechaza la variante sin verificar en las emisiones con tarifa de transferencia. Solución: usar transfer_checked / TransferChecked en todas partes.
Cantidades de la CLI leídas como unidades brutas - spl-token transfer <MINT> 2500000 envía 2,500,000 tokens, no 2.5. Solución: la CLI toma cantidades de UI; solo los datos de instrucción toman unidades base brutas.
approve / revoke dada la emisión - ambos toman la dirección de la cuenta de token, no la emisión. Solución: derivar primero el ATA (spl-token address --token <MINT>).
Versiones de la pila: Esta página fue escrita para Agave 4.1.1, Solana CLI 3.0.10, Anchor 0.32.1, anchor-lang 0.32.1, Rust 1.91.1, @solana/kit 7.0.0, Surfpool 0.12.0, y LiteSVM 0.6.x.
Revisado por Chris St. John·Última actualización: 19 jul 2026