session_keys/
lib.rs

1use anchor_lang::{prelude::*, solana_program::native_token::LAMPORTS_PER_SOL, system_program};
2
3#[cfg(feature = "no-entrypoint")]
4pub use session_keys_macros::*;
5
6declare_id!("KeyspM2ssCJbqUhQ4k7sveSiY4WjnYsrXkC8oDbwde5");
7
8#[cfg(not(feature = "no-entrypoint"))]
9solana_security_txt::security_txt! {
10    name: "session_keys",
11    project_url: "https://magicblock.gg",
12    contacts: "email:dev@magicblock.gg,twitter:@magicblock",
13    policy: "",
14    preferred_languages: "en",
15    source_code: "https://github.com/magicblock-labs"
16}
17
18#[program]
19pub mod gpl_session {
20    use super::*;
21
22    // create a session token
23    pub fn create_session(
24        ctx: Context<CreateSessionToken>,
25        top_up: Option<bool>,
26        valid_until: Option<i64>,
27        lamports: Option<u64>,
28    ) -> Result<()> {
29        let (top_up, valid_until) = process_session_params(top_up, valid_until)?;
30        create_session_token_handler(ctx, top_up, valid_until, lamports)
31    }
32
33    pub fn create_session_with_payer(
34        ctx: Context<CreateSessionTokenWithPayer>,
35        top_up: Option<bool>,
36        valid_until: Option<i64>,
37        lamports: Option<u64>,
38    ) -> Result<()> {
39        let (top_up, valid_until) = process_session_params(top_up, valid_until)?;
40        create_session_token_with_payer_handler(ctx, top_up, valid_until, lamports)
41    }
42    // revoke a session token
43    pub fn revoke_session(ctx: Context<RevokeSessionToken>) -> Result<()> {
44        revoke_session_token_handler(ctx)
45    }
46}
47
48fn process_session_params(top_up: Option<bool>, valid_until: Option<i64>) -> Result<(bool, i64)> {
49    let top_up = top_up.unwrap_or(false);
50    let valid_until = valid_until.unwrap_or(Clock::get()?.unix_timestamp + 60 * 60 * 1);
51    Ok((top_up, valid_until))
52}
53
54// Create a SessionToken account
55#[derive(Accounts)]
56pub struct CreateSessionToken<'info> {
57    #[account(
58        init,
59        seeds = [
60            SessionToken::SEED_PREFIX.as_bytes(),
61            target_program.key().as_ref(),
62            session_signer.key().as_ref(),
63            authority.key().as_ref()
64        ],
65        bump,
66        payer = authority,
67        space = SessionToken::LEN
68    )]
69    pub session_token: Account<'info, SessionToken>,
70
71    #[account(mut)]
72    pub session_signer: Signer<'info>,
73    #[account(mut)]
74    pub authority: Signer<'info>,
75
76    /// CHECK the target program is actually a program.
77    #[account(executable)]
78    pub target_program: AccountInfo<'info>,
79
80    pub system_program: Program<'info, System>,
81}
82
83fn create_session_token_internal<'info>(
84    session_token: &mut Account<'info, SessionToken>,
85    authority: Pubkey,
86    target_program: Pubkey,
87    session_signer: Pubkey,
88    system_program: AccountInfo<'info>,
89    payer: AccountInfo<'info>,
90    session_signer_account: AccountInfo<'info>,
91    top_up: bool,
92    valid_until: i64,
93    lamports: Option<u64>,
94) -> Result<()> {
95    // Valid until can't be greater than a week
96    require!(
97        valid_until <= Clock::get()?.unix_timestamp + (60 * 60 * 24 * 7),
98        SessionError::ValidityTooLong
99    );
100
101    session_token.set_inner(SessionToken {
102        authority,
103        target_program,
104        session_signer,
105        valid_until,
106    });
107
108    // Top up the session signer account with some lamports to pay for the transaction fees
109    if top_up {
110        system_program::transfer(
111            CpiContext::new(
112                system_program,
113                system_program::Transfer {
114                    from: payer,
115                    to: session_signer_account,
116                },
117            ),
118            lamports.unwrap_or(LAMPORTS_PER_SOL / 100),
119        )?;
120    }
121
122    Ok(())
123}
124
125// Handler to create a session token account
126pub fn create_session_token_handler(
127    ctx: Context<CreateSessionToken>,
128    top_up: bool,
129    valid_until: i64,
130    lamports: Option<u64>,
131) -> Result<()> {
132    create_session_token_internal(
133        &mut ctx.accounts.session_token,
134        ctx.accounts.authority.key(),
135        ctx.accounts.target_program.key(),
136        ctx.accounts.session_signer.key(),
137        ctx.accounts.system_program.to_account_info(),
138        ctx.accounts.authority.to_account_info(),
139        ctx.accounts.session_signer.to_account_info(),
140        top_up,
141        valid_until,
142        lamports,
143    )
144}
145
146// Create a SessionToken account
147#[derive(Accounts)]
148pub struct CreateSessionTokenWithPayer<'info> {
149    #[account(
150        init,
151        seeds = [
152            SessionToken::SEED_PREFIX.as_bytes(),
153            target_program.key().as_ref(),
154            session_signer.key().as_ref(),
155            authority.key().as_ref()
156        ],
157        bump,
158        payer = payer,
159        space = SessionToken::LEN
160    )]
161    pub session_token: Account<'info, SessionToken>,
162
163    #[account(mut)]
164    pub session_signer: Signer<'info>,
165    #[account(mut)]
166    pub payer: Signer<'info>,
167    pub authority: Signer<'info>,
168
169    /// CHECK the target program is actually a program.
170    #[account(executable)]
171    pub target_program: AccountInfo<'info>,
172
173    pub system_program: Program<'info, System>,
174}
175
176// Handler to create a session token account
177pub fn create_session_token_with_payer_handler(
178    ctx: Context<CreateSessionTokenWithPayer>,
179    top_up: bool,
180    valid_until: i64,
181    lamports: Option<u64>,
182) -> Result<()> {
183    create_session_token_internal(
184        &mut ctx.accounts.session_token,
185        ctx.accounts.authority.key(),
186        ctx.accounts.target_program.key(),
187        ctx.accounts.session_signer.key(),
188        ctx.accounts.system_program.to_account_info(),
189        ctx.accounts.payer.to_account_info(),
190        ctx.accounts.session_signer.to_account_info(),
191        top_up,
192        valid_until,
193        lamports,
194    )
195}
196
197// Revoke a session token
198// We allow *anyone* to revoke a session token. This is because the session token is designed to
199// expire on it's own after a certain amount of time. However, if the session token is compromised
200// anyone can revoke it immediately.
201//
202// One attack vector here to consider, however is that a malicious actor could enumerate all the tokens
203// created using the program and revoke them all or keep revoking them as they are created. It is a
204// nuisance but not a security risk. We can easily address this by whitelisting a revoker.
205#[derive(Accounts)]
206pub struct RevokeSessionToken<'info> {
207    #[account(
208        mut,
209        seeds = [
210            SessionToken::SEED_PREFIX.as_bytes(),
211            session_token.target_program.key().as_ref(),
212            session_token.session_signer.key().as_ref(),
213            session_token.authority.key().as_ref()
214        ],
215        bump,
216        has_one = authority,
217        close = authority,
218    )]
219    pub session_token: Account<'info, SessionToken>,
220
221    #[account(mut)]
222    // Only the token authority can reclaim the rent
223    pub authority: SystemAccount<'info>,
224
225    pub system_program: Program<'info, System>,
226}
227
228// Handler to revoke a session token
229pub fn revoke_session_token_handler(_: Context<RevokeSessionToken>) -> Result<()> {
230    Ok(())
231}
232
233pub struct ValidityChecker<'info> {
234    pub session_token: Account<'info, SessionToken>,
235    pub session_signer: Signer<'info>,
236    pub authority: Pubkey,
237    pub target_program: Pubkey,
238}
239
240// SessionToken Account
241#[account]
242#[derive(Copy)]
243pub struct SessionToken {
244    pub authority: Pubkey,
245    pub target_program: Pubkey,
246    pub session_signer: Pubkey,
247    pub valid_until: i64,
248}
249
250impl SessionToken {
251    pub const LEN: usize = 8 + std::mem::size_of::<Self>();
252    pub const SEED_PREFIX: &'static str = "session_token";
253
254    fn is_expired(&self) -> Result<bool> {
255        let now = Clock::get()?.unix_timestamp;
256        Ok(now < self.valid_until)
257    }
258
259    // validate the token
260    pub fn validate(&self, ctx: ValidityChecker) -> Result<bool> {
261        let target_program = ctx.target_program;
262        let session_signer = ctx.session_signer.key();
263        let authority = ctx.authority.key();
264
265        // Check the PDA seeds
266        let seeds = &[
267            SessionToken::SEED_PREFIX.as_bytes(),
268            target_program.as_ref(),
269            session_signer.as_ref(),
270            authority.as_ref(),
271        ];
272
273        let (pda, _) = Pubkey::find_program_address(seeds, &crate::id());
274
275        require_eq!(pda, ctx.session_token.key(), SessionError::InvalidToken);
276
277        // Check if the token has expired
278        self.is_expired()
279    }
280}
281
282pub trait Session<'info> {
283    fn session_token(&self) -> Option<Account<'info, SessionToken>>;
284    fn session_signer(&self) -> Signer<'info>;
285    fn session_authority(&self) -> Pubkey;
286    fn target_program(&self) -> Pubkey;
287
288    fn is_valid(&self) -> Result<bool> {
289        let session_token = self.session_token().ok_or(SessionError::NoToken)?;
290        let validity_ctx = ValidityChecker {
291            session_token: session_token.clone(),
292            session_signer: self.session_signer(),
293            authority: self.session_authority(),
294            target_program: self.target_program(),
295        };
296        // Check if the token is valid
297        session_token.validate(validity_ctx)
298    }
299}
300
301#[error_code]
302pub enum SessionError {
303    #[msg("Requested validity is too long")]
304    ValidityTooLong,
305    #[msg("Invalid session token")]
306    InvalidToken,
307    #[msg("No session token provided")]
308    NoToken,
309}