Solana Internals Part 2: How Is a Solana Program Deployed and Upgraded

January 16, 2022

What happens inside Solana when you deploy a smart contract to the Solana Mainnet? Can a Solana program be modified or closed? How to upgrade a Solana program? Who is authorized to change a Solana program?

This article focuses on the upgradability of Solana programs and highlights some intricacies.

Here is a list of take-away notes:

  • Solana programs can be modified and upgraded (by default)
  • The BPFLoaderUpgradeab1e loader is the owner of every upgradable Solana program account
  • Solana program data (i.e., the smart contract code) is stored in a separate buffer account) and it has a maximal size limit.
  • The upgrade authority has super power and must be securely managed
  • Users of an upgradable Solana program should be cautious to avoid Rug pull
  • Updates to Solana programs can introduce new security vulnerabilities and must be audited

Solana program account

Every user-deployed smart contract on Solana is associated with a Solana program account, which has a number of important attributes: program_id, owner , program_data , authority , etc.

Figure 1. The program account info of jet-v1 (link)
Note that these information attributes are not stored in a single executable program. The program data and authority are actually stored in a separate account (programdata) derived from the program_id. See this article for further details (credit: starry.sol and tmpjail)

The program_id is the address of the Solana program. We use jet-v1 (program_id JPv1rCqrhagNNmJVM5J1he7msQ5ybtvE1nNuHpDHMNU) as an example. Figure 1 shows a screen shot from explorer.solana.com.

There are several things to note:

  • Its Solana program is upgradable
  • Its owner is BPFLoaderUpgradeab1e (BPFLoaderUpgradeab1e11111111111111111111111)
  • It has upgrade authority CkkWJtdPoq22CVdfWBhV5vo9MXNVaPXJAjrVmsRpYGC1
  • Its executable_data account address is 45X4uzRnRvZoiG6X5ho6V8FJUU7HVxDApuyoL8mwgiP9

The executable_data account contains the actual BPF bytecode of jet-v1, and its data length is 1827341 bytes (>1.8MB), as shown in Figure 2 below.

Figure 2. The executable_data account info of jet-v1 (link)

To show detailed info of jet-v1’s Solana program account in the terminal, run:

$ solana program show JPv1rCqrhagNNmJVM5J1he7msQ5ybtvE1nNuHpDHMNU

To show detailed info of jet-v1’s Solana executable_data account in the terminal, run:

$ solana account 45X4uzRnRvZoiG6X5ho6V8FJUU7HVxDApuyoL8mwgiP9

On deploying a Solana program

According to Solana documentation, to deploy a Solana program, e.g., jet-v1, simply run the following command, which uploads the compiled BPF bytecode (i.e., an ELF shared object jet.so) to the Solana cluster:

$ solana program deploy /git/SOLANA/jet-v1/target/bpfel-unknown-unknown/release/jet.so

However, behind solana program deploy , deploying a Solana program is fairly complicated, and it can take many transactions:

  1. initialize a program account (first transaction)
  2. upload the BPF bytecode to the program account’s data buffer(one or more transactions)
  3. finalize the deployment by marking the program account executable (final transaction)

Step 1: initialize a program account

Step 1 is done by submitting a transaction with a system_instruction::create_account instruction:

system_instruction::create_account(
    &config.signers[0].pubkey(),
    buffer_pubkey,
    minimum_balance,
    buffer_data_len as u64,
    loader_id,
)
  • config.signers[0].pubkey() is the program_id the to-be-created smart contract. It can be specified by --program-id <PROGRAM_ID>, otherwise from a keypair loaded at a default location.
  • buffer_pubkey is the address of the data buffer, i.e., the executable_data account. It can be either specified by --buffer <BUFFER_SIGNER >or generated automatically by create_ephemeral_keypair() :
// Create ephemeral keypair to use for Buffer account, if not provided
let (words, mnemonic, buffer_keypair) = create_ephemeral_keypair()?;
  • minimum_balance is the minimal number of lamports to transfer to the program account for rent exemption. It is computed by minimum_balance = rpc_client.get_minimum_balance_for_rent_exemption(program_data.len())
  • buffer_data_len is the data length, i.e., number of bytes of the BPF bytecode. There is a limit on the maximal number of instructions in a Solana BPF program:
pub const PROG_MAX_INSNS: usize = 65_536;
  • loader_id specifies the owner of the program account. It can be either BPFLoader2 (the latest Solana BPF loader), BPFLoader (the original and now deprecated Solana BPF loader).
The owner of a program account can also be BPFLoaderUpgradeab1e . In fact, by default all user-deployed Solana programs are deployed with BPFLoaderUpgradeab1e and hence are upgradable.

Note: in this context loader_id cannot be BPFLoaderUpgradeab1e. The deployment steps for BPFLoaderUpgradeab1e are different from BPFLoader2 and BPFLoader, in that all steps happen in a single transaction. See more detail in Sec. “Deploying an upgradeable Solana program”. (credit: BlockBandit suggested discussing BPFLoaderUpgradeab1e here to avoid confusion)

Step 2: upload the BPF bytecode

Step 2 is done by first verifying the BPF bytecode (off-chain)

let program_data = read_and_verify_elf(program_location)?

and then submitting one or more transactions to upload the bytecode to the data buffer account via the LoaderInstruction::Write instruction:

loader_instruction::write(buffer_pubkey, loader_id, offset, bytes)

Note that a transaction on Solana has a maximal size: solana_sdk::packet:: PACKET_DATA_SIZE(less than 1280 bytes, determined by IPv6 packet limit).

PACKET_DATA_SIZE limit from Solana packet.rs

For BPF bytecode with data length larger than PACKET_DATA_SIZE , it has to be split into multiple small chunks and submit a transaction for each chunk.

The parameters offset and bytes to loader_instruction::write specify the offset of a chunk and the maximal chunk size, respectively.

For a typical Solana program, this step can take hundreds or more thousands. For instance, deploying jet-v1 takes ~1500 transactions (less than a second on Solana on average).

Step 3: finalize the deployment

This step submits a transaction with the LoaderInstruction::Finalize instruction :

loader_instruction::finalize(buffer_pubkey, loader_id)

The instruction will invoke the bpf_loader (with loader_id) in the Solana runtime, which sets the program’s executable flag to true:

program.try_account_ref_mut()?.set_executable(true)

Deploying an upgradeable Solana program

By default, all user-deployed Solana programs are deployed with the BPFLoaderUpgradeab1e (i.e., bpf_loader_upgradeable::id()) loader.

The deployment submits a single transaction with the UpgradeableLoaderInstruction::DeployWithMaxDataLen instruction:

The instruction will invoke the BPFLoaderUpgradeab1e loader in the Solana runtime, which creates a ProgramData account to store the buffer data, and finally sets the program executable.

To allocate space for future upgrade, the max_data_len of the ProgramData account is set to twice the size of the BPF bytecode.

On upgrading a Solana program

Solana programs can be upgraded by default. That is, it is possible to redeploy a new shared object (BPF bytecode) to the same Solana program account.

This can be done by the program’s upgrade authority, which can be specified during the original deployment by --upgrade-authority <UPGRADE_AUTHORITY_SIGNER>, otherwise it is set to be the default configured keypair.

$ solana program deploy --upgrade-authority

The program’s upgrade authority can also be changed to a new_authority by the UpgradeableLoaderInstruction::SetAuthority instruction (in a transaction signed by the current upgrade authority).

When a Solana program is redeployed by the upgrade authority, it first creates a new data buffer account for the new BPF bytecode, and then invokes the UpgradeableLoaderInstruction::Upgrade instruction to update the ProgramData account to store the new buffer data.

Setting a Solana program permanently immutable

Solana also provides an option --final to use BPFLoader2 at the deployment time (when --final is provided and the program will not be upgradeable).

If any changes are required to the finalized program (features, patches, etc…) the new program must be deployed to a new program ID.

On closing a Solana program

Both Solana program and buffer accounts can be closed by their upgrade authority, and their lamport balances will be transferred to a recipient’s account.

To close a program account:

$ solana program close 

Internally, it invokes the UpgradeableLoaderInstruction::Close instruction, which updates the account lamports and sets the state of close_account to UpgradeableLoaderState::Uninitialized

Final note: cautious on upgrading a Solana program

The upgradability of smart contracts is a distinctive feature of Solana compared to Ethereum. This design makes Solana applications easier to incorporate new features. However, there are a few caveats:

1. the upgrade authority has super power

The upgrade authority must be securely managed. If the upgrade authority becomes evil or the private key is obtained by an attacker, then the Solana program can be close directly or changed at anytime to lock or steal users’ fund.

2. users of an upgradable Solana program should be notified

If you are using an upgradable program, find a way to be notified whenever the program is upgraded to avoid malicious behaviors such as Rug pull.

3. updates to Solana programs must be cautious

Even tiny incremental program changes can introduce new security vulnerabilities, and must be carefully tested and audited.

As an example, recently, a critical vulnerability was discovered in jet-v1 due to an ad hoc upgrade to include a new feature. Luckily, the vulnerability was first found and reported by a white hat. See detail in this tweet.


Soteria audit

Soteria is founded by leading minds in the fields of blockchain security and software verification.

We are pleased to provide audit services to high-impact Dapps on Solana. Please visit soteria.dev or email contact@soteria.dev