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