// SPDX-License-Identifier: MIT pragma solidity 0.8.24; import { EIP712BaseV3 } from "../signatures/EIP712BaseV3.sol"; import { SignatureVerifierV3 } from "../signatures/SignatureVerifierV3.sol"; /// @title EvidenceRegistryV3 /// @notice Attestation registry with mandated fields from assignment ยง8: /// attester, attester authority, subject, bond/mandate, evidence /// schema, evidence commitment, observation period, timestamp, nonce, /// signature, expiration, independence classification. /// Duplicate attestations within the same round are rejected. contract EvidenceRegistryV3 is EIP712BaseV3 { bytes32 public constant GOVERNANCE_POLICY_MODULE_ID = keccak256("Vaultfire.BaseV3.EvidenceRegistry"); struct Attestation { address attester; bytes32 attesterAuthority; // authority tag hash (e.g. governance policy id) bytes32 subject; // agentId/wallet hash bytes32 bondOrMandate; // bondId or mandateHash bytes32-cast bytes32 evidenceSchema; // schema hash bytes32 evidenceCommitment; // content hash or merkle root uint64 observationStart; uint64 observationEnd; uint64 timestamp; uint64 expiresAt; uint96 nonce; uint8 independenceClass; // 0=self, 1=econ-backed, 2=independent, 3=disputed, 4=expired, 5=legacy, 6=cross-chain uint256 round; // uniqueness scope (same round + same subject + same attester = dup) } bytes32 public constant ATTESTATION_TYPEHASH = keccak256( "Attestation(address attester,bytes32 attesterAuthority,bytes32 subject,bytes32 bondOrMandate,bytes32 evidenceSchema,bytes32 evidenceCommitment,uint64 observationStart,uint64 observationEnd,uint64 timestamp,uint64 expiresAt,uint96 nonce,uint8 independenceClass,uint256 round)" ); error DuplicateAttestation(); error ExpiredAttestation(); error BadSignature(); error UnauthorizedAttester(); error ZeroAddress(); error NonceUsed(); error BadAttestation(); address public owner; mapping(address => bool) public authorizedAttester; mapping(address => mapping(uint96 => bool)) public nonceUsed; // (round, subject, attester) => attestationHash mapping(uint256 => mapping(bytes32 => mapping(address => bytes32))) public roundRecord; event AttestationRegisteredV3( bytes32 indexed attestationHash, address indexed attester, bytes32 indexed subject, bytes32 bondOrMandate, uint256 round, uint8 independenceClass ); event AttesterSetV3(address indexed a, bool authorized); event OwnerSetV3(address indexed prev, address indexed curr); constructor(address _owner) EIP712BaseV3("3.0.0") { if (_owner == address(0)) revert ZeroAddress(); owner = _owner; } modifier onlyOwner() { require(msg.sender == owner, "not owner"); _; } function setAttester(address a, bool v) external onlyOwner { authorizedAttester[a] = v; emit AttesterSetV3(a, v); } function hashAttestation(Attestation calldata a) public pure returns (bytes32) { return keccak256( abi.encode( ATTESTATION_TYPEHASH, a.attester, a.attesterAuthority, a.subject, a.bondOrMandate, a.evidenceSchema, a.evidenceCommitment, a.observationStart, a.observationEnd, a.timestamp, a.expiresAt, a.nonce, a.independenceClass, a.round ) ); } function register(Attestation calldata a, bytes calldata sig) external returns (bytes32 attestationHash) { if (!authorizedAttester[a.attester]) revert UnauthorizedAttester(); if ( a.attester == address(0) || a.subject == bytes32(0) || a.bondOrMandate == bytes32(0) || a.attesterAuthority == bytes32(0) || a.evidenceSchema == bytes32(0) || a.evidenceCommitment == bytes32(0) || a.independenceClass > 6 || a.observationEnd < a.observationStart || a.timestamp < a.observationStart || a.timestamp > a.observationEnd || a.observationEnd > block.timestamp || a.timestamp > block.timestamp ) { revert BadAttestation(); } if (a.expiresAt != 0 && a.expiresAt <= block.timestamp) revert ExpiredAttestation(); if (nonceUsed[a.attester][a.nonce]) revert NonceUsed(); if (roundRecord[a.round][a.subject][a.attester] != bytes32(0)) revert DuplicateAttestation(); attestationHash = hashAttestation(a); bytes32 digest = _hashTypedData(attestationHash); if (!SignatureVerifierV3.verify(a.attester, digest, sig)) revert BadSignature(); nonceUsed[a.attester][a.nonce] = true; roundRecord[a.round][a.subject][a.attester] = attestationHash; emit AttestationRegisteredV3( attestationHash, a.attester, a.subject, a.bondOrMandate, a.round, a.independenceClass ); } }