
Session Setup (The Intent Message)
A session first starts with an Intent Message, a payload that the user signs off-chain using their main wallet. It acts as the "configuration file" for the session, explicitly defining the boundaries of authority. The Message struct is parsed on-chain and used to enforce the following constraints:
// programs/session-manager/src/message.rs
pub struct Message {
pub version: Version,
pub chain_id: String,
pub domain: Domain,
pub expires: DateTime<FixedOffset>,
pub session_key: Pubkey,
pub tokens: Tokens,
pub extra: HashMap<String, String>,
}
pub struct Domain(String);
pub enum Tokens {
Specific(Vec<(SymbolOrMint, UiTokenAmount)>),
All,
}

Starting a Session
In order to ensure that a Session is valid, instruction introspection is used. The transaction needed to start a session requires two instructions called:
- First, an instruction to the Ed25519 program to verify the intent signature validity.
- Second, a start_session instruction to the Session Manager program.
Instruction 1: Signature Verification (Ed25519)
The first instruction invokes the Solana native Ed25519 Program with the user's public key, the signed Intent Message (serialized), and the signature as an input value. Because transactions are atomic, if this verification fails, the entire transaction reverts. This guarantees that the subsequent start_session instruction can never be reached unless the user's signature has been cryptographically proven valid.
Instruction 2: Session Initialization
The second instruction calls start_session on the Session Manager Program. This instruction does not take the signature as an argument. Instead, it looks backward at the previous instruction of the current transaction using introspection.
This mechanism ensures that a Session Account cannot be created unless the user has cryptographically signed the exact configuration parameters loaded by the program (i.e. the signed message matches the provided session configuration).
The start_session instruction performs the following operations:
Step 1: Load and Verify the Intent
let Intent {
signer,
message: Message {
version,
chain_id,
domain,
expires,
session_key,
tokens,
extra
}
} = Intent::load(&ctx.accounts.sysvar_instructions)
impl<...> Intent<M> {
pub fn load(sysvar_instructions: &AccountInfo<'_>) -> Result<...> {
get_instruction_relative(-1, sysvar_instructions)?.try_into()
}
}
The instruction introspects the sysvar_instructions account to find the Ed25519 verification instruction at index -1. This retrieves the user's signature and the signed message, binding them cryptographically to this transaction.
Step 2: Validate Contextual Integrity
ctx.accounts.check_session_key(message.session_key)?; // Match target key
ctx.accounts.check_chain_id(message.chain_id)?; // Verify chain ID
// ... additional validation checksThe program validates that:
- The session_key from the message matches the session account being initialized
- The chain_id matches the on-chain ChainId account
- The domain is properly registered in the Domain Registry
- The expiration timestamp is valid
Step 3: Apply Token Approvals
let authorized_tokens_with_mints = match tokens {
Tokens::Specific(tokens) => {
// Convert remaining accounts to pending approvals
let pending_approvals = convert_remaning_accounts_..._pending_approvals(
ctx.remaining_accounts,
tokens,
&signer,
)?;
// Extract authorized mints
let authorized_tokens_with_mints = AuthorizedTokensWithMints::Specific(
pending_approvals.iter().map(|p| p.mint()).collect(),
);
// Execute token approvals using SESSION_SETTER_SEED PDA
ctx.accounts.approve_tokens(pending_approvals, ctx.bumps.session_setter)?;
authorized_tokens_with_mints
}
Tokens::All => AuthorizedTokensWithMints::All,
};
For sessions with Tokens::Specific, the program:
- Converts the remaining accounts into pending approval operations
- Executes token approvals using a special PDA derived from SESSION_SETTER_SEED
- This PDA is recognized by the Token Program, allowing the Session Manager to set delegated amounts on user token accounts
For sessions with Tokens::All, no upfront approvals are set, as the Token Program will bypass delegation checks during runtime.
Step 4: Initialize the Session Account
let session = Session {
sponsor: ctx.accounts.sponsor.key(),
major,
session_info: SessionInfo::V4(V4::Active(ActiveSessionInfoWithDomainHash {
domain_hash: domain.get_domain_hash(),
active_session_info: ActiveSessionInfo {
user: signer,
authorized_programs: AuthorizedPrograms::Specific(program_domains),
authorized_tokens: authorized_tokens_with_mints,
extra: extra.into(),
expiration,
},
})),
};
ctx.accounts.initialize_and_store_session(&session)?;
The program creates a Session account containing:
- sponsor: The account that pays for the session's rent (typically the user or an application)
- user: The original signer who created the session
- domain_hash: Hash of the domain for efficient program authorization checks
- authorized_programs: List of program IDs the session can interact with
- authorized_tokens: Either specific mints with approved amounts or All for unlimited
- expiration: Unix timestamp when the session expires
- extra: Additional metadata provided by the user
The Session account is then written to the blockchain and assigned ownership to the Session Manager Program.
Runtime Checks
Once initialized, the Session Account can act as a signer on programs that integrate Fogo Sessions. Unlike a standard keypair, its authority is conditional. The Fogo Chain ecosystem enforces these conditions (expiration timestamp, authorized programs, ...) at the protocol level during every interaction.
Token Operations (modified Token Program)
The standard SPL Token program has been modified to natively recognize Session Accounts. Whenever a transaction involves a token transfer signed by a session key, the program triggers a Session hook to validate the operation before continuing the transfer:
- Expiry status: The hook deserializes the session account to ensure the current block timestamp is within the expires window and that the session state is not Revoked
- Token Account owner: it ensure that the user of the session matches the Token Account owner.
- Authorized programs: go through the signers list and verify it is listed as an authorized program in the active session, and that the authorized program signed with the expected PDA.
// packages/sessions-sdk-rs/src/session/token_program.rs
pub fn get_token_permissions_checked(
&self,
user: &Pubkey,
signers: &[AccountInfo],
) -> Result<AuthorizedTokens, SessionError> {
self.check_is_live_and_unrevoked()?;
self.check_user(user)?;
self.check_authorized_program_signer(signers)?;
Ok(self.authorized_tokens()?.clone())
}
Finally, the spending limits are checked:
- If the session tokens scope is Specific, it falls back to standard delegated_amount checks.
- If the session tokens scope is All, the Token Program bypasses the standard delegation limit, allowing the transfer to proceed based on the user's Intent signature.
Non-token Operations
For interactions with external applications that do not require a token transfer, the security is enforced via the Fogo Sessions SDK used to extract users from a Session account:
// packages/sessions-sdk-rs/src/session/mod.rs
pub fn extract_user_from_signer_or_session(
info: &AccountInfo,
program_id: &Pubkey,
) -> Result<Pubkey, SessionError> {
if !info.is_signer {
return Err(SessionError::MissingRequiredSignature);
}
// ensure the Session account is owned by the Session Manager Program
if info.owner == &SESSION_MANAGER_ID {
let session = Session::try_deserialize(...)?;
session.get_user_checked(program_id)
} else {
Ok(*info.key)
}
}
pub fn get_user_checked(&self, program_id: &Pubkey) -> Result<...> {
self.check_is_live_and_unrevoked()?;
self.check_authorized_program(program_id)?;
Ok(*self.user()?)
}
The function:
- Ensure that the Session account is a signer, preventing the unauthorized use of Sessions
- Verify that the Session account is owned by the Session Manager Program, otherwise it would be possible to deploy a fake Session account
- Finally check that the Session is still active and that the caller program is authorized
Session lifecycle
Expiration
Sessions naturally expire when the block timestamp exceeds the expires field. No on-chain transaction is required to "close" the session for security purposes; it simply ceases to function. The Token Program's JIT validation will reject any transaction attempting to use an expired session.
Explicit Revocation
A user can forcefully invalidate a session before it expires by calling the revoke_session instruction. Once revoked, the session state transitions from Active to Revoked. The Token Program hook checks this state before every transfer, ensuring immediate termination of access.
Closing a Session
After a session has expired or been revoked, the session account can be fully closed to reclaim the rent. The close_session instruction performs a complete cleanup:
- Liveness check: The session must not be live (either expired or revoked). The !session.is_live()? constraint ensures this.
- Token delegation cleanup: For sessions with Tokens::Specific, the instruction revokes all token approvals using once again the SESSION_SETTER_SEED PDA to authorize the revocation.
- Account closure: this is managed using the close Anchor constraint
Notes:
- Sessions with Tokens::All don't set upfront delegations, so no token revocations are needed
- Legacy session versions (V1/V2) with Tokens::Specific cannot be closed due to potential inconsistencies in stored mint information
- The close operation is only available to the sponsor who originally funded the session's creation
Integration in Ignition
Ignition is a liquid staking protocol for the FOGO native token, developed by Tempest Labs, and forked from the original SPL stake-pool program. The protocol was modified to integrate Fogo Sessions and wFOGO wrapping/unwrapping, a session key mechanism allowing users to execute protocol interactions without per-transaction signature requirements while maintaining defined security boundaries.
This section will show how Ignition implements session support in its native Solana program, demonstrating how the security guarantees described earlier are leveraged in practice.
You can also find an Anchor example from the Fogo Foundation in the fogo-session repository.
Session-Specific Instructions
Ignition implements dedicated instruction variants for session-based operations. Compared to standard instructions, these session variants require an additional account: the program_signer PDA.
As explained in a previous section, the modified Token Program enforces Program Binding by verifying that an authorized program's PDA has signed the transaction. This prevents a malicious application from hijacking a user's session to drain their tokens. Ignition must therefore include this PDA in any instruction that performs token transfers:
pub fn deposit_wsol_with_session(
program_id: &Pubkey,
stake_pool: &Pubkey,
withdraw_authority: &Pubkey,
reserve_stake: &Pubkey,
session_signer: &Pubkey,
pool_token_account: &Pubkey,
manager_fee_account: &Pubkey,
referrer_pool_account: &Pubkey,
pool_mint: &Pubkey,
token_program_id: &Pubkey,
wsol_token_account: &Pubkey,
transient_wsol_account: &Pubkey,
program_signer: &Pubkey,
payer: &Pubkey,
sol_deposit_authority: Option<&Pubkey>,
amount: u64,
) -> Instruction {
let accounts = vec![
// accounts extraction from parameters
// ...
AccountMeta::new_readonly(*session_signer, true), // Session must sign
AccountMeta::new(*program_signer, false), // Program signer PDA
AccountMeta::new_readonly(*payer, true),
];
// ...
}
The session_signer is the Session account created during session initialization. The program_signer is Ignition's PDA that will co-sign token transfers, proving to the Token Program that the operation originates from an authorized application.
User Extraction
A critical security consideration when integrating sessions is properly identifying the actual user behind a transaction. The Session account signs on behalf of the user, but Ignition needs to know the real user's identity to validate token account ownership and other user-specific constraints.
The SDK provides extract_user_from_signer_or_session to this purpose. This function performs the necessary validations: checking that the session is live, unrevoked, and authorized for the calling program:
fn process_deposit_wsol_with_session(
program_id: &Pubkey,
accounts: &[AccountInfo],
deposit_lamports: u64,
minimum_pool_tokens_out: Option<u64>,
) -> ProgramResult {
use fogo_sessions_sdk::{session::Session, token::PROGRAM_SIGNER_SEED};
let account_info_iter = &mut accounts.iter();
// ... account parsing ...
let signer_or_session_info = next_account_info(account_info_iter)?;
// ...
// Extract the real user from the session
// This also validates expiration, revocation status, and program authorization
let user_pubkey = Session::extract_user_from_signer_or_session(
signer_or_session_info,
program_id
)?;
// Use the extracted user to validate token account ownership
let expected_wsol_ata = get_associated_token_address_with_program_id(
&user_pubkey,
wsol_mint_info.key,
token_program_info.key,
);
if *wsol_token_info.key != expected_wsol_ata {
msg!("`wsol_token` is not the expected ATA for the user");
return Err(ProgramError::InvalidAccountData);
}
// ...
}
By deriving the expected ATA from the session's user, Ignition ensures the wSOL being deposited actually belongs to the user who authorized the session.
Program Signer PDA Validation
Before performing any session token transfer, Ignition must validate that the provided program_signer account is the correct PDA. The Token Program's session hook calls check_authorized_program_signer, which verifies the PDA is derived from the expected seed (PROGRAM_SIGNER_SEED) and the authorized program's ID.
Ignition performs the same derivation to ensure consistency:
let (expected_program_signer, program_signer_bump) =
Pubkey::find_program_address(&[PROGRAM_SIGNER_SEED], program_id);
if *program_signer_info.key != expected_program_signer {
msg!("`program_signer` does not match expected address");
return Err(ProgramError::InvalidSeeds);
}
let program_signer_seeds: &[&[u8]] = &[PROGRAM_SIGNER_SEED, &[program_signer_bump]];
The program_signer_seeds are stored for later use when signing CPIs to the Token Program. When Ignition invokes a token transfer, it signs with this PDA, and the Token Program's session hook will verify that:
- The PDA matches one of the authorized_programs in the Session account
- The program making the CPI is indeed the one that owns this PDA
Session Token Transfers
With the user extracted and the program signer validated, Ignition can perform token transfers. The SDK provides session-aware versions of standard SPL token instructions (transfer_checked, burn, etc.) that accept an optional program_signer parameter.
When this parameter is provided, the instruction includes the program signer as an additional signer, which the modified Token Program requires for session-based transfers:
use fogo_sessions_sdk::token::instruction::transfer_checked;
invoke_signed(
&transfer_checked(
token_program_info.key,
wsol_token_info.key, // Source
wsol_mint_info.key,
wsol_transient_info.key, // Destination
signer_or_session_info.key, // Authority: the Session account
Some(program_signer_info.key), // Program signer: proves authorized program
deposit_lamports,
native_mint::DECIMALS,
)?,
&[
token_program_info.clone(),
wsol_token_info.clone(),
wsol_mint_info.clone(),
wsol_transient_info.clone(),
signer_or_session_info.clone(),
program_signer_info.clone(),
],
&[program_signer_seeds], // Ignition signs with its PDA
)?;
When the Token Program processes this transfer, it detects that the authority (signer_or_session_info) is a Session account owned by the Session Manager. This triggers the session hook which:
- Verifies the session is live and unrevoked
- Confirms the token account owner matches the session's user
- Checks that program_signer_info corresponds to an authorized program in the session
- Validates spending limits (or bypasses them if the session has Tokens::All)
The same pattern applies to token burns during withdrawals:
use fogo_sessions_sdk::token::instruction::burn;
invoke_signed(
&burn(
token_program_info.key,
burn_from_pool_info.key,
pool_mint_info.key,
user_transfer_authority_info.key,
program_signer_info.key, // Program signer for session binding
pool_tokens_burnt,
)?,
&[
burn_from_pool_info.clone(),
pool_mint_info.clone(),
user_transfer_authority_info.clone(),
program_signer_info.clone(),
],
&[program_signer_seeds],
)?;
Session vs Direct User Routing
Ignition also supports direct user interactions (without sessions) for backwards compatibility. For certain instructions, the program detects which path to take by checking whether the program signer account is present:
// Detect session path by presence of program signer account
if let Ok(program_signer_info) = next_account_info(account_info_iter) {
use fogo_sessions_sdk::token::instruction::{burn, transfer_checked};
use fogo_sessions_sdk::token::PROGRAM_SIGNER_SEED;
// Validate the program signer PDA
let (expected_program_signer, program_signer_bump) =
Pubkey::find_program_address(&[PROGRAM_SIGNER_SEED], program_id);
if expected_program_signer != *program_signer_info.key {
msg!("Invalid program signer account");
return Err(ProgramError::InvalidProgram);
}
let program_signer_seeds: &[&[u8]] = &[PROGRAM_SIGNER_SEED, &[program_signer_bump]];
// Use session-aware token operations with dual signatures...
} else {
// Direct user path: no program signer, use standard SPL token operations
// The user signs directly, no session validation needed
}
This design allows Ignition to serve both session-enabled clients and traditional clients (who sign each transaction individually). The security model remains intact in both cases: session users are protected by the Token Program's runtime hooks, while direct users maintain full control over each transaction they sign.
Conclusion
Fogo Sessions provide a protocol-level solution to one of the biggest usability challenges in blockchain systems: repeated user signing. By binding authority to a signed intent, enforcing scope through on-chain validation, and integrating directly with the Token Program, sessions enable smoother user flows without expanding the trust surface.
The Ignition integration shows how these guarantees hold up in a real protocol. Session-based execution maintains clear boundaries around program authorization, token ownership, and expiration, while still allowing applications to offer fast, low-friction interactions. As more applications adopt sessions, this pattern establishes a strong foundation for building responsive, user-friendly systems on Fogo without compromising security assumptions.
We publish practical, high-signal deep dives like this based on real audit work and hands-on protocol analysis. If you’re building close to the metal and care about correctness, follow Adevar Labs on X and LinkedIn for future write-ups on smart contract security.
Ship Safely.



