session_keys/
lib.rs

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