// SPDX-License-Identifier: MIT pragma solidity ^0.8.24; import { FHE, euint64, euint8, ebool, eaddress, externalEuint64, externalEuint8, externalEbool, externalEaddress } from "@fhevm/solidity/lib/FHE.sol"; import { ZamaEthereumConfig } from "@fhevm/solidity/config/ZamaConfig.sol"; /** * @title Escrow * @notice Private escrow with encrypted amounts - secure P2P trades * @dev Escrow amounts are hidden until release, protecting trade privacy * * Use Cases: * - P2P trades without revealing amounts * - Freelance payments with milestone privacy * - Private real estate deposits * * FHE Operations Used: * - add: Accumulate deposits * - sub: Partial releases * - gte: Verify sufficient funds * - eq: Exact amount matching * - select: Conditional transfers */ contract Escrow is ZamaEthereumConfig { // ============ Errors ============ error EscrowNotFound(); error NotAuthorized(); error EscrowNotFunded(); error EscrowAlreadyComplete(); error EscrowInDispute(); error InsufficientFunds(); error CannotSelfEscrow(); // ============ Events ============ event EscrowCreated(uint256 indexed escrowId, address indexed buyer, address indexed seller); event EscrowFunded(uint256 indexed escrowId); event FundsReleased(uint256 indexed escrowId, address indexed to); event FundsRefunded(uint256 indexed escrowId, address indexed to); event DisputeRaised(uint256 indexed escrowId, address indexed by); event DisputeResolved(uint256 indexed escrowId, address indexed winner); // ============ Enums ============ enum EscrowState { Created, Funded, Released, Refunded, Disputed } // ============ Structs ============ struct EscrowRecord { address buyer; address seller; address arbiter; euint64 amount; // Encrypted escrow amount euint64 funded; // Encrypted funded amount EscrowState state; string description; uint256 deadline; uint256 createdAt; } // ============ State Variables ============ mapping(uint256 => EscrowRecord) public _escrows; uint256 public _escrowCount; uint256 public escrowFeeRate; // Fee in basis points address public feeRecipient; // ============ Modifiers ============ modifier escrowExists(uint256 escrowId) { if (escrowId >= _escrowCount) revert EscrowNotFound(); _; } modifier onlyParty(uint256 escrowId) { EscrowRecord storage e = _escrows[escrowId]; if (msg.sender != e.buyer && msg.sender != e.seller && msg.sender != e.arbiter) revert NotAuthorized(); _; } // ============ Constructor ============ constructor(uint256 _escrowFeeRate, address _feeRecipient) { escrowFeeRate = _escrowFeeRate; feeRecipient = _feeRecipient; } // ============ External Functions ============ /** * @notice Create a new escrow * @param seller The seller/recipient address * @param arbiter Optional dispute resolver * @param encryptedAmount Encrypted escrow amount * @param description What the escrow is for * @param deadline When the escrow expires */ function createEscrow( address seller, address arbiter, externalEuint64 encryptedAmount, bytes calldata inputProof, string calldata description, uint256 deadline ) external returns (uint256) { if (seller == msg.sender) revert CannotSelfEscrow(); uint256 escrowId = _escrowCount++; euint64 amount = FHE.fromExternal(encryptedAmount, inputProof); _escrows[escrowId] = EscrowRecord({ buyer: msg.sender, seller: seller, arbiter: arbiter, amount: amount, funded: FHE.asEuint64(0), state: EscrowState.Created, description: description, deadline: deadline, createdAt: block.timestamp }); // Allow contract to use encrypted values FHE.allowThis(_escrows[escrowId].funded); FHE.allowThis(amount); FHE.allow(amount, msg.sender); FHE.allow(amount, seller); if (arbiter != address(0)) { FHE.allow(amount, arbiter); } emit EscrowCreated(escrowId, msg.sender, seller); return escrowId; } /** * @notice Fund the escrow with encrypted amount * @param escrowId The escrow to fund * @param encryptedAmount Encrypted funding amount */ function fundEscrow(uint256 escrowId, externalEuint64 encryptedAmount, bytes calldata inputProof) external escrowExists(escrowId) { EscrowRecord storage e = _escrows[escrowId]; require(e.state == EscrowState.Created, "Cannot fund"); require(msg.sender == e.buyer, "Only buyer"); euint64 fundAmount = FHE.fromExternal(encryptedAmount, inputProof); e.funded = FHE.add(e.funded, fundAmount); // Check if fully funded ebool fullyFunded = FHE.ge(e.funded, e.amount); // Update state if fully funded (simplified - in production use callback) FHE.allowThis(e.funded); FHE.allowThis(fullyFunded); e.state = EscrowState.Funded; emit EscrowFunded(escrowId); } /** * @notice Release funds to seller (buyer confirms delivery) * @param escrowId The escrow to release */ function releaseFunds(uint256 escrowId) external escrowExists(escrowId) { EscrowRecord storage e = _escrows[escrowId]; if (e.state != EscrowState.Funded) revert EscrowNotFunded(); if (e.state == EscrowState.Disputed) revert EscrowInDispute(); require(msg.sender == e.buyer, "Only buyer can release"); e.state = EscrowState.Released; // Allow seller to claim the funds FHE.allow(e.funded, e.seller); emit FundsReleased(escrowId, e.seller); } /** * @notice Refund funds to buyer (seller cancels or deadline passed) * @param escrowId The escrow to refund */ function refund(uint256 escrowId) external escrowExists(escrowId) { EscrowRecord storage e = _escrows[escrowId]; if (e.state != EscrowState.Funded) revert EscrowNotFunded(); bool canRefund = msg.sender == e.seller || (block.timestamp > e.deadline && msg.sender == e.buyer); require(canRefund, "Cannot refund"); e.state = EscrowState.Refunded; // Allow buyer to reclaim FHE.allow(e.funded, e.buyer); emit FundsRefunded(escrowId, e.buyer); } /** * @notice Raise a dispute * @param escrowId The escrow to dispute */ function dispute(uint256 escrowId) external escrowExists(escrowId) onlyParty(escrowId) { EscrowRecord storage e = _escrows[escrowId]; if (e.state != EscrowState.Funded) revert EscrowNotFunded(); require(e.arbiter != address(0), "No arbiter set"); e.state = EscrowState.Disputed; emit DisputeRaised(escrowId, msg.sender); } /** * @notice Resolve dispute (arbiter only) * @param escrowId The escrow in dispute * @param releaseToSeller true = seller wins, false = buyer wins */ function resolveDispute(uint256 escrowId, bool releaseToSeller) external escrowExists(escrowId) { EscrowRecord storage e = _escrows[escrowId]; require(e.state == EscrowState.Disputed, "Not in dispute"); require(msg.sender == e.arbiter, "Only arbiter"); if (releaseToSeller) { e.state = EscrowState.Released; FHE.allow(e.funded, e.seller); emit DisputeResolved(escrowId, e.seller); } else { e.state = EscrowState.Refunded; FHE.allow(e.funded, e.buyer); emit DisputeResolved(escrowId, e.buyer); } } // ============ View Functions ============ /** * @notice Get escrow info */ function getEscrow(uint256 escrowId) external view returns ( address buyer, address seller, address arbiter, EscrowState state, string memory description, uint256 deadline ) { EscrowRecord storage e = _escrows[escrowId]; return (e.buyer, e.seller, e.arbiter, e.state, e.description, e.deadline); } /** * @notice Get total escrow count */ function getEscrowCount() external view returns (uint256) { return _escrowCount; } /** * @notice Check if escrow is active */ function isActive(uint256 escrowId) external view returns (bool) { EscrowRecord storage e = _escrows[escrowId]; return e.state == EscrowState.Created || e.state == EscrowState.Funded; } // ============ Internal Functions ============ }