// SPDX-License-Identifier: MIT pragma solidity ^0.8.26; import "./IVaultfireIdentity.sol"; /// @title SessionKeyManager — Time-limited, scoped signing keys for AI agents /// @author Vaultfire Protocol /// @notice Creates ephemeral session keys with specific permissions that expire /// automatically. Agents get the minimum authority needed for a task, /// nothing more. If the session expires or the agent goes rogue, the /// key is worthless. /// @dev Session keys are mapped to their creator's ERC-8004 identity. contract SessionKeyManager { // ═══════════════════════════════════════════════════ // STRUCTS // ═══════════════════════════════════════════════════ struct SessionKey { address owner; // ERC-8004 registered agent who created this session address sessionAddress; // The address authorized to sign uint256 maxValue; // Max native token per transaction (wei) uint256 totalLimit; // Max total spend over session lifetime (wei) uint256 totalSpent; // Running total spent by this session key uint256 expiresAt; // Unix timestamp when session expires uint256 createdAt; // Unix timestamp of creation bool revoked; // Manually revoked by owner bool allowAnyContract; // If true, can call any contract } // ═══════════════════════════════════════════════════ // STATE // ═══════════════════════════════════════════════════ IVaultfireIdentity public immutable identityRegistry; /// @notice Incrementing session ID counter uint256 public sessionCount; /// @notice Session ID → SessionKey data mapping(uint256 => SessionKey) public sessions; /// @notice Session ID → allowed contract address → bool mapping(uint256 => mapping(address => bool)) public sessionAllowedContracts; /// @notice Owner address → list of session IDs they created mapping(address => uint256[]) public ownerSessions; /// @notice Session address → session ID (for reverse lookup) mapping(address => uint256) public sessionToId; // ═══════════════════════════════════════════════════ // EVENTS // ═══════════════════════════════════════════════════ event SessionCreated( uint256 indexed sessionId, address indexed owner, address indexed sessionAddress, uint256 expiresAt, uint256 maxValue, uint256 totalLimit ); event SessionUsed( uint256 indexed sessionId, address indexed target, uint256 value ); event SessionRevoked(uint256 indexed sessionId, address indexed owner); event AllSessionsRevoked(address indexed owner); // ═══════════════════════════════════════════════════ // ERRORS // ═══════════════════════════════════════════════════ error NotRegistered(); error NotSessionOwner(); error SessionExpired(); error SessionRevoked_(); error ExceedsSessionValueLimit(uint256 requested, uint256 limit); error ExceedsSessionTotalLimit(uint256 wouldSpend, uint256 limit); error ContractNotAllowedInSession(address target); error InvalidSessionAddress(); error InvalidDuration(); error ZeroAddress(); // ═══════════════════════════════════════════════════ // CONSTRUCTOR // ═══════════════════════════════════════════════════ constructor(address _identityRegistry) { if (_identityRegistry == address(0)) revert ZeroAddress(); identityRegistry = IVaultfireIdentity(_identityRegistry); } // ═══════════════════════════════════════════════════ // CREATE SESSION KEY // ═══════════════════════════════════════════════════ /// @notice Create a new session key with scoped permissions /// @param sessionAddress The address to authorize as a session signer /// @param maxValue Max native token per transaction (wei) /// @param totalLimit Max total spend for the session lifetime (wei) /// @param durationSeconds How long the session key is valid (seconds) /// @param allowedContracts Contracts this session can interact with (empty = any) /// @return sessionId The ID of the created session function createSession( address sessionAddress, uint256 maxValue, uint256 totalLimit, uint256 durationSeconds, address[] calldata allowedContracts ) external returns (uint256 sessionId) { // Verify caller is registered via ERC-8004 getAgent() (string memory agentURI,,,) = identityRegistry.getAgent(msg.sender); if (bytes(agentURI).length == 0) revert NotRegistered(); if (sessionAddress == address(0)) revert InvalidSessionAddress(); if (durationSeconds == 0) revert InvalidDuration(); sessionId = sessionCount++; uint256 expiresAt = block.timestamp + durationSeconds; sessions[sessionId] = SessionKey({ owner: msg.sender, sessionAddress: sessionAddress, maxValue: maxValue, totalLimit: totalLimit, totalSpent: 0, expiresAt: expiresAt, createdAt: block.timestamp, revoked: false, allowAnyContract: allowedContracts.length == 0 }); // Set allowed contracts for (uint256 i = 0; i < allowedContracts.length; i++) { sessionAllowedContracts[sessionId][allowedContracts[i]] = true; } ownerSessions[msg.sender].push(sessionId); sessionToId[sessionAddress] = sessionId; emit SessionCreated( sessionId, msg.sender, sessionAddress, expiresAt, maxValue, totalLimit ); } // ═══════════════════════════════════════════════════ // VALIDATE SESSION — Called by KeyVault before executing // ═══════════════════════════════════════════════════ /// @notice Validate that a session key can execute a specific transaction /// @param sessionId The session to validate /// @param target The contract being called /// @param value The value being sent /// @return valid True if the session key is authorized for this transaction function validateSession( uint256 sessionId, address target, uint256 value ) external returns (bool valid) { SessionKey storage session = sessions[sessionId]; // Check expiry if (block.timestamp > session.expiresAt) revert SessionExpired(); // Check revocation if (session.revoked) revert SessionRevoked_(); // Check per-tx value limit if (value > session.maxValue) { revert ExceedsSessionValueLimit(value, session.maxValue); } // Check total lifetime limit if (session.totalSpent + value > session.totalLimit) { revert ExceedsSessionTotalLimit(session.totalSpent + value, session.totalLimit); } // Check contract allowlist if (!session.allowAnyContract && !sessionAllowedContracts[sessionId][target]) { revert ContractNotAllowedInSession(target); } // Update spending session.totalSpent += value; emit SessionUsed(sessionId, target, value); return true; } // ═══════════════════════════════════════════════════ // REVOKE // ═══════════════════════════════════════════════════ /// @notice Revoke a specific session key function revokeSession(uint256 sessionId) external { if (sessions[sessionId].owner != msg.sender) revert NotSessionOwner(); sessions[sessionId].revoked = true; emit SessionRevoked(sessionId, msg.sender); } /// @notice Revoke ALL session keys for the caller (emergency) function revokeAllSessions() external { uint256[] storage ids = ownerSessions[msg.sender]; for (uint256 i = 0; i < ids.length; i++) { if (!sessions[ids[i]].revoked) { sessions[ids[i]].revoked = true; emit SessionRevoked(ids[i], msg.sender); } } emit AllSessionsRevoked(msg.sender); } // ═══════════════════════════════════════════════════ // VIEW FUNCTIONS // ═══════════════════════════════════════════════════ /// @notice Check if a session key is currently valid (not expired, not revoked) function isSessionValid(uint256 sessionId) external view returns (bool) { SessionKey storage session = sessions[sessionId]; return !session.revoked && block.timestamp <= session.expiresAt && session.totalSpent < session.totalLimit; } /// @notice Get the remaining spend allowance for a session function sessionRemaining(uint256 sessionId) external view returns (uint256) { SessionKey storage session = sessions[sessionId]; if (session.revoked || block.timestamp > session.expiresAt) return 0; if (session.totalSpent >= session.totalLimit) return 0; return session.totalLimit - session.totalSpent; } /// @notice Get all session IDs for an owner function getOwnerSessions(address owner_) external view returns (uint256[] memory) { return ownerSessions[owner_]; } /// @notice Get full session data function getSession(uint256 sessionId) external view returns (SessionKey memory) { return sessions[sessionId]; } /// @notice Check if a contract is allowed for a session function isContractAllowed(uint256 sessionId, address target) external view returns (bool) { if (sessions[sessionId].allowAnyContract) return true; return sessionAllowedContracts[sessionId][target]; } }