El propietario de una cuenta en Solana puede reducir el saldo de SOL, escribir datos en la cuenta y cambiar el propietario.
A continuación se presenta un resumen de la propiedad de cuentas en Solana:
- El
system programes propietario de las wallets y las cuentas keypair a las que no se les ha asignado la propiedad a un programa (inicializadas). - El BPFLoader es propietario de los programas.
- Un programa es propietario de las PDAs de Solana. También puede ser propietario de cuentas keypair si la propiedad ha sido transferida al programa (esto es lo que ocurre durante la inicialización).
Ahora examinaremos las implicaciones de estos hechos.
El system program es propietario de las cuentas keypair
Para ilustrar esto, veamos la dirección de nuestra wallet de Solana utilizando la Solana CLI e inspeccionemos sus metadatos:

Observe que el propietario (owner) no es nuestra dirección, sino una cuenta con la dirección 111...111. Este es el system program, el mismo system program que mueve los SOL como vimos en tutoriales anteriores.
Solo el propietario de una cuenta tiene la capacidad de modificar los datos en ella
Esto incluye reducir los datos de lamports (no es necesario ser el propietario para incrementar los datos de lamports de otra cuenta, como veremos más adelante).
Aunque usted es el “propietario” de su wallet en un sentido metafísico, no tiene directamente la capacidad de escribir datos en ella o reducir el saldo de lamports porque, desde la perspectiva del runtime de Solana, usted no es el propietario.
La razón por la que puede gastar los SOL de su wallet es porque posee la clave privada que generó la dirección, o clave pública. Cuando el system program reconoce que usted ha producido una firma válida para la clave pública, reconocerá su solicitud para gastar los lamports en la cuenta como legítima, y luego los gastará de acuerdo con sus instrucciones.
Sin embargo, el system program no ofrece un mecanismo para que un firmante (signer) escriba datos directamente en la cuenta.
La cuenta mostrada en el ejemplo anterior es una cuenta keypair, o lo que podríamos considerar una “wallet regular de Solana”. El system program es el propietario de las cuentas keypair.
Las PDAs y las cuentas keypair inicializadas por programas son propiedad del programa
La razón por la que los programas pueden escribir en PDAs o cuentas keypair que fueron creadas fuera del programa pero inicializadas por el programa, es porque el programa es el propietario de las mismas.
Exploraremos la inicialización más de cerca cuando hablemos del ataque de reinicialización, pero por ahora, la conclusión importante es que inicializar una cuenta cambia el propietario de la cuenta del system program al programa que la inicializa.
Para ilustrar esto, considere el siguiente programa que inicializa una PDA y una cuenta keypair. La prueba en Typescript imprimirá en consola el propietario antes y después de la transacción de inicialización.
Si intentamos determinar el propietario de una dirección que no existe, obtenemos un null.
Aquí está el código en Rust:
use anchor_lang::prelude::*;
declare_id!("C2ZKJPhNiCM6CqTneGUXJoE4o6YhMzNUes3q5WNcH3un");
#[program]
pub mod owner {
use super::*;
pub fn initialize_keypair(ctx: Context<InitializeKeypair>) -> Result<()> {
Ok(())
}
pub fn initialize_pda(ctx: Context<InitializePda>) -> Result<()> {
Ok(())
}
}
#[derive(Accounts)]
pub struct InitializeKeypair<'info> {
#[account(init, payer = signer, space = 8)]
keypair: Account<'info, Keypair>,
#[account(mut)]
signer: Signer<'info>,
system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct InitializePda<'info> {
#[account(init, payer = signer, space = 8, seeds = [], bump)]
pda: Account<'info, Pda>,
#[account(mut)]
signer: Signer<'info>,
system_program: Program<'info, System>,
}
#[account]
pub struct Keypair();
#[account]
pub struct Pda();
Aquí está el código en Typescript:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Owner } from "../target/types/owner";
async function airdropSol(publicKey, amount) {
let airdropTx = await anchor.getProvider().connection.requestAirdrop(publicKey, amount * anchor.web3.LAMPORTS_PER_SOL);
await confirmTransaction(airdropTx);
}
async function confirmTransaction(tx) {
const latestBlockHash = await anchor.getProvider().connection.getLatestBlockhash();
await anchor.getProvider().connection.confirmTransaction({
blockhash: latestBlockHash.blockhash,
lastValidBlockHeight: latestBlockHash.lastValidBlockHeight,
signature: tx,
});
}
describe("owner", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Owner as Program<Owner>;
it("Is initialized!", async () => {
console.log("program address", program.programId.toBase58());
const seeds = []
const [pda, bump_] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
console.log("owner of pda before initialize:",
await anchor.getProvider().connection.getAccountInfo(pda));
await program.methods.initializePda().accounts({pda: pda}).rpc();
console.log("owner of pda after initialize:",
(await anchor.getProvider().connection.getAccountInfo(pda)).owner.toBase58());
let keypair = anchor.web3.Keypair.generate();
console.log("owner of keypair before airdrop:",
await anchor.getProvider().connection.getAccountInfo(keypair.publicKey));
await airdropSol(keypair.publicKey, 1); // 1 SOL
console.log("owner of keypair after airdrop:",
(await anchor.getProvider().connection.getAccountInfo(keypair.publicKey)).owner.toBase58());
await program.methods.initializeKeypair()
.accounts({keypair: keypair.publicKey})
.signers([keypair]) // the signer must be the keypair
.rpc();
console.log("owner of keypair after initialize:",
(await anchor.getProvider().connection.getAccountInfo(keypair.publicKey)).owner.toBase58());
});
});
Las pruebas funcionan de la siguiente manera:
- Predice la dirección de la PDA y consulta al propietario. Obtiene
null. - Llama a
initializePday luego consulta al propietario. Obtiene la dirección del programa. - Genera una cuenta keypair y consulta al propietario. Obtiene
null. - Envía un airdrop de SOL a la cuenta keypair. Ahora el propietario es el system program, al igual que en una wallet normal.
- Llama a
initializeKeypairy luego consulta al propietario. Obtiene la dirección del programa.
La captura de pantalla con el resultado de la prueba se muestra a continuación:

Así es como el programa puede escribir datos en las cuentas: es su propietario. Durante la inicialización, el programa toma posesión de la cuenta.
Ejercicio: Modifique la prueba para imprimir la dirección del keypair y la PDA. Luego use la Solana CLI para inspeccionar quién es el propietario de esas cuentas. Debería coincidir con lo que imprime la prueba. Asegúrese de que el solana-test-validator se esté ejecutando en segundo plano para que pueda usar la CLI.
El BPFLoaderUpgradeable es propietario de los programas
Usemos la Solana CLI para determinar el propietario de nuestro programa:

La wallet que desplegó el programa no es su propietaria. La razón por la que los programas de Solana pueden ser actualizados por la wallet que los despliega es porque el BPFLoaderUpgradeable tiene la capacidad de escribir un nuevo bytecode en el programa, y solo aceptará un nuevo bytecode de una dirección predesignada: la dirección que desplegó originalmente el programa.
Cuando desplegamos (o actualizamos) un programa, en realidad estamos haciendo una llamada al programa BPFLoaderUpgradeable, como se puede ver en los registros (logs):
Signature: 2zBBEPWsMvf8t7wkNEDqfHJKw83aBMgwGi3G9uZ6m9qG9t4kjJA2wFEP84dkKCjiCdbh54xeEDYFeDcNS7FkyLEw
Status: Ok
Log Messages:
Program 11111111111111111111111111111111 invoke [1]
Program 11111111111111111111111111111111 success
Program BPFLoaderUpgradeab1e11111111111111111111111 invoke [1]
Program 11111111111111111111111111111111 invoke [2]
Program 11111111111111111111111111111111 success
Deployed program C2ZKJPhNiCM6CqTneGUXJoE4o6YhMzNUes3q5WNcH3un
Program BPFLoaderUpgradeab1e11111111111111111111111 success
Transaction executed in slot 34:
Los programas pueden transferir la propiedad de las cuentas que poseen
Esta es una característica que probablemente no usará muy a menudo, pero aquí está el código para hacerlo.
Rust:
use anchor_lang::prelude::*;
use std::mem::size_of;
use anchor_lang::system_program;
declare_id!("Hxj38tktrD7YcSvKRxVrYQfxptkZd7NVbmrRKvLxznyA");
#[program]
pub mod change_owner {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
Ok(())
}
pub fn change_owner(ctx: Context<ChangeOwner>) -> Result<()> {
let account_info = &mut ctx.accounts.my_storage.to_account_info();
// assign is the function to transfer ownership
account_info.assign(&system_program::ID);
// we must erase all the data in the account or the transfer will fail
let res = account_info.realloc(0, false);
if !res.is_ok() {
return err!(Err::ReallocFailed);
}
Ok(())
}
}
#[error_code]
pub enum Err {
#[msg("realloc failed")]
ReallocFailed,
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(init,
payer = signer,
space=size_of::<MyStorage>() + 8,
seeds = [],
bump)]
pub my_storage: Account<'info, MyStorage>,
#[account(mut)]
pub signer: Signer<'info>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct ChangeOwner<'info> {
#[account(mut)]
pub my_storage: Account<'info, MyStorage>,
}
#[account]
pub struct MyStorage {
x: u64,
}
Typescript:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { ChangeOwner } from "../target/types/change_owner";
import privateKey from '/Users/jeffreyscholz/.config/solana/id.json';
describe("change_owner", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.ChangeOwner as Program<ChangeOwner>;
it("Is initialized!", async () => {
const deployer = anchor.web3.Keypair.fromSecretKey(Uint8Array.from(privateKey));
const seeds = []
const [myStorage, _bump] = anchor.web3.PublicKey.findProgramAddressSync(seeds, program.programId);
console.log("the storage account address is", myStorage.toBase58());
await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
await program.methods.changeOwner().accounts({myStorage: myStorage}).rpc();
// after the ownership has been transferred
// the account can still be initialized again
await program.methods.initialize().accounts({myStorage: myStorage}).rpc();
});
});
Aquí hay algunas cosas sobre las que queremos llamar la atención:
- Después de transferir la cuenta, los datos deben ser borrados en la misma transacción. De lo contrario, podríamos insertar datos en cuentas que son propiedad de otros programas. Este es el código
account_info.realloc(0, false);. Elfalsesignifica que no se llenen los datos con ceros, pero no hay diferencia porque ya no hay datos. - Transferir la propiedad de la cuenta no la elimina permanentemente; se puede volver a inicializar como muestran las pruebas.
Ahora que entendemos claramente que los programas son propietarios de las PDAs y las cuentas keypair inicializadas por ellos, lo interesante y útil que podemos hacer es transferir SOL fuera de ellas.
Transfiriendo SOL fuera de una PDA: Ejemplo de Crowdfund
A continuación mostramos el código para una aplicación básica de crowdfunding. La función de interés es la función withdraw, donde el programa transfiere lamports fuera de la PDA hacia el usuario que retira los fondos.
use anchor_lang::prelude::*;
use anchor_lang::system_program;
use std::mem::size_of;
use std::str::FromStr;
declare_id!("BkthFL8LV2V2MxVgQtA9tT5goeeJhUdxRPahzavqHPFZ");
#[program]
pub mod crowdfund {
use super::*;
pub fn initialize(ctx: Context<Initialize>) -> Result<()> {
let initialized_pda = &mut ctx.accounts.pda;
Ok(())
}
pub fn donate(ctx: Context<Donate>, amount: u64) -> Result<()> {
let cpi_context = CpiContext::new(
ctx.accounts.system_program.to_account_info(),
system_program::Transfer {
from: ctx.accounts.signer.to_account_info().clone(),
to: ctx.accounts.pda.to_account_info().clone(),
},
);
system_program::transfer(cpi_context, amount)?;
Ok(())
}
pub fn withdraw(ctx: Context<Withdraw>, amount: u64) -> Result<()> {
ctx.accounts.pda.sub_lamports(amount)?;
ctx.accounts.signer.add_lamports(amount)?;
// in anchor 0.28 or lower, use the following syntax:
// **ctx.accounts.pda.to_account_info().try_borrow_mut_lamports()? -= amount;
// **ctx.accounts.signer.to_account_info().try_borrow_mut_lamports()? += amount;
Ok(())
}
}
#[derive(Accounts)]
pub struct Initialize<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(init, payer = signer, space=size_of::<Pda>() + 8, seeds=[], bump)]
pub pda: Account<'info, Pda>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Donate<'info> {
#[account(mut)]
pub signer: Signer<'info>,
#[account(mut)]
pub pda: Account<'info, Pda>,
pub system_program: Program<'info, System>,
}
#[derive(Accounts)]
pub struct Withdraw<'info> {
#[account(mut, address = Pubkey::from_str("5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj").unwrap())]
pub signer: Signer<'info>,
#[account(mut)]
pub pda: Account<'info, Pda>,
}
#[account]
pub struct Pda {}
Debido a que el programa es el propietario de la PDA, puede deducir directamente el saldo de lamports de la cuenta.
Cuando transferimos SOL como parte de una transacción de wallet normal, no deducimos el saldo de lamports directamente ya que no somos los propietarios de la cuenta. El system program es el propietario de la wallet y deducirá el saldo de lamports si observa una firma válida en una transacción que solicite hacerlo.
En este caso, el programa es el propietario de la PDA y, por lo tanto, puede deducir directamente lamports de ella.
Algunos otros elementos en el código a los que vale la pena prestar atención:
- Escribimos directamente en el código (hardcoded) quién puede retirar fondos de la PDA utilizando la restricción
#[account(mut, address = Pubkey::from_str("5jmigjgt77kAfKsHri3MHpMMFPo6UuiAMF19VdDfrrTj").unwrap())]. Esto verifica que la dirección de esa cuenta coincida con la de la cadena de texto (string). Para que este código funcione, también necesitamos importaruse std::str::FromStr;. Para probar este código, cambie la dirección en el string por la suya usandosolana address. - Con Anchor 0.29, podemos usar la sintaxis
ctx.accounts.pda.sub_lamports(amount)?;yctx.accounts.signer.add_lamports(amount)?;. Para versiones anteriores de Anchor, utilicectx.accounts.pda.to_account_info().try_borrow_mut_lamports()? -= amount;yctx.accounts.signer.to_account_info().try_borrow_mut_lamports()? += amount;. - No necesita ser el propietario de la cuenta a la que está transfiriendo lamports.
Aquí está el código en Typescript que lo acompaña:
import * as anchor from "@coral-xyz/anchor";
import { Program } from "@coral-xyz/anchor";
import { Crowdfund } from "../target/types/crowdfund";
describe("crowdfund", () => {
// Configure the client to use the local cluster.
anchor.setProvider(anchor.AnchorProvider.env());
const program = anchor.workspace.Crowdfund as Program<Crowdfund>;
it("Is initialized!", async () => {
const programId = await program.account.pda.programId;
let seeds = [];
let pdaAccount = anchor.web3.PublicKey.findProgramAddressSync(seeds, programId)[0];
const tx = await program.methods.initialize().accounts({
pda: pdaAccount
}).rpc();
// transfer 2 SOL
const tx2 = await program.methods.donate(new anchor.BN(2_000_000_000)).accounts({
pda: pdaAccount
}).rpc();
console.log("lamport balance of pdaAccount",
await anchor.getProvider().connection.getBalance(pdaAccount));
// transfer back 1 SOL
// the signer is the permitted address
await program.methods.withdraw(new anchor.BN(1_000_000_000)).accounts({
pda: pdaAccount
}).rpc();
console.log("lamport balance of pdaAccount",
await anchor.getProvider().connection.getBalance(pdaAccount));
});
});
Ejercicio: intente agregar más lamports a la dirección receptora de los que retira de la PDA. Es decir, cambie el código a lo siguiente:
ctx.accounts.pda.sub_lamports(amount)?;
// sneak in an extra lamport
ctx.accounts.signer.add_lamports(amount + 1)?;
El runtime debería bloquearle.
Tenga en cuenta que retirar el saldo de lamports por debajo del umbral de exención de renta (rent-exempt threshold) dará como resultado el cierre de la cuenta. Si hay datos en la cuenta, estos serán borrados. Por lo tanto, los programas deben rastrear cuánto SOL se requiere para la exención de renta antes de retirar SOL, a menos que no les importe que la cuenta sea borrada.
Aprenda más con RareSkills
Consulte nuestro tutorial de Solana para ver la lista completa de temas.
Publicado originalmente el 7 de marzo de 2024