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)
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.
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 authorityCkkWJtdPoq22CVdfWBhV5vo9MXNVaPXJAjrVmsRpYGC1
Its executable_dataaccount 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.
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:
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:
initialize a program account (first transaction)
upload the BPF bytecode to the program account’s data buffer(one or more transactions)
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:
buffer_data_len as u64,
config.signers.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).
Note: in this context loader_id cannot beBPFLoaderUpgradeab1e. The deployment steps forBPFLoaderUpgradeab1eare different fromBPFLoader2andBPFLoader, 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:
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 is founded by leading minds in the fields of blockchain security and software verification.