Session-Enabled Account Contract for Starknet

THIS PROJECT DOES NOT COMPETE FOR A PRIZE

A OpenZeppelin-based account contract with native session key support for gasless, time-limited transaction execution on Starknet.

Overview

This Cairo smart contract extends OpenZeppelin's standard account implementation to support temporary session keys - cryptographic keys with built-in expiration, usage limits, and optional function restrictions. Session keys enable users to pre-authorize transactions without requiring wallet signatures for each action, dramatically improving UX for gaming, DeFi, and high-frequency dApps.

Key Features

🔑 Session Key Management

  • Time-limited sessions: Each session key has a valid_until timestamp (Unix seconds)
  • Call limits: Maximum number of transactions per session (max_calls)
  • Usage tracking: Automatic counter increment (calls_used) on each transaction
  • Entrypoint restrictions: Optional whitelist of allowed function selectors
  • Zero-config mode: Empty entrypoint list = allow all functions

🛡️ Dual Signature Validation

The contract supports two signature formats in __validate__:

  1. Owner Path (2 elements): [r, s]

    • Standard ECDSA signature from account owner
    • Full control over the account
    • Uses OpenZeppelin's transaction hash validation
  2. Session Path (4 elements): [session_pubkey, r, s, valid_until]

    • ECDSA signature from session key
    • Validates timestamp, call limits, and entrypoint restrictions
    • Compatible with AVNU paymaster for gasless transactions

🔐 Security Mechanisms

if get_block_timestamp() > valid_until {
    return 0;  // Expired session
}

if session.calls_used >= session.max_calls {
    return 0;  // Usage limit exceeded
}

if !self._is_session_allowed_for_calls(session_pubkey, calls.span()) {
    return 0;  // Unauthorized function call
}
  • Temporal safety: Sessions automatically expire after valid_until
  • Rate limiting: Prevents session key abuse via max_calls cap
  • Function whitelisting: Optional entrypoint restrictions per session
  • Owner-only management: Only the account owner can add/revoke sessions (assert_only_self())

Architecture

Storage Structure

#[storage]
struct Storage {
    account: AccountComponent::Storage,           // OZ account state
    src5: SRC5Component::Storage,                 // ERC-165 introspection
    upgradeable: UpgradeableComponent::Storage,   // Proxy upgrade logic
    session_keys: Map<felt252, SessionData>,      // Session metadata
    session_entrypoints: Map<(felt252, u32), felt252>, // Allowed functions
}

SessionData struct:

pub struct SessionData {
    pub valid_until: u64,              // Expiration timestamp
    pub max_calls: u32,                // Maximum allowed calls
    pub calls_used: u32,               // Current usage counter
    pub allowed_entrypoints_len: u32,  // Number of whitelisted functions
}

Session Lifecycle

  1. Creation (via owner transaction):

    fn add_or_update_session_key(
    session_key: felt252,        // Public key of session signer
    valid_until: u64,            // Unix timestamp expiration
    max_calls: u32,              // Usage limit
    allowed_entrypoints: Array<felt252>  // Optional function whitelist
    )
    
  2. Validation (on each transaction):

    // Checks performed in __validate__:
    - Signature length == 4
    - Current timestamp <= valid_until
    - calls_used < max_calls
    - All call selectors in allowed_entrypoints (if specified)
    - ECDSA signature verification
    
  3. Revocation (via owner transaction):

    fn revoke_session_key(session_key: felt252)
    // Clears SessionData and all stored entrypoints
    

Message Hash Computation

Session signatures use Poseidon hash over the following structure:

poseidon_hash_span([
    account_address,
    chain_id,
    nonce,
    valid_until,
    // For each call:
    call.to,
    call.selector,
    call.calldata.len(),
    ...calldata_elements
])

This matches the message construction in the frontend TypeScript code, ensuring signature compatibility.

Usage Example

Step 1: Deploy Account

constructor(public_key: felt252)  // Owner's public key

Step 2: Add Session Key (Owner Signs)

add_or_update_session_key(
    session_key: 0x123...,           // Session public key
    valid_until: 1735689600,         // 6 hours from now
    max_calls: 100,                  // Allow 100 transactions
    allowed_entrypoints: array![]    // Empty = allow all functions
)

Step 3: Execute Transaction (Session Signs)

// Submit transaction with 4-element signature:
signature: [
    session_pubkey,  // 0x123...
    r,               // Signature r component
    s,               // Signature s component  
    valid_until      // Must match session data
]

The contract validates the session, verifies the signature, increments calls_used, and executes the transaction.

Advantages Over Standard Accounts

Feature Standard Account Session-Enabled Account
UX Sign every transaction Sign once, execute many
Gas Fees User pays per tx Paymaster can sponsor
Mobile Gaming Constant popups Seamless gameplay
Security Full key exposure risk Time-limited, revocable
DeFi Automation Manual signing Pre-authorized strategies

Compatibility

  • OpenZeppelin Account Standard - Inherits battle-tested account logic
  • SRC-6 (Account Interface) - Standard Starknet account validation
  • ERC-1271 - Off-chain signature validation support
  • UORC (Upgradeable) - Proxy pattern for contract upgrades
  • AVNU Paymaster - Compatible with gasless transaction sponsorship

Security Considerations

✅ Audited Components

  • Built on OpenZeppelin's audited AccountComponent
  • Uses standard Cairo cryptographic primitives (check_ecdsa_signature, poseidon_hash_span)

✅ Protection Mechanisms

  • Replay protection: Nonce included in session message hash
  • Temporal bounds: Automatic expiration via valid_until
  • Usage caps: Maximum call limit prevents abuse
  • Owner privilege: Only owner can manage sessions
  • Self-call validation: Prevents unauthorized empty signatures

⚠️ Best Practices

  • Set reasonable valid_until values (hours, not years)
  • Use max_calls limits appropriate to use case
  • Specify allowed_entrypoints for sensitive operations
  • Monitor and revoke compromised session keys immediately
  • Always validate session key generation happens in secure environment

Events

#[derive(Drop, starknet::Event)]
struct SessionKeyAdded {
    #[key]
    session_key: felt252,
    valid_until: u64,
    max_calls: u32,
}

#[derive(Drop, starknet::Event)]
struct SessionKeyRevoked {
    #[key]
    session_key: felt252,
}

Events enable off-chain indexing and frontend state synchronization.

Technical Highlights

  • Zero-storage wildcard: Empty allowed_entrypoints enables all functions without storage overhead
  • Efficient validation: Pure check (_is_session_allowed_for_calls) before mutation (_consume_session_call)
  • Flexible whitelisting: Per-session function restrictions via indexed storage map
  • Component-based: Modular architecture using OpenZeppelin's component pattern
  • Upgrade-safe: Implements IUpgradeable for seamless contract migrations

Use Cases

🎮 Gaming

Pre-authorize 1000 game actions for 24 hours. Players execute moves without wallet popups.

💱 DeFi Trading

Session key for automated limit orders. Execute trades within predefined parameters.

🌐 Social dApps

Like, comment, share without signing. Session expires after browsing session.

🤖 Bot Automation

Controlled bot access with time + call limits. Revoke immediately if compromised.


Contract Version: v22 License: MIT
Base: OpenZeppelin Account v0.15+

Built With

  • openzeppelin
Share this project:

Updates