// SPDX-License-Identifier: MIT pragma solidity ^0.8.26; import "./IVaultfireIdentity.sol"; import "./IVaultfireBonds.sol"; import "./IVaultfireReputation.sol"; /// @title ScopedDelegation — ERC-7710-compatible scoped permissions for AI agent delegation /// @author Vaultfire Protocol /// @notice Extends Vaultfire's delegation with industry-standard scoped permissions: /// - Agent-to-agent delegation with progressive permission narrowing /// - Rate limiting, spending caps, contract allowlists /// - Compatible with MetaMask Delegation Toolkit and Ledger Agent Policies /// @dev Implements delegation chains where each hop can only narrow permissions, never widen. contract ScopedDelegation { // ═══════════════════════════════════════════════════ // STRUCTS // ═══════════════════════════════════════════════════ struct Scope { uint256 maxValuePerTx; // Max native token per transaction (wei) uint256 maxTotalValue; // Max total value over scope lifetime (wei) uint256 totalSpent; // Running total spent uint256 maxTxCount; // Max number of transactions allowed (0 = unlimited) uint256 txCount; // Running transaction count uint256 rateLimit; // Min seconds between transactions (0 = no limit) uint256 lastTxAt; // Timestamp of last transaction uint256 expiresAt; // When this scope expires uint256 createdAt; // When this scope was created address grantor; // Who granted this scope address grantee; // Who received this scope bool revoked; // Whether scope has been revoked bool active; // Whether scope is currently active } struct DelegationChain { uint256[] scopeIds; // Chain of scope IDs from root to leaf uint256 depth; // How deep in the chain (0 = root) address rootGrantor; // Original authority holder bool valid; // Whether chain is still valid } // ═══════════════════════════════════════════════════ // STATE // ═══════════════════════════════════════════════════ IVaultfireIdentity public immutable identityRegistry; IVaultfireBonds public immutable bondsRegistry; IVaultfireReputation public immutable reputationRegistry; address public owner; uint256 public totalScopes; uint256 public totalChains; uint256 public constant MAX_CHAIN_DEPTH = 5; // Max delegation chain depth mapping(uint256 => Scope) public scopes; mapping(uint256 => DelegationChain) public chains; // Agent's granted scopes (as grantor) mapping(address => uint256[]) public grantedScopes; // Agent's received scopes (as grantee) mapping(address => uint256[]) public receivedScopes; // Contract allowlists per scope: scopeId => contract => allowed mapping(uint256 => mapping(address => bool)) public contractAllowlist; // Function allowlists per scope: scopeId => bytes4 selector => allowed mapping(uint256 => mapping(bytes4 => bool)) public functionAllowlist; // Scope has contract restrictions mapping(uint256 => bool) public hasContractRestrictions; // Scope has function restrictions mapping(uint256 => bool) public hasFunctionRestrictions; // ═══════════════════════════════════════════════════ // EVENTS // ═══════════════════════════════════════════════════ event ScopeCreated(uint256 indexed scopeId, address indexed grantor, address indexed grantee, uint256 expiresAt); event ScopeRevoked(uint256 indexed scopeId, address indexed revokedBy); event ScopeUsed(uint256 indexed scopeId, address indexed grantee, uint256 value); event ChainCreated(uint256 indexed chainId, address indexed rootGrantor, uint256 depth); event ContractAllowlistUpdated(uint256 indexed scopeId, address indexed contractAddr, bool allowed); event FunctionAllowlistUpdated(uint256 indexed scopeId, bytes4 indexed selector, bool allowed); // ═══════════════════════════════════════════════════ // MODIFIERS // ═══════════════════════════════════════════════════ modifier onlyOwner() { require(msg.sender == owner, "ScopedDelegation: caller is not owner"); _; } modifier onlyRegisteredAgent(address agent) { (string memory uri, bool active, , ) = identityRegistry.getAgent(agent); require(bytes(uri).length > 0 && active, "ScopedDelegation: agent not registered or inactive"); _; } // ═══════════════════════════════════════════════════ // CONSTRUCTOR // ═══════════════════════════════════════════════════ constructor( address _identityRegistry, address _bondsRegistry, address _reputationRegistry ) { require(_identityRegistry != address(0), "ScopedDelegation: zero identity address"); require(_bondsRegistry != address(0), "ScopedDelegation: zero bonds address"); require(_reputationRegistry != address(0), "ScopedDelegation: zero reputation address"); identityRegistry = IVaultfireIdentity(_identityRegistry); bondsRegistry = IVaultfireBonds(_bondsRegistry); reputationRegistry = IVaultfireReputation(_reputationRegistry); owner = msg.sender; } // ═══════════════════════════════════════════════════ // SCOPE CREATION // ═══════════════════════════════════════════════════ /// @notice Create a new delegation scope from one agent to another /// @param grantee Agent receiving the delegation /// @param maxValuePerTx Max value per transaction (wei) /// @param maxTotalValue Max total value over lifetime (wei) /// @param maxTxCount Max transaction count (0 = unlimited) /// @param rateLimit Min seconds between transactions (0 = no limit) /// @param duration Duration in seconds function createScope( address grantee, uint256 maxValuePerTx, uint256 maxTotalValue, uint256 maxTxCount, uint256 rateLimit, uint256 duration ) external onlyRegisteredAgent(msg.sender) onlyRegisteredAgent(grantee) returns (uint256 scopeId) { require(grantee != msg.sender, "ScopedDelegation: cannot self-delegate"); require(duration > 0 && duration <= 365 days, "ScopedDelegation: invalid duration"); require(maxValuePerTx > 0 || maxTxCount > 0, "ScopedDelegation: must have at least one limit"); scopeId = totalScopes; totalScopes++; scopes[scopeId] = Scope({ maxValuePerTx: maxValuePerTx, maxTotalValue: maxTotalValue, totalSpent: 0, maxTxCount: maxTxCount, txCount: 0, rateLimit: rateLimit, lastTxAt: 0, expiresAt: block.timestamp + duration, createdAt: block.timestamp, grantor: msg.sender, grantee: grantee, revoked: false, active: true }); grantedScopes[msg.sender].push(scopeId); receivedScopes[grantee].push(scopeId); emit ScopeCreated(scopeId, msg.sender, grantee, block.timestamp + duration); } /// @notice Sub-delegate: create a narrower scope from a received scope (delegation chain) /// @dev The new scope must be strictly narrower than the parent scope /// @param parentScopeId The scope being sub-delegated /// @param grantee New grantee for the sub-delegation /// @param maxValuePerTx Must be <= parent's maxValuePerTx /// @param maxTotalValue Must be <= parent's remaining value /// @param maxTxCount Must be <= parent's remaining tx count /// @param rateLimit Must be >= parent's rateLimit (more restrictive) /// @param duration Must be <= parent's remaining duration function subDelegate( uint256 parentScopeId, address grantee, uint256 maxValuePerTx, uint256 maxTotalValue, uint256 maxTxCount, uint256 rateLimit, uint256 duration ) external onlyRegisteredAgent(grantee) returns (uint256 scopeId) { Scope storage parent = scopes[parentScopeId]; require(parent.active && !parent.revoked, "ScopedDelegation: parent scope not active"); require(parent.grantee == msg.sender, "ScopedDelegation: caller is not grantee of parent"); require(block.timestamp < parent.expiresAt, "ScopedDelegation: parent scope expired"); require(grantee != msg.sender, "ScopedDelegation: cannot self-delegate"); // Enforce progressive narrowing — new scope must be strictly more restrictive require(maxValuePerTx <= parent.maxValuePerTx, "ScopedDelegation: maxValuePerTx exceeds parent"); uint256 parentRemaining = parent.maxTotalValue - parent.totalSpent; require(maxTotalValue <= parentRemaining, "ScopedDelegation: maxTotalValue exceeds parent remaining"); if (parent.maxTxCount > 0) { uint256 parentRemainingTx = parent.maxTxCount - parent.txCount; require(maxTxCount <= parentRemainingTx, "ScopedDelegation: maxTxCount exceeds parent remaining"); } require(rateLimit >= parent.rateLimit, "ScopedDelegation: rateLimit less restrictive than parent"); uint256 parentRemainingDuration = parent.expiresAt - block.timestamp; require(duration <= parentRemainingDuration, "ScopedDelegation: duration exceeds parent remaining"); scopeId = totalScopes; totalScopes++; scopes[scopeId] = Scope({ maxValuePerTx: maxValuePerTx, maxTotalValue: maxTotalValue, totalSpent: 0, maxTxCount: maxTxCount, txCount: 0, rateLimit: rateLimit, lastTxAt: 0, expiresAt: block.timestamp + duration, createdAt: block.timestamp, grantor: msg.sender, grantee: grantee, revoked: false, active: true }); grantedScopes[msg.sender].push(scopeId); receivedScopes[grantee].push(scopeId); emit ScopeCreated(scopeId, msg.sender, grantee, block.timestamp + duration); } // ═══════════════════════════════════════════════════ // SCOPE ALLOWLISTS // ═══════════════════════════════════════════════════ /// @notice Add or remove a contract from the scope's allowlist function setContractAllowlist(uint256 scopeId, address contractAddr, bool allowed) external { Scope storage scope = scopes[scopeId]; require(scope.grantor == msg.sender, "ScopedDelegation: not grantor"); require(scope.active && !scope.revoked, "ScopedDelegation: scope not active"); contractAllowlist[scopeId][contractAddr] = allowed; hasContractRestrictions[scopeId] = true; emit ContractAllowlistUpdated(scopeId, contractAddr, allowed); } /// @notice Add or remove a function selector from the scope's allowlist function setFunctionAllowlist(uint256 scopeId, bytes4 selector, bool allowed) external { Scope storage scope = scopes[scopeId]; require(scope.grantor == msg.sender, "ScopedDelegation: not grantor"); require(scope.active && !scope.revoked, "ScopedDelegation: scope not active"); functionAllowlist[scopeId][selector] = allowed; hasFunctionRestrictions[scopeId] = true; emit FunctionAllowlistUpdated(scopeId, selector, allowed); } // ═══════════════════════════════════════════════════ // SCOPE USAGE & VALIDATION // ═══════════════════════════════════════════════════ /// @notice Validate and record a scope usage (called before executing delegated action) /// @param scopeId The scope being used /// @param value The value of the transaction /// @param targetContract The contract being called (address(0) if N/A) /// @param functionSelector The function being called (bytes4(0) if N/A) function useScope( uint256 scopeId, uint256 value, address targetContract, bytes4 functionSelector ) external returns (bool) { Scope storage scope = scopes[scopeId]; require(scope.grantee == msg.sender, "ScopedDelegation: not grantee"); require(scope.active && !scope.revoked, "ScopedDelegation: scope not active"); require(block.timestamp < scope.expiresAt, "ScopedDelegation: scope expired"); // Check value limits require(value <= scope.maxValuePerTx, "ScopedDelegation: exceeds per-tx limit"); require(scope.totalSpent + value <= scope.maxTotalValue, "ScopedDelegation: exceeds total limit"); // Check transaction count if (scope.maxTxCount > 0) { require(scope.txCount < scope.maxTxCount, "ScopedDelegation: tx count exceeded"); } // Check rate limit if (scope.rateLimit > 0 && scope.lastTxAt > 0) { require(block.timestamp >= scope.lastTxAt + scope.rateLimit, "ScopedDelegation: rate limited"); } // Check contract allowlist if (hasContractRestrictions[scopeId] && targetContract != address(0)) { require(contractAllowlist[scopeId][targetContract], "ScopedDelegation: contract not allowed"); } // Check function allowlist if (hasFunctionRestrictions[scopeId] && functionSelector != bytes4(0)) { require(functionAllowlist[scopeId][functionSelector], "ScopedDelegation: function not allowed"); } // Record usage scope.totalSpent += value; scope.txCount++; scope.lastTxAt = block.timestamp; emit ScopeUsed(scopeId, msg.sender, value); return true; } // ═══════════════════════════════════════════════════ // SCOPE MANAGEMENT // ═══════════════════════════════════════════════════ /// @notice Revoke a scope (grantor only) function revokeScope(uint256 scopeId) external { Scope storage scope = scopes[scopeId]; require(scope.grantor == msg.sender || msg.sender == owner, "ScopedDelegation: not authorized"); require(scope.active && !scope.revoked, "ScopedDelegation: scope not active"); scope.revoked = true; scope.active = false; emit ScopeRevoked(scopeId, msg.sender); } // ═══════════════════════════════════════════════════ // VIEW FUNCTIONS // ═══════════════════════════════════════════════════ /// @notice Check if a scope is currently valid function isScopeValid(uint256 scopeId) external view returns (bool) { Scope storage scope = scopes[scopeId]; return scope.active && !scope.revoked && block.timestamp < scope.expiresAt; } /// @notice Get remaining allowance for a scope function getScopeRemaining(uint256 scopeId) external view returns ( uint256 remainingValue, uint256 remainingTxCount, uint256 remainingDuration ) { Scope storage scope = scopes[scopeId]; if (!scope.active || scope.revoked || block.timestamp >= scope.expiresAt) { return (0, 0, 0); } remainingValue = scope.maxTotalValue - scope.totalSpent; remainingTxCount = scope.maxTxCount > 0 ? scope.maxTxCount - scope.txCount : type(uint256).max; remainingDuration = scope.expiresAt - block.timestamp; } /// @notice Get all scopes granted by an agent function getGrantedScopes(address agent) external view returns (uint256[] memory) { return grantedScopes[agent]; } /// @notice Get all scopes received by an agent function getReceivedScopes(address agent) external view returns (uint256[] memory) { return receivedScopes[agent]; } /// @notice Transfer ownership function transferOwnership(address newOwner) external onlyOwner { require(newOwner != address(0), "ScopedDelegation: zero owner"); owner = newOwner; } }