// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import { EIP712AuthV3 } from "./common/EIP712AuthV3.sol"; import { EmergencyControlsV3 } from "./common/EmergencyControlsV3.sol"; import { RuntimeCodeGuardV3 } from "./common/RuntimeCodeGuardV3.sol"; interface ICapabilityTargetV3 { /// @return authority Account controlling target configuration. /// @return implementation Zero for immutable/direct targets; current implementation for proxies. /// @return configHash Application configuration snapshot relevant to the delegated operation. function capabilityTargetConfig() external view returns (address authority, address implementation, bytes32 configHash); } /// @notice Finite delegated calls bound to chain, runtime, authority, configuration and exact calldata. /// @dev Only direct, non-delegating targets are supported. A target-supplied implementation getter is not /// evidence of the code that dispatch will execute, so every proxy profile fails closed. contract SessionCapabilitiesV3 is EIP712AuthV3, EmergencyControlsV3, RuntimeCodeGuardV3 { struct Capability { address principal; address session; uint256 chainId; address target; bytes32 targetCodehash; address implementation; bytes32 implementationCodehash; address targetAuthority; bytes4 selector; bytes32 callDataHash; bytes32 callDataSchemaHash; bytes32 targetConfigHash; uint96 callValue; uint32 maxCalls; uint64 notBefore; uint64 notAfter; uint256 nonce; } bytes32 public constant CAPABILITY_TYPEHASH = keccak256( "Capability(address principal,address session,uint256 chainId,address target,bytes32 targetCodehash,address implementation,bytes32 implementationCodehash,address targetAuthority,bytes4 selector,bytes32 callDataHash,bytes32 callDataSchemaHash,bytes32 targetConfigHash,uint96 callValue,uint32 maxCalls,uint64 notBefore,uint64 notAfter,uint256 nonce)" ); mapping(address => uint256) public nextNonce; mapping(bytes32 => Capability) private _capabilities; mapping(bytes32 => uint32) public callsUsed; mapping(bytes32 => bool) public revoked; uint256 private _entered; error BadCapability(); error BadNonce(); error Unauthorized(); error CapabilityInactive(); error CallLimitReached(); error WrongCall(); error TargetIdentityChanged(); error UnsafeProxy(); error Reentrancy(); error TargetCallFailed(bytes revertData); event CapabilityRegisteredV3( bytes32 indexed capabilityId, address indexed principal, address indexed session, address target, bytes4 selector ); event CapabilityRevokedV3(bytes32 indexed capabilityId, address indexed caller); event CapabilityUsedV3(bytes32 indexed capabilityId, uint32 callNumber, bytes32 callHash, uint256 value); event PrincipalNonceInvalidatedV3(address indexed principal, uint256 previousNonce, uint256 nextNonce); constructor(address governance_, address guardian_, uint64 minimumPauseDuration_) EIP712AuthV3("Vaultfire Session Capabilities", "3.2") EmergencyControlsV3(governance_, guardian_, minimumPauseDuration_) { } function registerDirect(Capability calldata capability) external whenNotPaused returns (bytes32 id) { if (msg.sender != capability.principal) revert Unauthorized(); id = _register(capability); } function registerBySignature(Capability calldata capability, bytes calldata signature) external whenNotPaused returns (bytes32 id) { _requireSignature(capability.principal, _hashTypedData(_structHash(capability)), signature); id = _register(capability); } function invalidateNonces(uint256 newNextNonce) external { uint256 prior = nextNonce[msg.sender]; if (newNextNonce <= prior) revert BadNonce(); nextNonce[msg.sender] = newNextNonce; emit PrincipalNonceInvalidatedV3(msg.sender, prior, newNextNonce); } function revoke(bytes32 id) external { Capability storage capability = _capabilities[id]; if (msg.sender != capability.principal && msg.sender != capability.session) revert Unauthorized(); if (revoked[id] || capability.principal == address(0)) revert BadCapability(); revoked[id] = true; emit CapabilityRevokedV3(id, msg.sender); } function execute(bytes32 id, bytes calldata callData) external payable whenNotPaused returns (bytes memory result) { if (_entered != 0) revert Reentrancy(); Capability storage capability = _capabilities[id]; if (msg.sender != capability.session) revert Unauthorized(); if ( revoked[id] || block.timestamp < capability.notBefore || block.timestamp > capability.notAfter || capability.principal == address(0) || capability.chainId != block.chainid ) revert CapabilityInactive(); if (callsUsed[id] >= capability.maxCalls) revert CallLimitReached(); if (msg.value != capability.callValue || callData.length < 4) revert WrongCall(); bytes4 selector; assembly { selector := calldataload(callData.offset) } if (selector != capability.selector || keccak256(callData) != capability.callDataHash) revert WrongCall(); _requireLiveIdentity(capability); _entered = 1; uint32 useNumber = callsUsed[id] + 1; callsUsed[id] = useNumber; (bool ok, bytes memory returned) = capability.target.call{ value: msg.value }(callData); _entered = 0; if (!ok) revert TargetCallFailed(returned); emit CapabilityUsedV3(id, useNumber, keccak256(callData), msg.value); return returned; } function getCapability(bytes32 id) external view returns (Capability memory) { return _capabilities[id]; } function _register(Capability calldata capability) private returns (bytes32 id) { if ( capability.principal == address(0) || capability.session == address(0) || capability.chainId != block.chainid || capability.target.code.length == 0 || capability.target.codehash != capability.targetCodehash || capability.selector == bytes4(0) || capability.callDataHash == bytes32(0) || capability.callDataSchemaHash == bytes32(0) || capability.targetConfigHash == bytes32(0) || capability.targetAuthority == address(0) || capability.maxCalls == 0 || capability.notBefore > block.timestamp || capability.notAfter <= block.timestamp || capability.notBefore > capability.notAfter ) revert BadCapability(); _requireAttestedIdentity( capability.target, capability.implementation, capability.implementationCodehash, capability.targetAuthority, capability.targetConfigHash ); if (capability.nonce != nextNonce[capability.principal]) revert BadNonce(); id = keccak256(abi.encode(capability)); nextNonce[capability.principal] = capability.nonce + 1; _capabilities[id] = capability; emit CapabilityRegisteredV3( id, capability.principal, capability.session, capability.target, capability.selector ); } function _requireLiveIdentity(Capability storage capability) private view { if (capability.target.codehash != capability.targetCodehash) revert TargetIdentityChanged(); _requireAttestedIdentity( capability.target, capability.implementation, capability.implementationCodehash, capability.targetAuthority, capability.targetConfigHash ); } function _requireAttestedIdentity( address target, address expectedImplementation, bytes32 expectedImplementationCodehash, address expectedAuthority, bytes32 expectedConfigHash ) private view { (bool ok, address authority, address implementation, bytes32 configHash) = _readIdentity(target); if (!ok) revert UnsafeProxy(); // No target getter can prove which implementation a proxy will actually dispatch to. // Keep the legacy fields in the signed struct for wire compatibility, but require the // only reviewed profile: an immutable direct target with no implementation indirection. if ( implementation != address(0) || expectedImplementation != address(0) || expectedImplementationCodehash != bytes32(0) ) revert UnsafeProxy(); if ( authority != expectedAuthority || implementation != expectedImplementation || configHash != expectedConfigHash ) revert TargetIdentityChanged(); _rejectDelegatingRuntime(target); } function _readIdentity(address target) private view returns (bool ok, address authority, address implementation, bytes32 configHash) { bytes memory result; (ok, result) = target.staticcall(abi.encodeCall(ICapabilityTargetV3.capabilityTargetConfig, ())); if (!ok || result.length != 96) return (false, address(0), address(0), bytes32(0)); (authority, implementation, configHash) = abi.decode(result, (address, address, bytes32)); } function _structHash(Capability calldata c) private pure returns (bytes32) { return keccak256( abi.encode( CAPABILITY_TYPEHASH, c.principal, c.session, c.chainId, c.target, c.targetCodehash, c.implementation, c.implementationCodehash, c.targetAuthority, c.selector, c.callDataHash, c.callDataSchemaHash, c.targetConfigHash, c.callValue, c.maxCalls, c.notBefore, c.notAfter, c.nonce ) ); } }