// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /** * @title CryptOpsGuard * @notice Non-custodial Safe{Wallet} (Gnosis Safe) Pre-Broadcast Transaction Guard & Policy Firewall. * @dev Enforces four deterministic on-chain invariants before any multisig or AI-agent transaction executes: * 1. Blocks unallowlisted DelegateCall operations (mitigates Bybit/Radiant-class implementation hijacks). * 2. Blocks infinite ERC-20 approve(address, uint256.max) and Permit2 drains to unverified spenders. * 3. Enforces per-transaction and rolling 24-hour native/token velocity limits. * 4. Emits a canonical SHA-256 / Keccak-256 pre-broadcast audit receipt for ProofAudit verification. * * Built by StratEdge Workflow Systems LLC (https://cryptops.ai | https://www.stratedgeworkflow.com/trust) */ interface IERC165 { function supportsInterface(bytes4 interfaceId) external view returns (bool); } abstract contract Enum { enum Operation { Call, DelegateCall } } interface ITransactionGuard is IERC165 { function checkTransaction( address to, uint256 value, bytes memory data, Enum.Operation operation, uint256 safeTxGas, uint256 baseGas, uint256 gasPrice, address gasToken, address payable refundReceiver, bytes memory signatures, address msgSender ) external; function checkAfterExecution(bytes32 txHash, bool success) external; } contract CryptOpsGuard is ITransactionGuard { // ERC-20 & Proxy Function Selectors bytes4 public constant ERC20_APPROVE = 0x095ea7b3; // approve(address,uint256) bytes4 public constant ERC20_TRANSFER = 0xa9059cbb; // transfer(address,uint256) bytes4 public constant ERC20_TRANSFER_FROM = 0x23b872dd; // transferFrom(address,address,uint256) bytes4 public constant PROXY_UPGRADE_TO = 0x3659cfe6; // upgradeTo(address) address public immutable owner; uint256 public maxSingleTransferWei; uint256 public dailyVelocityCapWei; uint256 public currentDayBucket; uint256 public currentDaySpentWei; mapping(address => bool) public allowlistedDestinations; mapping(address => bool) public allowlistedDelegateCallTargets; event CryptOpsDecisionReceipt( bytes32 indexed receiptDigest, address indexed safe, address indexed destination, uint256 value, bytes4 selector, Enum.Operation operation, string disposition ); error CryptOpsBlockedDelegateCall(address target); error CryptOpsBlockedProxyUpgrade(address target); error CryptOpsBlockedInfiniteApproval(address token, address spender, uint256 allowance); error CryptOpsBlockedUnallowlistedSpender(address token, address spender); error CryptOpsExceededSingleTransferCap(uint256 requested, uint256 maxAllowed); error CryptOpsExceededDailyVelocityCap(uint256 projectedDailyTotal, uint256 dailyCap); modifier onlyOwner() { require(msg.sender == owner, "CryptOpsGuard: caller is not owner"); _; } constructor(uint256 _maxSingleTransferWei, uint256 _dailyVelocityCapWei) { owner = msg.sender; maxSingleTransferWei = _maxSingleTransferWei; dailyVelocityCapWei = _dailyVelocityCapWei; } function setDestinationAllowlist(address target, bool allowed) external onlyOwner { allowlistedDestinations[target] = allowed; } function setDelegateCallAllowlist(address target, bool allowed) external onlyOwner { allowlistedDelegateCallTargets[target] = allowed; } function setPolicyLimits(uint256 _maxSingleTransferWei, uint256 _dailyVelocityCapWei) external onlyOwner { maxSingleTransferWei = _maxSingleTransferWei; dailyVelocityCapWei = _dailyVelocityCapWei; } function checkTransaction( address to, uint256 value, bytes memory data, Enum.Operation operation, uint256, uint256, uint256, address, address payable, bytes memory, address msgSender ) external override { // Gate 1: Block unallowlisted DelegateCall operations (Bybit / Safe implementation hijack defense) if (operation == Enum.Operation.DelegateCall && !allowlistedDelegateCallTargets[to]) { revert CryptOpsBlockedDelegateCall(to); } bytes4 selector = bytes4(0); if (data.length >= 4) { assembly { selector := mload(add(data, 32)) } } // Gate 2: Block unauthorized proxy implementation upgrades if (selector == PROXY_UPGRADE_TO && !allowlistedDestinations[to]) { revert CryptOpsBlockedProxyUpgrade(to); } // Gate 3: Inspect ERC-20 approve(address spender, uint256 amount) calldata if (selector == ERC20_APPROVE && data.length >= 68) { address spender; uint256 allowanceAmount; assembly { spender := mload(add(data, 36)) allowanceAmount := mload(add(data, 68)) } if (!allowlistedDestinations[spender]) { revert CryptOpsBlockedUnallowlistedSpender(to, spender); } if (allowanceAmount == type(uint256).max) { revert CryptOpsBlockedInfiniteApproval(to, spender, allowanceAmount); } } // Gate 4: Enforce per-transaction and 24-hour rolling velocity limits if (value > maxSingleTransferWei && !allowlistedDestinations[to]) { revert CryptOpsExceededSingleTransferCap(value, maxSingleTransferWei); } uint256 today = block.timestamp / 1 days; if (today > currentDayBucket) { currentDayBucket = today; currentDaySpentWei = 0; } if (currentDaySpentWei + value > dailyVelocityCapWei) { revert CryptOpsExceededDailyVelocityCap(currentDaySpentWei + value, dailyVelocityCapWei); } currentDaySpentWei += value; bytes32 digest = sha256( abi.encodePacked(msg.sender, to, value, selector, uint8(operation), msgSender, block.timestamp) ); emit CryptOpsDecisionReceipt(digest, msg.sender, to, value, selector, operation, "CLEAR"); } function checkAfterExecution(bytes32, bool) external override {} function supportsInterface(bytes4 interfaceId) external pure override returns (bool) { return interfaceId == type(ITransactionGuard).interfaceId || interfaceId == type(IERC165).interfaceId; } }