Un programa de venta de tokens es un contrato inteligente que vende un token específico, generalmente a cambio de un token nativo como SOL, a un precio fijo. La venta continúa hasta que se venda un suministro predefinido o el propietario actúe para finalizar la venta.
Nuestra implementación sigue este flujo:
- Un usuario deposita SOL basándose en nuestra tasa, por ejemplo, 1 SOL por 100 tokens.
- El programa almacena los SOL en una tesorería Program Derived Address (PDA), una cuenta controlada por el programa.
- Una vez recibido el SOL, los tokens se acuñan para el usuario.
- La venta continúa hasta alcanzar un límite de suministro predefinido.
- Un administrador puede retirar los SOL recaudados de la tesorería.
Creación del programa de Venta de Tokens
El programa de Solana que construiremos acuña tokens SPL directamente a los compradores, sin requerir que firmemos cada transacción como la autoridad de acuñación (mint authority). Este es el enfoque estándar; de lo contrario, el administrador tendría que aprobar manualmente cada compra, lo cual no es práctico.
Creación de las cuentas requeridas para la venta de tokens
Primero, crea un nuevo programa token_sale con Anchor y reemplaza el código base en programs/token_sale/src/lib.rs con el código a continuación.
El siguiente código importa las dependencias de nuestro programa y define una función initialize. La función hace lo siguiente:
- Configura la cuenta de administrador para controlar los retiros de la tesorería
- Crea una cuenta mint para el nuevo token que estamos vendiendo
- Crea una cuenta de tesorería para recaudar SOL de las compras de tokens
use anchor_lang::prelude::*;
use anchor_lang::system_program::{transfer, Transfer};
use anchor_spl::token::{mint_to, Mint, MintTo, Token, TokenAccount};
declare_id!("Gm8bFHtX3TapZDqA2tjviP1Qn1f8bLjTf8tbhFcgzcFs"); // REPLACE THIS WITH YOUR PROGRAM ID OR RUN `anchor sync`
// Tokens per SOL, i.e., 1 SOL == 100 of our tokens
const TOKENS_PER_SOL: u64 = 100;
// Max supply: 1000 tokens (with 9 decimals)
const SUPPLY_CAP: u64 = 1000e9 as u64;
#[program]
pub mod token_sale {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
// Set the admin key
ctx.accounts.admin_config.admin = ctx.accounts.admin.key();
Ok(())
}
}
En el código anterior, definimos constantes para nuestro programa de venta de tokens: TOKENS_PER_SOL = 100 y SUPPLY_CAP = 1000 (con 9 decimales).
A continuación, añade el struct de cuenta Initialize para nuestra función. Contiene las siguientes cuentas:
admin: La cuenta que paga las tarifas de transacción y sirve como administrador del programaadmin_config: Esta es una cuenta propiedad del programa que almacena la clave pública del administrador, para que más adelante, durante los retiros, podamos verificar que el firmante es el mismo administrador (similar a verificarmsg.sender == adminen Solidity, dondeadmines una variable de estado que almacena la clave pública de un administrador).mint: Una PDA mint autorreferencial que sirve tanto como el mint del token como su propia autoridad (explicaremos este concepto más adelante)treasury: Una PDA que retiene los SOL recaudados de las ventas de tokens- Finalmente, pasamos el Token Program y el System Program con los que interactuamos.
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub admin: Signer<'info>, // The transaction signer
#[account(
init,
payer = admin,
space = 8+AdminConfig::INIT_SPACE, // 8 is for the discriminator
)]
pub admin_config: Account<'info, AdminConfig>,
#[account(
init,
payer = admin,
seeds = [b"token_mint"],
bump,
mint::decimals = 9,
mint::authority = mint.key(),
)]
pub mint: Account<'info, Mint>,
/// CHECK: PDA for treasury
#[account(
seeds = [b"treasury"],
bump
)]
pub treasury: AccountInfo<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
// Stores the admin public key
#[account]
#[derive(InitSpace)] // This is a derive attribute macro provided by anchor, it calculates the space needed for the account and gives us access to AdminConfig::INIT_SPACE, as used above
pub struct AdminConfig {
pub admin: Pubkey,
}
#[error_code]
pub enum Errors {
#[msg("Max token supply limit reached")]
SupplyLimit,
#[msg("Math overflow")]
Overflow,
#[msg("Only admin can withdraw")]
UnauthorizedAccess,
#[msg("Not enough SOL in treasury")]
InsufficientFunds,
}
Entendiendo las cuentas del struct Initialize
Desglosaremos cada cuenta en el struct de cuentas Initialize y entenderemos su propósito:
admin config
admin_config: Esta cuenta contiene la clave pública del administrador (definida por el struct AdminConfig) y se utiliza para asegurar que solo el administrador pueda retirar SOL de la tesorería.


mint account
Esta es la cuenta mint para nuestro token SPL (el token que se está vendiendo). La creamos como una PDA para que el programa pueda firmar por ella más tarde (explicaremos esto más adelante en este artículo).
La cuenta no existe en la cadena (on-chain) hasta que invocamos initialize. En esa llamada, Anchor hará lo siguiente:
- Calcular la dirección de la PDA mint con la semilla
"token_mint"y el ID del programa - Crear la cuenta con
mint::decimals = 9(esto es lo que configuramos, como se muestra a continuación) - Establecer la autoridad del mint a sí mismo (
mint::authority = mint.key()). Esta parte es importante porque al hacer que la PDA sea su propia autoridad, solo nuestro programa, usando la misma semilla y bump, puede firmar instruccionesmint_to(nuevamente, explicaremos cómo funciona esto más adelante en este artículo).
treasury
Esta PDA se utiliza únicamente para retener los SOL (lamports) enviados por los usuarios durante la venta.

Ahora actualiza el archivo programs/token_sale/Cargo.toml con lo siguiente.
[package]
name = "token_sale"
version = "0.1.0"
description = "Created with Anchor"
edition = "2021"
[lib]
crate-type = ["cdylib", "lib"]
name = "token_sale"
[features]
default = []
cpi = ["no-entrypoint"]
no-entrypoint = []
no-idl = []
no-log-ix-name = []
idl-build = ["anchor-lang/idl-build", "anchor-spl/idl-build"] # "anchor-spl/idl-build" was added
[dependencies]
anchor-lang = "0.31.0"
anchor-spl = "0.31.0" # THIS WAS ADDED
Ahora actualiza la prueba (test) para nuestro programa.
Esta prueba es muy similar a lo que vimos en tutoriales anteriores. Simplemente llama a la instrucción initialize de nuestro programa con las cuentas requeridas y verifica las propiedades de la cuenta mint recién creada (el token).
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import {
createAssociatedTokenAccount,
getAccount,
getMint,
TOKEN_PROGRAM_ID
} from "@solana/spl-token";
import * as web3 from "@solana/web3.js";
import { assert } from "chai";
import { TokenSale } from "../target/types/token_sale";
describe("token_sale", async () => {
const provider = anchor.AnchorProvider.env();
anchor.setProvider(provider);
const program = anchor.workspace.TokenSale as Program<TokenSale>;
const connection = provider.connection;
const adminKp = provider.wallet.payer;
const buyer = adminKp; // Using the same keypair as both admin and buyer for testing
const TOKENS_PER_SOL = 100;
// Generate keypair for admin config account (will be passed as signer to authorize adminConfig account creation)
const adminConfigKp = web3.Keypair.generate();
let mint: anchor.web3.PublicKey;
let treasuryPda: anchor.web3.PublicKey;
let buyerAta: anchor.web3.PublicKey;
it("creates mint", async () => {
[mint] = web3.PublicKey.findProgramAddressSync(
[Buffer.from("token_mint")],
program.programId
);
[treasuryPda] = web3.PublicKey.findProgramAddressSync(
[Buffer.from("treasury")],
program.programId
);
const tx = await program.methods
.initialize()
.accounts({
admin: adminKp.publicKey,
adminConfig: adminConfigKp.publicKey,
mint: mint,
treasury: treasuryPda,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([adminKp, adminConfigKp])
.rpc();
console.log("initialize tx:", tx);
const mintInfo = await getMint(connection, mint);
assert.equal(mintInfo.mintAuthority.toBase58(), mint.toBase58());
assert.equal(Number(mintInfo.supply), 0);
assert.equal(mintInfo.decimals, 9);
});
});
Ejecuta npm install @solana/spl-token para actualizar la dependencia.
Ejecuta la prueba y pasará.

Comprando 100 tokens con 1 SOL
Hemos configurado nuestro programa de Venta de Tokens. Ahora añadiremos una función para acuñar nuevas unidades de tokens para la venta, de modo que los usuarios puedan comprar nuestro token.
El código hace lo siguiente:
- Calcula el número de tokens a acuñar basándose en la entrada de lamports.
- Verifica que no excedamos el suministro total.
- Transfiere SOL del comprador a la tesorería.
- Prepara las semillas de los firmantes (signer seeds) para que el programa pueda firmar en nombre de la PDA mint (más sobre esto después del bloque de código a continuación).
- Configura la instrucción mint con la cuenta mint como su propia autoridad.
- Crea un contexto CPI utilizando las semillas de los firmantes.
- Acuña los tokens hacia la cuenta de tokens del comprador.
pub fn mint(ctx: Context<MintTokens>, lamports: u64) -> Result<()> {
// Calculate how many tokens to mint (lamports * TOKENS_PER_SOL)
let amount = lamports
.checked_mul(TOKENS_PER_SOL)
.ok_or(Errors::Overflow)?; // If overflow, return error
// Ensure we don't exceed the max supply
let current_supply = ctx.accounts.mint.supply;
let new_supply = current_supply.checked_add(amount).ok_or(Errors::Overflow)?; // If overflow, return error
require!(new_supply <= SUPPLY_CAP, Errors::SupplyLimit);
// Send SOL to treasury
let transfer_instruction = Transfer {
from: ctx.accounts.buyer.to_account_info(),
to: ctx.accounts.treasury.to_account_info(),
};
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
transfer_instruction,
);
transfer(cpi_context, lamports)?;
// Create signer seeds for the mint PDA
let bump = ctx.bumps.mint;
let signer_seeds: &[&[&[u8]]] = &[&[b"token mint".as_ref(), &[bump]]];
// Setup mint instruction with mint as its own authority
let mint_to_instruction = MintTo {
mint: ctx.accounts.mint.to_account_info(),
to: ctx.accounts.buyer_token_account.to_account_info(),
authority: ctx.accounts.mint.to_account_info(),
};
// Create CPI context with `new_with_signer` - allows our token sale program to sign for the mint PDA. This works because the Solana runtime verifies that our program derived the mint PDA with these seeds and bump
// See here for more: <https://github.com/solana-foundation/developer-content/blob/main/content/guides/getstarted/how-to-cpi-with-signer.md>
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.token_program.to_account_info(),
mint_to_instruction,
signer_seeds,
);
mint_to(cpi_ctx, amount)?;
Ok(())
}
Por qué establecemos el mint como su propia autoridad
En la función mint() de arriba, puedes ver cómo usamos CpiContext::new_with_signer para acuñar tokens. Esto funciona debido a cómo configuramos la cuenta mint anteriormente. Recuerda que durante la inicialización, establecimos mint::authority = mint.key(), haciendo que la PDA mint sea su propia autoridad.
A continuación, explicamos por qué este patrón es esencial.
El desafío de las autoridades de acuñación de tokens
La acuñación de tokens requiere un control de autoridad. Normalmente, asignarías un par de claves (keypair) específico como la autoridad mint, y ese par de claves necesitaría firmar cada instrucción mint_to. Aunque esto proporciona seguridad, crea un problema práctico: tendríamos que gestionar este par de claves y asegurarnos de que esté disponible para firmar cada operación de acuñación.
Sin embargo, este enfoque no es adecuado para una venta automatizada de tokens. Los usuarios no pueden comprar tokens a menos que el par de claves de la autoridad esté disponible para firmar cada acuñación. Esto anula el propósito de crear un sistema sin permisos (permissionless).
Cómo las PDAs resuelven el problema de la autoridad
En lugar de usar un par de claves tradicional, hacemos que la PDA mint sea su propia autoridad. Esto parece confuso al principio: ¿cómo puede una PDA firmar transacciones si las PDAs no tienen claves privadas?
La solución radica en la firma de PDA. Cuando nuestro programa quiere acuñar tokens, utiliza CpiContext::new_with_signer con las semillas exactas utilizadas para crear la PDA mint ("token_mint" + bump). El runtime de Solana reconoce que nuestro programa derivó esta PDA con estas semillas específicas, por lo que permite que nuestro programa actúe como firmante para esa PDA.
Esto crea un patrón útil:
- La autoridad de mint es la dirección de la PDA (no un par de claves)
- Solo nuestro programa puede “firmar” por esta PDA usando las semillas correctas
- Nuestro programa actúa indirectamente como la autoridad de mint (porque posee la PDA mint) y puede acuñar tokens a pedido sin la necesidad de un firmante externo dedicado
- Nadie más puede acuñar estos tokens, incluso si descubren las semillas (aquí el token solo puede ser acuñado cuando un usuario lo compra a través de la función mint de nuestro programa)
¿Por qué no hacer que el administrador sea la autoridad de mint?
Podríamos haber establecido mint::authority = admin.key(), y hacer que el administrador sea la autoridad de mint. Pero como se mencionó anteriormente, entonces el administrador necesitaría firmar cada transacción de acuñación.
Ahora continuemos con el programa y agreguemos el struct de cuentas MintTokens.
MintTokens especifica las cuentas involucradas durante la venta/acuñación del token.
buyer: La cuenta que realiza la compra del token y también firma la transacción.mint: Nuestro token SPL a la venta.buyer_ata: La cuenta de tokens asociada (ATA) del comprador para recibir las unidades de token acuñadas.treasury: La cuenta que recibirá los SOL de la venta de tokens.- Las dos últimas cuentas, el Token program (para la acuñación) y el System program (para las transferencias de SOL), son los programas nativos con los que interactuamos.
#[derive(Accounts)]
pub struct MintTokens<'info> {
#[account(mut)]
pub buyer: Signer<'info>,
#[account(
mut,
seeds = [b"token mint"],
bump
)]
pub mint: Account<'info, Mint>,
#[account(
mut,
token::mint = mint,
token::authority = buyer,
)]
pub buyer_ata: Account<'info, TokenAccount>,
/// CHECK: PDA for treasury
#[account(
mut,
seeds = [b"treasury"],
bump
)]
pub treasury: AccountInfo<'info>,
pub token_program: Program<'info, Token>,
pub system_program: Program<'info, System>,
}
Añade el siguiente error personalizado al código del programa. Los errores aquí se utilizan en la función mint y en la próxima función que agregaremos.
#[error_code]
pub enum Errors {
#[msg("Max token supply limit reached")]
SupplyLimit,
#[msg("Math overflow")]
Overflow,
#[msg("Only admin can withdraw")]
UnauthorizedAccess,
#[msg("Not enough SOL in treasury")]
InsufficientFunds,
}
Ahora actualiza la prueba con el código a continuación.
Esta prueba hace lo siguiente:
- Crea una cuenta ATA del comprador para nuestro token
- Compra el equivalente a 1 SOL de nuestros tokens (100 tokens) llamando a la función mint de nuestro programa
- Verifica que la tesorería tenga la cantidad correcta de SOL de la operación
- Verifica que la ATA del comprador tenga la cantidad correcta de tokens acuñados después de la compra
it("buys tokens", async () => {
const solToSend = new anchor.BN(1e9); // 1 SOL
const expectedTokenAmount = Number(solToSend) * TOKENS_PER_SOL; // 1*100 tokens
const initialTreasuryBalance = await connection.getBalance(treasuryPda);
// Create buyer's ata
buyerAta = await createAssociatedTokenAccount(
provider.connection,
buyer,
mint,
buyer.publicKey,
undefined,
TOKEN_PROGRAM_ID
);
const buyerAtaInfo = await getAccount(connection, buyerAta, undefined, TOKEN_PROGRAM_ID);
const initialBuyerAtaBalance = Number(buyerAtaInfo.amount);
// Call our program's mint function to purchase tokens
const tx = await program.methods
.mint(solToSend)
.accounts({
buyer: buyer.publicKey,
mint: mint,
buyerAta: buyerAta,
treasury: treasuryPda,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("mint tx:", tx);
console.log("Sent", lamportsToSol(solToSend), "SOL, expecting", toDisplayAmount(expectedTokenAmount), "tokens");
const newTreasuryBalance = await connection.getBalance(treasuryPda);
assert.equal(
newTreasuryBalance - initialTreasuryBalance,
Number(solToSend),
"SOL was not correctly transferred to treasury"
);
const updatedBuyerAtaInfo = await getAccount(connection, buyerAta, undefined, TOKEN_PROGRAM_ID);
const newBuyerAtaBalance = Number(updatedBuyerAtaInfo.amount);
assert.equal(
newBuyerAtaBalance - initialBuyerAtaBalance,
expectedTokenAmount,
"Tokens were not correctly minted"
);
});
Ahora ejecuta la prueba, y pasará.

Asegurar que la venta de tokens finalice cuando se alcance el límite de suministro
Recuerda que el límite de suministro es de 1000 tokens y estamos acuñando 100 con 1 SOL en la prueba anterior. Intentaremos acuñar 920 tokens con 9.2 SOL para confirmar que la venta de tokens evita acuñar más del límite de suministro.
Añade el siguiente bloque de prueba.
Esta prueba verifica que la venta de tokens falle cuando intentamos comprar más del límite de 1000.
it("stops minting when supply cap is reached", async () => {
const mintInfo = await getMint(connection, mint, undefined, TOKEN_PROGRAM_ID);
const currentSupply = Number(mintInfo.supply);
const SUPPLY_CAP = toRawTokenAmount(1000);
const remainingSupply = SUPPLY_CAP - currentSupply;
console.log(`Current supply: ${toDisplayAmount(currentSupply)} tokens, Remaining: ${toDisplayAmount(remainingSupply)} tokens`);
const tokensToMint = remainingSupply + toRawTokenAmount(20);
const solToSend = new anchor.BN(Math.ceil(tokensToMint / TOKENS_PER_SOL));
console.log(`Trying to mint ${toDisplayAmount(tokensToMint)} tokens by sending ${lamportsToSol(solToSend)} SOL`);
try {
await program.methods
.mint(solToSend)
.accounts({
buyer: buyer.publicKey,
mint: mint,
buyerAta: buyerAta,
treasury: treasuryPda,
tokenProgram: TOKEN_PROGRAM_ID,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
assert.fail("Minting succeeded but should have failed due to supply cap");
} catch (error) {
console.log("Expected error:", error.toString().substring(0, 150) + "...");
assert.include(
error.toString(),
"SupplyLimit",
"Expected supply limit error not received"
);
console.log("Supply cap limit correctly enforced");
}
});
Ejecuta la prueba y debería pasar.

Retirando el saldo de SOL de la cuenta de tesorería
Hasta ahora, hemos configurado pruebas para inicializar nuestro programa y comprar nuestros tokens. Ahora, añadiremos código para que el administrador del programa de venta de tokens (nuestra cuenta) retire los SOL recaudados de la cuenta de tesorería y probaremos esta funcionalidad.
Añade la función withdraw_funds a continuación al programa de venta de tokens. Hace lo siguiente:
- Comprueba que la tesorería tenga suficiente saldo para el retiro.
- Prepara las semillas de los firmantes para que el programa pueda firmar en nombre de la PDA de tesorería.
- Configura un contexto CPI para llamar a la instrucción transfer del System Program.
- Usa
CpiContext::new_with_signerpara permitir que el programa firme por la tesorería. - Transfiere SOL (lamports) de la tesorería a la billetera del administrador.
Nota: Esto utiliza el transfer del System Program de Solana, no el de tokens SPL, porque estamos transfiriendo SOL (el token nativo) en lugar de un token SPL. Las transferencias de SPL requieren interactuar con cuentas de tokens, mientras que las transferencias de SOL son manejadas directamente por el System Program.
pub fn withdraw_funds(ctx: Context<WithdrawFunds>, amount: u64) -> Result<()> {
// Check balance
let treasury_balance = ctx.accounts.treasury.lamports();
require!(treasury_balance >= amount, Errors::InsufficientFunds);
// Create signer seeds for PDA
let bump = ctx.bumps.treasury;
let signer_seeds: &[&[&[u8]]] = &[&[b"treasury".as_ref(), &[bump]]];
// Prepare the CPI context to System Program::transfer
// DO NOT CONFUSE THIS WITH SPL TOKEN TRANSFER
let transfer_instruction = Transfer {
from: ctx.accounts.treasury.to_account_info(),
to: ctx.accounts.admin.to_account_info(),
};
let cpi_ctx = CpiContext::new_with_signer(
ctx.accounts.system_program.to_account_info(),
transfer_instruction,
signer_seeds,
);
transfer(cpi_ctx, amount)?; // DO NOT CONFUSE THIS WITH SPL TOKEN TRANSFER
Ok(())
}
Ahora añade el struct de cuenta WithdrawFunds, utilizado por la función withdraw_funds. Contiene las siguientes cuentas:
admin: El firmante de la transacción, y nuestra cuenta de administrador.admin_config: La cuenta que almacena la clave pública del administrador, con una restricción para verificar que el firmante esté autorizado. La pasamos porque necesitamos comprobar que el firmante actual coincida con la clave de administrador almacenada durante la inicialización.treasury: La PDA mutable de tesorería que contiene los SOL que se van a retirar.system_program: El System Program para manejar las transferencias de SOL.
#[derive(Accounts)]
pub struct WithdrawFunds<'info> {
#[account(mut)]
pub admin: Signer<'info>,
#[account(
constraint = admin_config.admin == admin.key() @ Errors::UnauthorizedAccess // Ensure the signer is authorized
)]
pub admin_config: Account<'info, AdminConfig>,
/// CHECK: PDA for treasury
#[account(
mut,
seeds = [b"treasury"],
bump
)]
pub treasury: AccountInfo<'info>,
pub system_program: Program<'info, System>,
}
A continuación, actualiza la prueba.
Este bloque de prueba retira la mitad del saldo de la tesorería desde la PDA de la tesorería hacia el par de claves del administrador y verifica que el saldo del administrador haya aumentado en la cantidad retirada, mientras que el saldo de la tesorería haya disminuido en la misma cantidad.
it("allows the admin to withdraw funds from treasury", async () => {
const initialAdminBalance = await connection.getBalance(adminKp.publicKey);
const initialTreasuryBalance = await connection.getBalance(treasuryPda);
console.log("Initial treasury balance:", lamportsToSol(initialTreasuryBalance), "SOL");
console.log("Initial admin balance:", lamportsToSol(initialAdminBalance), "SOL");
assert.isAbove(
initialTreasuryBalance,
0,
"Treasury should have funds from previous tests"
);
const amountToWithdraw = new anchor.BN(Math.floor(initialTreasuryBalance / 2)); // Withdraw half of the treasury balance
try {
const tx = await program.methods
.withdrawFunds(amountToWithdraw)
.accounts({
admin: adminKp.publicKey,
adminConfig: adminConfigKp.publicKey,
treasury: treasuryPda,
systemProgram: anchor.web3.SystemProgram.programId,
})
.rpc();
console.log("withdrawFunds tx:", tx);
const newAdminBalance = await connection.getBalance(adminKp.publicKey);
const newTreasuryBalance = await connection.getBalance(treasuryPda);
console.log("New treasury balance:", lamportsToSol(newTreasuryBalance), "SOL");
console.log("New admin balance:", lamportsToSol(newAdminBalance), "SOL");
// assert that the treasury balance decreased by the amount we withdrew, which is half of the initial treasury balance
assert.approximately(
initialTreasuryBalance - newTreasuryBalance,
Number(amountToWithdraw),
10000,
"Treasury balance did not decrease by approximately the correct amount"
);
// assert that the admin balance increased by the amount we withdrew
assert.isTrue(
newAdminBalance > initialAdminBalance,
"Admin balance did not increase after withdrawal"
);
} catch (error) {
console.error("Error in withdraw test:", error);
throw error;
}
});
Ejecuta la prueba. Podemos ver que el saldo de SOL de la cuenta de tesorería se reduce y nuestro saldo aumenta.

Probar que los no administradores no pueden retirar de la tesorería
Añade el siguiente bloque de prueba para confirmar que las cuentas no autorizadas no pueden retirar SOL del saldo de la cuenta de tesorería.
Una cuenta no autorizada aquí, es cualquier cuenta cuya clave pública no coincida con lo que previamente habíamos almacenado en la cuenta adminConfig durante la inicialización.
it("prevents non-admins from withdrawing funds", async () => {
const nonAdminKeypair = web3.Keypair.generate();
const amountToWithdraw = new anchor.BN(1e8);
try {
await program.methods
.withdrawFunds(amountToWithdraw)
.accounts({
admin: nonAdminKeypair.publicKey,
adminConfig: adminConfigKp.publicKey,
treasury: treasuryPda,
systemProgram: anchor.web3.SystemProgram.programId,
})
.signers([nonAdminKeypair])
.rpc();
assert.fail("Non-admin was able to withdraw funds, but should be prohibited");
} catch (error) {
console.log("Expected error occurred:", error.toString().substring(0, 150) + "...");
assert.include(
error.toString(),
"UnauthorizedAccess",
"Expected unauthorized access error not received"
);
console.log("Non-admin withdrawal was correctly rejected");
}
});
Ejecuta la prueba.

A partir de los registros (logs), podemos ver que Anchor arroja un error cuando intentamos retirar fondos con una cuenta no autorizada.
Resumen
En este tutorial, hemos demostrado un caso de uso práctico de un Token SPL al construir un programa de Venta de Tokens (con un límite de suministro) que permite a los usuarios intercambiar SOL por nuestro token SPL a una tasa fija.
El programa utiliza dos PDAs clave: una PDA mint autorreferencial y una PDA de tesorería. Establecemos el mint como su propia autoridad con mint::authority = mint.key() durante la inicialización, eliminando así la necesidad de una cuenta separada como autoridad de mint. Este patrón garantiza que cualquier persona pueda comprar/acuñar el token a través de nuestro programa, sin que tengamos que autorizar la acuñación cada vez.
Al utilizar CpiContext::new_with_signer con semillas derivadas correctamente, nuestro programa puede acuñar tokens para los compradores y actuar indirectamente como la autoridad de mint.
Este artículo es parte de una serie de tutoriales sobre Solana.