Solana Internals Part 2
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)
Program Account
─────────────────────────────────────────────────────────────────
Address JPv1rCqrhagNNmJVM5J1he7msQ5ybtvE1nNuHpDHMNU
Balance (SOL) 0.00114144
Executable Yes
Executable Data 45X4uzRnRvZoiG6X5ho6V8FJUU7HVxDApuyoL8mwgiP9
Upgradeable Yes
Last Deployed Slot 112,773,963
Upgrade Authority CkkWJtdPoq22CVdfWBhV5vo9MXNVaPXJAjrVmsRpYGC1
> 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_dataaccountaddress is45X4uzRnRvZoiG6X5ho6V8FJUU7HVxDApuyoL8mwgiP9
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)
Program Executable Data Account
─────────────────────────────────────────────────────────────────
Address 45X4uzRnRvZoiG6X5ho6V8FJUU7HVxDApuyoL8mwgiP9
Balance (SOL) 12.71918424
Data (Bytes) 1827341
Upgradeable Yes
Last Deployed Slot 112,773,963
Upgrade Authority CkkWJtdPoq22CVdfWBhV5vo9MXNVaPXJAjrVmsRpYGC1
To show detailed info of jet-v1’s Solana program account in the terminal, run:
$ solana program show JPv1rCqrhagNNmJVM5J1he7msQ5ybtvE1nNuHpDHMNU
Program Id: JPv1rCqrhagNNmJVM5J1he7msQ5ybtvE1nNuHpDHMNU
Owner: BPFLoaderUpgradeab1e11111111111111111111111
ProgramData Address: 45X4uzRnRvZoiG6X5ho6V8FJUU7HVxDApuyoL8mwgiP9
Authority: CkkWJtdPoq22CVdfWBhV5vo9MXNVaPXJAjrVmsRpYGC1
Last Deployed In Slot: 112773963
Data Length: 1827296 (0x1be1e0) bytes
Balance: 12.71918424 SOL
To show detailed info of jet-v1’s Solana executable_data account in the terminal, run:
$ solana account 45X4uzRnRvZoiG6X5ho6V8FJUU7HVxDApuyoL8mwgiP9
Public Key: 45X4uzRnRvZoiG6X5ho6V8FJUU7HVxDApuyoL8mwgiP9
Balance: 12.71918424 SOL
Owner: BPFLoaderUpgradeab1e11111111111111111111111
Executable: false
Rent Epoch: 269
Length: 1827341 (0x1be20d) bytes
0000: 03 00 00 00 4b cb b8 06 00 00 00 00 01 ae a5 b8 ....K...........
0010: fc 52 41 24 82 b6 93 d1 e8 55 86 c8 a2 07 16 0f .RA$.....U......
0020: bf c8 84 e0 be 8d e0 d5 e0 a7 df bb e2 7f 45 4c ..............EL
0030: 46 02 01 01 00 00 00 00 00 00 00 00 00 03 00 f7 F...............
0040: 00 01 00 00 00 50 a2 0a 00 00 00 00 00 40 00 00 .....P.......@..
0050: 00 00 00 00 00 00 00 00 08 df 0e 00 00 00 00 00
0060: 00 40 00 38 00 03 00 40 00 0c 00 0b 00 01 00 00 .@.8...@........
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:
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:
-
config.signers\[0\].pubkey()is theprogram_idthe to-be-created smart contract. It can be specified by\--program-id <PROGRAM_ID>, otherwise from a keypair loaded at a default location. -
buffer_pubkeyis the address of the data buffer, i.e., the executable_data account. It can be either specified by\--buffer <BUFFER_SIGNER >or generated automatically bycreate_ephemeral_keypair(): -
minimum_balanceis the minimal number of lamports to transfer to the program account for rent exemption. It is computed byminimum_balance = rpc_client.get_minimum_balance_for_rent_exemption(program_data.len()) -
buffer_data_lenis 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: -
loader_idspecifies 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)
and then submitting one or more transactions to upload the bytecode to the data buffer account via the LoaderInstruction::Write instruction:
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
/// Maximum over-the-wire size of a Transaction
/// 1280 is IPv6 minimum MTU
/// 40 bytes is the size of the IPv6 header
/// 8 bytes is the size of the fragment header
pub const PACKET_DATA_SIZE: usize = 1280 - 40 - 8;
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 :
let message = if loader_id == &bpf_loader_upgradeable::id() {
Message::new_with_blockhash(
instructions: &bpf_loader_upgradeable::deploy_with_max_program_len(
payer_address: &config.signers[0].pubkey(),
program_address: &program_signers[0].pubkey(),
buffer_address: buffer_pubkey,
upgrade_authority_address: &program_signers[1].pubkey(),
program_lamports: rpc_client.get_minimum_balance_for_rent_exemption(
data_len: UpgradeableLoaderState::program_len()?,
)?,
max_data_len: programdata_len,
)?,
payer: Some(&config.signers[0].pubkey()),
The instruction will invoke the bpf_loader (with loader_id) in the Solana runtime, which sets the program’s executable flag to 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.
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.
// Update the ProgramData account, record the upgraded data, and zero
// the rest
programdata.set_state(&UpgradeableLoaderState::ProgramData {
slot: clock.slot,
upgrade_authority_address: Some(*authority.unsigned_key()),
})?;
programdata.try_account_ref_mut()?.data_as_mut_slice()
[programdata_data_offset..programdata_data_offset + buffer_data_len]: [u8]
.copy_from_slice(src: &buffer.try_account_ref()?.data()[buffer_data_offset..]);
programdata.try_account_ref_mut()?.data_as_mut_slice()
[programdata_data_offset + buffer_data_len..]: [u8]
.fill(0);
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:
instructions: &[bpf_loader_upgradeable::close_any(
close_address: account_pubkey,
recipient_address: recipient_pubkey,
authority_address: Some(&authority_signer.pubkey()),
program_address: program_pubkey,
)],
Internally, it invokes the UpgradeableLoaderInstruction::Close instruction, which updates the account lamports and sets the state of close_account to UpgradeableLoaderState::Uninitialized
recipient_account: &KeyedAccount
.try_account_ref_mut()?: RefMut<AccountSharedData>
.checked_add_lamports(close_account.lamports()?)?;
close_account.try_account_ref_mut()?.set_lamports(0);
close_account.set_state(&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.
@JetProtocol: During an ad hoc upgrade to our mainnet program we introduced a bug, thanks to the sharp eye of a community member we were able to rapidly deploy a patch with no loss of funds.
About sec3 (Formerly Soteria)
sec3 is a security research firm that prepares Solana projects for millions of users. sec3’s Launch Audit is a rigorous, researcher-led code examination that investigates and certifies mainnet-grade smart contracts; sec3’s continuous auditing software platform, X-ray, integrates with GitHub to progressively scan pull requests, helping projects fortify code before deployment; and sec3’s post-deployment security solution, WatchTower, ensures funds stay safe. sec3 is building technology-based scalable solutions for Web3 projects to ensure protocols stay safe as they scale.
To learn more about sec3, please visit https://www.sec3.dev