aere-contracts/contracts/compliance/AereTravelRuleHashRegistry.sol
Aere Network a13a649b77
Some checks are pending
contracts-ci / Install (lockfile) → compile → full test suite (push) Waiting to run
contracts-ci / PQC known-answer tests (NIST vectors) (push) Waiting to run
contracts-ci / Coverage (scoped, with artifacts) (push) Waiting to run
Initial public release
Aere Network public source. Everything here can be checked against the live
chain (chain id 2800, https://rpc.aere.network).

Scope note, stated up front rather than buried: consensus on chain 2800 is
classical secp256k1 ECDSA QBFT. The post-quantum work in this repository is at
the signature, precompile, account and transport layers. Nothing here makes the
consensus post-quantum, and no document in it should be read as claiming so.
2026-07-20 01:02:37 +03:00

129 lines
5.5 KiB
Solidity

// 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;
}
}