// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import "@openzeppelin/contracts/access/AccessControl.sol"; import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; /** * @title BridgeVerifier * @notice Verifies cross-chain proofs and attestor signatures for bridge operations * @dev Supports multi-sig quorum for attestations */ contract BridgeVerifier is AccessControl, EIP712 { using ECDSA for bytes32; bytes32 public constant ATTESTOR_ROLE = keccak256("ATTESTOR_ROLE"); bytes32 private constant ATTESTATION_TYPEHASH = keccak256("Attestation(bytes32 transferId,bytes32 proofHash,uint256 nonce,uint256 deadline)"); struct Attestation { bytes32 transferId; bytes32 proofHash; uint256 nonce; uint256 deadline; bytes signature; } struct AttestorConfig { address attestor; bool enabled; uint256 weight; // Weight for quorum calculation } mapping(address => AttestorConfig) public attestors; mapping(bytes32 => mapping(address => bool)) public attestations; // transferId -> attestor -> attested mapping(bytes32 => uint256) public attestationWeights; // transferId -> total weight mapping(uint256 => bool) public usedNonces; address[] public attestorList; uint256 public totalAttestorWeight; uint256 public quorumThreshold; // Minimum weight required (in basis points, 10000 = 100%) event AttestationSubmitted( bytes32 indexed transferId, address indexed attestor, bytes32 proofHash ); event AttestationVerified( bytes32 indexed transferId, uint256 totalWeight, bool quorumMet ); event AttestorAdded(address indexed attestor, uint256 weight); event AttestorRemoved(address indexed attestor); event AttestorUpdated(address indexed attestor, bool enabled, uint256 weight); event QuorumThresholdUpdated(uint256 oldThreshold, uint256 newThreshold); error ZeroAddress(); error AttestorNotFound(); error InvalidSignature(); error NonceAlreadyUsed(); error DeadlineExpired(); error InvalidQuorum(); error AlreadyAttested(); constructor( address admin, uint256 _quorumThreshold ) EIP712("BridgeVerifier", "1") { _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(ATTESTOR_ROLE, admin); if (_quorumThreshold > 10000) revert InvalidQuorum(); quorumThreshold = _quorumThreshold; } /** * @notice Add an attestor * @param attestor Attestor address * @param weight Weight for quorum calculation */ function addAttestor( address attestor, uint256 weight ) external onlyRole(DEFAULT_ADMIN_ROLE) { if (attestor == address(0)) revert ZeroAddress(); if (attestors[attestor].attestor != address(0)) revert AttestorNotFound(); attestors[attestor] = AttestorConfig({ attestor: attestor, enabled: true, weight: weight }); attestorList.push(attestor); totalAttestorWeight += weight; emit AttestorAdded(attestor, weight); } /** * @notice Remove an attestor * @param attestor Attestor address */ function removeAttestor(address attestor) external onlyRole(DEFAULT_ADMIN_ROLE) { AttestorConfig memory config = attestors[attestor]; if (config.attestor == address(0)) revert AttestorNotFound(); totalAttestorWeight -= config.weight; delete attestors[attestor]; // Remove from list for (uint256 i = 0; i < attestorList.length; i++) { if (attestorList[i] == attestor) { attestorList[i] = attestorList[attestorList.length - 1]; attestorList.pop(); break; } } emit AttestorRemoved(attestor); } /** * @notice Update attestor configuration * @param attestor Attestor address * @param enabled Enabled status * @param weight New weight */ function updateAttestor( address attestor, bool enabled, uint256 weight ) external onlyRole(DEFAULT_ADMIN_ROLE) { AttestorConfig storage config = attestors[attestor]; if (config.attestor == address(0)) revert AttestorNotFound(); uint256 oldWeight = config.weight; totalAttestorWeight = totalAttestorWeight - oldWeight + weight; config.enabled = enabled; config.weight = weight; emit AttestorUpdated(attestor, enabled, weight); } /** * @notice Update quorum threshold * @param newThreshold New threshold in basis points */ function setQuorumThreshold(uint256 newThreshold) external onlyRole(DEFAULT_ADMIN_ROLE) { if (newThreshold > 10000) revert InvalidQuorum(); uint256 oldThreshold = quorumThreshold; quorumThreshold = newThreshold; emit QuorumThresholdUpdated(oldThreshold, newThreshold); } /** * @notice Submit an attestation * @param attestation Attestation with signature */ function submitAttestation(Attestation calldata attestation) external { AttestorConfig memory config = attestors[msg.sender]; if (config.attestor == address(0) || !config.enabled) revert AttestorNotFound(); if (block.timestamp > attestation.deadline) revert DeadlineExpired(); if (usedNonces[attestation.nonce]) revert NonceAlreadyUsed(); if (attestations[attestation.transferId][msg.sender]) revert AlreadyAttested(); // Verify signature bytes32 structHash = keccak256( abi.encode( ATTESTATION_TYPEHASH, attestation.transferId, attestation.proofHash, attestation.nonce, attestation.deadline ) ); bytes32 hash = _hashTypedDataV4(structHash); if (hash.recover(attestation.signature) != msg.sender) { revert InvalidSignature(); } usedNonces[attestation.nonce] = true; attestations[attestation.transferId][msg.sender] = true; attestationWeights[attestation.transferId] += config.weight; emit AttestationSubmitted(attestation.transferId, msg.sender, attestation.proofHash); } /** * @notice Verify if quorum is met for a transfer * @param transferId Transfer identifier * @return quorumMet Whether quorum threshold is met * @return totalWeight Total weight of attestations * @return requiredWeight Required weight for quorum */ function verifyQuorum( bytes32 transferId ) external view returns (bool quorumMet, uint256 totalWeight, uint256 requiredWeight) { totalWeight = attestationWeights[transferId]; requiredWeight = (totalAttestorWeight * quorumThreshold) / 10000; quorumMet = totalWeight >= requiredWeight; return (quorumMet, totalWeight, requiredWeight); } /** * @notice Check if an attestor has attested to a transfer * @param transferId Transfer identifier * @param attestor Attestor address * @return True if attested */ function hasAttested( bytes32 transferId, address attestor ) external view returns (bool) { return attestations[transferId][attestor]; } /** * @notice Get all attestors * @return Array of attestor addresses */ function getAllAttestors() external view returns (address[] memory) { return attestorList; } }