// SPDX-License-Identifier: MIT pragma solidity ^0.8.20; import {AccessControl} from "@openzeppelin/contracts/access/AccessControl.sol"; import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; /** * @title ReserveCommitmentStore * @notice Stores attested reserve R per line with TTL, merkle evidence root, versioning, primary + mirror, * and optional threshold ECDSA attestation for commits. */ contract ReserveCommitmentStore is AccessControl { bytes32 public constant RESERVE_COMMITTER_ROLE = keccak256("RESERVE_COMMITTER_ROLE"); bytes32 public constant ATTESTATION_ADMIN_ROLE = keccak256("ATTESTATION_ADMIN_ROLE"); /// @notice EIP-191 structured preimage prefix for attested commits (see commitReserveAttested). bytes32 public constant ATTESTATION_TYPEHASH = keccak256("OMNLReserveCommit(uint256 chainId,address store,bytes32 lineId,uint256 R,uint256 validUntil,bytes32 evidenceHash,bytes32 merkleRoot,uint256 nonce)"); struct Commitment { uint256 R; uint256 validUntil; bytes32 evidenceHash; bytes32 merkleRoot; uint256 version; } address public mirrorReceiver; mapping(bytes32 lineId => Commitment) private _commitments; mapping(bytes32 lineId => uint256) public lineAttestationNonce; mapping(address => bool) public isAttestationSigner; uint256 public attestationThreshold; event ReserveCommitted( bytes32 indexed lineId, uint256 R, uint256 validUntil, bytes32 evidenceHash, bytes32 merkleRoot, uint256 version, address indexed by ); event MirrorReceiverUpdated(address indexed oldReceiver, address indexed newReceiver); event AttestationSignersUpdated(uint256 threshold); event AttestationSignerSet(address indexed signer, bool active); constructor(address admin) { require(admin != address(0), "ReserveCommitmentStore: zero admin"); _grantRole(DEFAULT_ADMIN_ROLE, admin); _grantRole(RESERVE_COMMITTER_ROLE, admin); _grantRole(ATTESTATION_ADMIN_ROLE, admin); } function setMirrorReceiver(address receiver) external onlyRole(DEFAULT_ADMIN_ROLE) { address old = mirrorReceiver; mirrorReceiver = receiver; emit MirrorReceiverUpdated(old, receiver); } function setAttestationSigner(address signer, bool active) external onlyRole(ATTESTATION_ADMIN_ROLE) { isAttestationSigner[signer] = active; emit AttestationSignerSet(signer, active); } function setAttestationThreshold(uint256 threshold) external onlyRole(ATTESTATION_ADMIN_ROLE) { attestationThreshold = threshold; emit AttestationSignersUpdated(threshold); } /// @notice Primary chain (or designated committer) updates reserves. function commitReserve( bytes32 lineId, uint256 R, uint256 validUntil, bytes32 evidenceHash, bytes32 merkleRoot ) external onlyRole(RESERVE_COMMITTER_ROLE) { _commit(lineId, R, validUntil, evidenceHash, merkleRoot, msg.sender); } /// @notice Threshold ECDSA attestation (any EOA may submit once enough distinct signers included). function commitReserveAttested( bytes32 lineId, uint256 R, uint256 validUntil, bytes32 evidenceHash, bytes32 merkleRoot, uint256 nonce, bytes[] calldata signatures ) external { require(attestationThreshold > 0, "ReserveCommitmentStore: attestation off"); require(nonce == lineAttestationNonce[lineId], "ReserveCommitmentStore: nonce"); bytes32 digest = keccak256( abi.encode( ATTESTATION_TYPEHASH, block.chainid, address(this), lineId, R, validUntil, evidenceHash, merkleRoot, nonce ) ); bytes32 ethSignedMessageHash = keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", digest)); uint256 n = signatures.length; require(n >= attestationThreshold, "ReserveCommitmentStore: sig count"); address[] memory seen = new address[](n); uint256 uniq = 0; for (uint256 i = 0; i < n; i++) { address recovered = ECDSA.recover(ethSignedMessageHash, signatures[i]); require(isAttestationSigner[recovered], "ReserveCommitmentStore: not signer"); for (uint256 j = 0; j < uniq; j++) { require(seen[j] != recovered, "ReserveCommitmentStore: duplicate signer"); } seen[uniq] = recovered; unchecked { ++uniq; } } require(uniq >= attestationThreshold, "ReserveCommitmentStore: threshold"); lineAttestationNonce[lineId] = nonce + 1; _commit(lineId, R, validUntil, evidenceHash, merkleRoot, msg.sender); } function _commit( bytes32 lineId, uint256 R, uint256 validUntil, bytes32 evidenceHash, bytes32 merkleRoot, address by ) internal { Commitment storage c = _commitments[lineId]; unchecked { c.version += 1; } c.R = R; c.validUntil = validUntil; c.evidenceHash = evidenceHash; c.merkleRoot = merkleRoot; emit ReserveCommitted(lineId, R, validUntil, evidenceHash, merkleRoot, c.version, by); } /// @notice Applied by OMNLMirrorReceiver after CCIP validation (monotonic version). function applyMirrorCommit( bytes32 lineId, uint256 R, uint256 validUntil, bytes32 evidenceHash, bytes32 merkleRoot, uint256 version ) external { require(msg.sender == mirrorReceiver, "ReserveCommitmentStore: only mirror"); Commitment storage c = _commitments[lineId]; require(version > c.version, "ReserveCommitmentStore: version"); c.version = version; c.R = R; c.validUntil = validUntil; c.evidenceHash = evidenceHash; c.merkleRoot = merkleRoot; emit ReserveCommitted(lineId, R, validUntil, evidenceHash, merkleRoot, version, msg.sender); } function getCommitment(bytes32 lineId) external view returns (Commitment memory) { return _commitments[lineId]; } }