// SPDX-License-Identifier: MIT pragma solidity 0.8.23; /** * @title AereTravelRuleHashRegistry — on-chain anchor for FATF Travel Rule * message exchanges (Notabene-compatible) * @notice The FATF Travel Rule requires VASPs to exchange identity payloads * for transfers above a jurisdiction-set threshold (typically * USD 1,000 for the US, EUR 1,000 for the EU MiCA regime). The * payloads themselves are confidential and exchanged off-chain via * a message bus like Notabene IVMS-101. * * This contract is the on-chain ANCHOR for the off-chain exchange. * The originating VASP commits a keccak256 hash of the IVMS-101 * payload before the transaction goes through, and the beneficiary * VASP later verifies that hash matches what was exchanged. The * on-chain anchor: * 1. Proves the payload existed at a specific block. * 2. Cannot be retroactively altered (commitments are * append-only). * 3. Is auditable by regulators without revealing the payload. * * No payload data is stored on chain. No identity is revealed. * * @dev PHASE 1 — minimal viable Travel Rule anchor. * - `commit(payloadHash, beneficiaryVasp, threshold)` permissionless * callable by the originator VASP. The VASP self-identifies via * the msg.sender; consumers can query "did VASP X commit to a * payload referencing this txid?" via the events index. * - `acknowledge(commitmentId)` callable by the beneficiary VASP * to mark receipt + verification of the off-chain payload. * - `dispute(commitmentId, reason)` callable by the beneficiary * if the payload doesn't match the on-chain hash. * * NO admin. NO Foundation interception. NO custody. * * INTEGRATIONS: * - Notabene SDK: the JS SDK on each VASP calls * contract.commit(hash, recipientVasp, threshold) automatically * when the user initiates a > $1k transfer. * - Compliance dashboards: index Commitments + Acknowledgements * + Disputes by VASP address for audit trail. * * AERE Foundation Seychelles is registered as a VASP and uses this * registry for its own institutional flows (e.g. via the * SettlementHub for BUIDL/USDY/OUSG receipts). */ contract AereTravelRuleHashRegistry { enum State { Committed, Acknowledged, Disputed } struct Commitment { bytes32 payloadHash; address originatorVasp; address beneficiaryVasp; uint256 threshold; // jurisdiction threshold in USD units (cents) uint64 committedAt; uint64 statusUpdatedAt; State state; string disputeReason; } /// @notice commitmentId → Commitment mapping(uint256 => Commitment) public commitments; uint256 public nextCommitmentId; /// @notice Per-VASP commitment index (for audit retrieval). mapping(address => uint256[]) public vaspCommitments; event Committed(uint256 indexed id, address indexed originator, address indexed beneficiary, bytes32 payloadHash, uint256 threshold); event Acknowledged(uint256 indexed id, address indexed beneficiary); event Disputed(uint256 indexed id, address indexed beneficiary, string reason); error UnknownCommitment(); error NotBeneficiary(); error AlreadyAcknowledged(); error AlreadyDisputed(); error ZeroAddress(); function commit(bytes32 payloadHash, address beneficiaryVasp, uint256 threshold) external returns (uint256 id) { if (beneficiaryVasp == address(0)) revert ZeroAddress(); id = nextCommitmentId++; commitments[id] = Commitment({ payloadHash: payloadHash, originatorVasp: msg.sender, beneficiaryVasp: beneficiaryVasp, threshold: threshold, committedAt: uint64(block.timestamp), statusUpdatedAt: uint64(block.timestamp), state: State.Committed, disputeReason: "" }); vaspCommitments[msg.sender].push(id); vaspCommitments[beneficiaryVasp].push(id); emit Committed(id, msg.sender, beneficiaryVasp, payloadHash, threshold); } function acknowledge(uint256 id) external { Commitment storage c = commitments[id]; if (c.committedAt == 0) revert UnknownCommitment(); if (msg.sender != c.beneficiaryVasp) revert NotBeneficiary(); if (c.state == State.Acknowledged) revert AlreadyAcknowledged(); if (c.state == State.Disputed) revert AlreadyDisputed(); c.state = State.Acknowledged; c.statusUpdatedAt = uint64(block.timestamp); emit Acknowledged(id, msg.sender); } function dispute(uint256 id, string calldata reason) external { Commitment storage c = commitments[id]; if (c.committedAt == 0) revert UnknownCommitment(); if (msg.sender != c.beneficiaryVasp) revert NotBeneficiary(); if (c.state == State.Disputed) revert AlreadyDisputed(); c.state = State.Disputed; c.disputeReason = reason; c.statusUpdatedAt = uint64(block.timestamp); emit Disputed(id, msg.sender, reason); } /* ----------------------------------- views ---------------------------------- */ function vaspCommitmentCount(address vasp) external view returns (uint256) { return vaspCommitments[vasp].length; } }