// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; /// @title ConsentRegistry /// @author Binary Helix /// @notice Public, tamper-evident record that a MyHelix consent grant existed, when it expires and whether the /// patient revoked it. Only hashes and timestamps are stored - never health data, scopes, names or DIDs. /// @dev The grantor is `msg.sender`: the patient's ERC-4337 smart account (passkey-owned, P-256 signatures /// verified via the RIP-7212 precompile, gas sponsored by a paymaster). The signed receipt itself /// ({type:"MyHelixConsent", grantId, grantor, grantee, scopeHash, purpose, issuedAt, expiresAt, nonce}) /// stays off-chain; `receiptHash` = SHA-256(canonical JSON of the receipt). /// `grantee` is an opaque 32-byte commitment to the recipient, e.g. SHA-256(granteeDID || receipt nonce), /// so the chain does not publicly reveal which clinic a patient shares with. /// Revocation stops future access by the wallet/relay; it cannot claw back data already viewed or saved. /// Reference implementation: not audited. contract ConsentRegistry { /// @notice On-chain view of a grant. struct Grant { address grantor; uint64 issuedAt; uint64 expiresAt; uint64 revokedAt; bytes32 grantee; bytes32 receiptHash; } /// @notice grantId => grant. grantId is random 32 bytes chosen by the wallet. mapping(bytes32 => Grant) private _grants; /// @notice Emitted when a grant is registered. event Granted(bytes32 indexed grantId, address indexed grantor, bytes32 grantee, bytes32 receiptHash, uint64 expiresAt); /// @notice Emitted when a grant is revoked by its grantor. event Revoked(bytes32 indexed grantId, address indexed grantor, uint64 revokedAt); error GrantExists(bytes32 grantId); error UnknownGrant(bytes32 grantId); error NotGrantor(); error AlreadyRevoked(bytes32 grantId); error InvalidExpiry(); error EmptyReceipt(); /// @notice Register a consent grant. /// @param grantId Random 32-byte grant identifier (matches the receipt's grantId). /// @param grantee Opaque 32-byte commitment to the grantee (see contract docs). /// @param receiptHash SHA-256 of the canonical signed receipt. /// @param expiresAt Unix time (seconds) after which the grant is inactive. Must be in the future. function grant(bytes32 grantId, bytes32 grantee, bytes32 receiptHash, uint64 expiresAt) external { if (_grants[grantId].grantor != address(0)) revert GrantExists(grantId); if (receiptHash == bytes32(0)) revert EmptyReceipt(); if (expiresAt <= block.timestamp) revert InvalidExpiry(); _grants[grantId] = Grant({ grantor: msg.sender, issuedAt: uint64(block.timestamp), expiresAt: expiresAt, revokedAt: 0, grantee: grantee, receiptHash: receiptHash }); emit Granted(grantId, msg.sender, grantee, receiptHash, expiresAt); } /// @notice Revoke a grant. Only the original grantor (smart account) can revoke. /// @dev Revoking an already-expired grant is allowed, so the audit trail can show an explicit revocation. function revoke(bytes32 grantId) external { Grant storage g = _grants[grantId]; if (g.grantor == address(0)) revert UnknownGrant(grantId); if (g.grantor != msg.sender) revert NotGrantor(); if (g.revokedAt != 0) revert AlreadyRevoked(grantId); g.revokedAt = uint64(block.timestamp); emit Revoked(grantId, msg.sender, g.revokedAt); } /// @notice True if the grant exists, is not revoked and has not expired. function isActive(bytes32 grantId) external view returns (bool) { Grant storage g = _grants[grantId]; return g.grantor != address(0) && g.revokedAt == 0 && block.timestamp < g.expiresAt; } /// @notice Full on-chain grant record (zeroed struct if unknown). function getGrant(bytes32 grantId) external view returns (Grant memory) { return _grants[grantId]; } /// @notice Check that an off-chain receipt matches what was registered. /// @param grantId Grant identifier. /// @param receiptHash SHA-256 of the canonical receipt presented off-chain. function matchesReceipt(bytes32 grantId, bytes32 receiptHash) external view returns (bool) { Grant storage g = _grants[grantId]; return g.grantor != address(0) && g.receiptHash == receiptHash; } }