En el framework Anchor para Solana, close es lo opuesto a init (inicializar una cuenta en Anchor): reduce el saldo de lamports a cero, enviando los lamports a una dirección de destino, y cambia el propietario de la cuenta para que sea el programa del sistema.
Aquí hay un ejemplo del uso de la instrucción close en Rust:
use anchor_lang::prelude::*;
use std::mem::size_of;
declare_id!("8gaSDFr5cVy2BkLrWfSX9MCtPX9N4gmXDvTVm7RS6DYK");
#[program]
pub mod close_program {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
pub fn delete(ctx: Context<Delete>) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init, payer = signer, space = size_of::<ThePda>() + 8, seeds = [], bump)]
pub the_pda: Account<'info, ThePda>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Delete<'info> {
#[account(mut, close = signer, )]
pub the_pda: Account<'info, ThePda>,
#[account(mut)]
pub signer: Signer<'info>,
}
#[account]
pub struct ThePda {
pub x: u32,
}
Solana devuelve la renta por cerrar cuentas
La macro close = signer especifica que el firmante en la transacción recibirá la renta que se reservó para pagar el almacenamiento (aunque, por supuesto, se podría especificar otra dirección). Esto es similar a cómo selfdestruct en Ethereum (antes de la actualización Dencun) reembolsaba a los usuarios por liberar espacio. La cantidad de SOL que se puede ganar al cerrar una cuenta es proporcional al tamaño de la cuenta.
Aquí está el código en Typescript para llamar a initialize seguido de delete:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { CloseProgram } from "../target/types/close_program";
import { assert } from "chai";
describe("close_program", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.CloseProgram as Program<CloseProgram>;
it("Is initialized!", async () => {
let [thePda, _bump] = anchor.web3.PublicKey.findProgramAddressSync([], program.programId);
await program.methods.initialize().accounts({thePda: thePda}).rpc();
await program.methods.delete().accounts({thePda: thePda}).rpc();
let account = await program.account.thePda.fetchNullable(thePda);
console.log(account)
});
});
La instrucción close = signer indica que se envíen los lamports de la renta al firmante, pero puedes especificar la dirección que prefieras.
La construcción anterior permite que cualquiera cierre la cuenta, ¡probablemente quieras agregar algún tipo de control de acceso en una aplicación real!
Las cuentas pueden ser inicializadas después de ser cerradas
Si llamas a initialize después de cerrar una cuenta, esta se inicializará de nuevo. Por supuesto, la renta, que fue canjeada anteriormente, debe pagarse nuevamente.
Ejercicio: agrega otra llamada a initialize en la prueba unitaria para ver cómo se ejecuta correctamente. Ten en cuenta que la cuenta ya no es nula al final de la prueba.
¿Qué hace close bajo el capó?
Si observamos el código fuente del comando close en Anchor, podemos ver que realiza las operaciones que describimos anteriormente:

Muchos ejemplos de Anchorlang están desactualizados
En la versión 0.25 de Anchor, la secuencia de close era diferente.
De manera similar a la implementación actual, primero enviaba todos los lamports a la dirección de destino.
Sin embargo, en lugar de borrar los datos y transferirlos al programa del sistema, close escribía una secuencia especial de 8 bytes llamada CLOSE_ACCOUNT_DISCRIMINATOR. (código original):
/// The discriminator anchor uses to mark an account as closed.
pub const CLOSED_ACCOUNT_DISCRIMINATOR: [u8; 8] = [255, 255, 255, 255, 255, 255, 255, 255];
Eventualmente, el runtime borraba la cuenta porque tenía cero lamports.
¿Qué es el account discriminator en Anchor?
Cuando Anchor inicializa una cuenta, calcula el discriminator y lo almacena en los primeros 8 bytes de la cuenta. El account discriminator corresponde a los primeros 8 bytes del SHA256 del identificador de Rust del struct.
Cuando un usuario solicita al programa que cargue una cuenta a través de pub the_pda: Account<'info, ThePda>, el programa calculará los primeros 8 bytes del SHA256 del identificador ThePda. Luego cargará los datos de ThePda y comparará el discriminator almacenado allí con el que calculó. Si no coinciden, Anchor no deserializará la cuenta.
La intención aquí es evitar que un atacante cree una cuenta maliciosa que se deserialice produciendo resultados inesperados al ser analizada “a través del struct equivocado”.
Por qué Anchor solía establecer el account discriminator en [255, ..., 255]
Al establecer el account discriminator con todos sus valores en unos, Anchor siempre rechazará la deserialización de la cuenta porque no coincidirá con ninguno de los account discriminators.
La razón para escribir el account discriminator con todos sus valores en unos era evitar que un atacante enviara SOL directamente a la cuenta antes de que el runtime la borrara. Bajo esta circunstancia, el programa “pensaba” que había cerrado el programa, pero el atacante lo “revivía”. Si el antiguo account discriminator todavía estaba allí, entonces los datos que se creían eliminados volverían a ser leídos.
Por qué ya no es necesario establecer el account discriminator en [255, …, 255]
Al cambiar en su lugar la propiedad al programa del sistema, revivir la cuenta no dará como resultado que el programa repentinamente vuelva a ser “dueño” de la cuenta; el programa del sistema es dueño de la cuenta revivida y el atacante habrá desperdiciado SOL.
Para devolverle la propiedad al programa, debe ser inicializado explícitamente de nuevo; no puede ser revivido a través de un canal lateral como enviar SOL para evitar que el runtime lo borre.
Cerrar un programa a través de la CLI
Para cerrar un programa, a diferencia de una cuenta de su propiedad, podemos usar la línea de comandos:
solana program close <address> --bypass warning
La advertencia es que, una vez que se cierra un programa, no se puede volver a crear un programa con la misma dirección. Aquí hay una secuencia de comandos de shell que ilustra el cierre de una cuenta:

Aquí está la secuencia de comandos de la captura de pantalla anterior:
- Primero desplegamos el programa.
- Cerramos el programa sin la bandera
--bypass-warningy la herramienta nos da una advertencia de que el programa no se puede volver a desplegar. - Cerramos el programa con la bandera; el programa se cierra y recibimos 2.918 SOL como reembolso por cerrar la cuenta.
- Intentamos desplegar de nuevo y fallamos porque un programa cerrado no se puede volver a desplegar.
Aprende más con RareSkills
Para continuar aprendiendo sobre el desarrollo en Solana, por favor consulta nuestro curso de Solana. Para otros temas de blockchain, consulta nuestro bootcamp de blockchain.
Publicado originalmente el 12 de marzo de 2024