Encrypted Escrow

Confidential escrow service with hidden transaction amounts and arbiters. Secures funds with encrypted amounts. Details remain hidden until release or refund, supporting multi-party dispute resolution.

circle-info

To run this example correctly, make sure the files are placed in the following directories:

  • .sol file → <your-project-root-dir>/contracts/

  • .ts file → <your-project-root-dir>/test/

This ensures Hardhat can compile and test your contracts as expected.

chevron-right🔐 FHE API Reference (6 items)hashtag

Types: ebool · euint64 · externalEuint64

Functions:

  • FHE.allow() - Grants PERMANENT permission for address to decrypt/use value

  • FHE.allowThis() - Grants contract permission to operate on ciphertext

  • FHE.fromExternal() - Validates and converts external encrypted input using inputProof

// SPDX-License-Identifier: BSD-3-Clause-Clear
pragma solidity ^0.8.24;

import {
    FHE,
    euint64,
    ebool,
    externalEuint64
} from "@fhevm/solidity/lib/FHE.sol";
import {ZamaEthereumConfig} from "@fhevm/solidity/config/ZamaConfig.sol";

/**
 * @notice Confidential escrow service with hidden transaction amounts and arbiters.
 *         Secures funds with encrypted amounts. Details remain hidden until
 *         release or refund, supporting multi-party dispute resolution.
 *
 * @dev Flow: createEscrow() → fundEscrow() → release()/requestRefund()/raiseDispute()
 */
contract EncryptedEscrow is ZamaEthereumConfig {
    enum EscrowState {
        Created, // Escrow created, awaiting deposit
        Funded, // Funds deposited
        Released, // Funds released to seller
        Refunded, // Funds returned to buyer
        Disputed // Under dispute resolution
    }

    struct Escrow {
        address buyer;
        address seller;
        address arbiter;
        euint64 encryptedAmount;
        uint256 depositedAmount;
        EscrowState state;
        uint256 createdAt;
        uint256 deadline;
    }

    /// Contract owner
    address public owner;

    /// Escrow ID counter
    uint256 public escrowCount;

    /// Mapping from escrow ID to Escrow data
    mapping(uint256 => Escrow) private _escrows;

    /// Arbiter fee percentage (default 1%)
    uint256 public arbiterFeePercent;

    /// @notice Emitted when escrow is created
    /// @param escrowId ID of the escrow
    /// @param buyer Address of buyer
    /// @param seller Address of seller
    event EscrowCreated(
        uint256 indexed escrowId,
        address indexed buyer,
        address indexed seller
    );

    /// @notice Emitted when escrow is funded
    /// @param escrowId ID of the escrow
    /// @param amount Amount deposited
    event EscrowFunded(uint256 indexed escrowId, uint256 amount);

    /// @notice Emitted when funds are released
    /// @param escrowId ID of the escrow
    /// @param recipient Address receiving funds
    event FundsReleased(uint256 indexed escrowId, address indexed recipient);

    /// @notice Emitted when funds are refunded
    /// @param escrowId ID of the escrow
    /// @param recipient Address receiving refund
    event FundsRefunded(uint256 indexed escrowId, address indexed recipient);

    /// @notice Emitted when dispute is raised
    /// @param escrowId ID of the escrow
    /// @param raisedBy Address raising dispute
    event DisputeRaised(uint256 indexed escrowId, address indexed raisedBy);

    /// @notice Emitted when dispute is resolved
    /// @param escrowId ID of the escrow
    /// @param winner Address favored in resolution
    event DisputeResolved(uint256 indexed escrowId, address indexed winner);

    /// @notice Creates the escrow contract
    /// @param _arbiterFeePercent Fee percentage for arbiter (0-10)
    constructor(uint256 _arbiterFeePercent) {
        require(_arbiterFeePercent <= 10, "Fee too high");
        owner = msg.sender;
        arbiterFeePercent = _arbiterFeePercent;
    }

    /// @notice Create a new escrow with encrypted amount
    /// @param seller Address of the seller
    /// @param arbiter Address of dispute arbiter
    /// @param encryptedAmount Encrypted escrow amount
    /// @param inputProof Proof validating the encrypted input
    /// @param deadline Deadline timestamp for delivery
    /// @return escrowId The ID of created escrow
    function createEscrow(
        address seller,
        address arbiter,
        externalEuint64 encryptedAmount,
        bytes calldata inputProof,
        uint256 deadline
    ) external returns (uint256 escrowId) {
        require(seller != address(0), "Invalid seller");
        require(seller != msg.sender, "Buyer cannot be seller");
        require(arbiter != address(0), "Invalid arbiter");
        require(arbiter != msg.sender && arbiter != seller, "Invalid arbiter");
        require(deadline > block.timestamp, "Deadline must be future");

        escrowId = ++escrowCount;

        // 🔐 Convert external encrypted input
        euint64 amount = FHE.fromExternal(encryptedAmount, inputProof);

        // ✅ Grant permissions
        FHE.allowThis(amount);
        FHE.allow(amount, msg.sender); // Buyer can view
        FHE.allow(amount, seller); // Seller can view
        FHE.allow(amount, arbiter); // Arbiter can view

        _escrows[escrowId] = Escrow({
            buyer: msg.sender,
            seller: seller,
            arbiter: arbiter,
            encryptedAmount: amount,
            depositedAmount: 0,
            state: EscrowState.Created,
            createdAt: block.timestamp,
            deadline: deadline
        });

        emit EscrowCreated(escrowId, msg.sender, seller);
    }

    /// @notice Fund the escrow
    /// @dev Amount verified against encrypted value on release
    /// @param escrowId ID of escrow to fund
    function fundEscrow(uint256 escrowId) external payable {
        Escrow storage escrow = _escrows[escrowId];
        require(escrow.buyer == msg.sender, "Only buyer can fund");
        require(escrow.state == EscrowState.Created, "Invalid state");
        require(msg.value > 0, "Must send funds");

        escrow.depositedAmount = msg.value;
        escrow.state = EscrowState.Funded;

        emit EscrowFunded(escrowId, msg.value);
    }

    /// @notice Release funds to seller
    /// @dev Only buyer can release after delivery
    /// @param escrowId ID of escrow to release
    function release(uint256 escrowId) external {
        Escrow storage escrow = _escrows[escrowId];
        require(escrow.buyer == msg.sender, "Only buyer can release");
        require(escrow.state == EscrowState.Funded, "Not funded");

        uint256 amount = escrow.depositedAmount;
        escrow.depositedAmount = 0;
        escrow.state = EscrowState.Released;

        // Transfer to seller
        (bool sent, ) = escrow.seller.call{value: amount}("");
        require(sent, "Transfer failed");

        emit FundsReleased(escrowId, escrow.seller);
    }

    /// @notice Request refund (before deadline or after timeout)
    /// @param escrowId ID of escrow
    function requestRefund(uint256 escrowId) external {
        Escrow storage escrow = _escrows[escrowId];
        require(escrow.buyer == msg.sender, "Only buyer");
        require(escrow.state == EscrowState.Funded, "Not funded");
        require(block.timestamp > escrow.deadline, "Deadline not passed");

        uint256 amount = escrow.depositedAmount;
        escrow.depositedAmount = 0;
        escrow.state = EscrowState.Refunded;

        (bool sent, ) = escrow.buyer.call{value: amount}("");
        require(sent, "Transfer failed");

        emit FundsRefunded(escrowId, escrow.buyer);
    }

    /// @notice Raise a dispute
    /// @param escrowId ID of escrow
    function raiseDispute(uint256 escrowId) external {
        Escrow storage escrow = _escrows[escrowId];
        require(
            escrow.buyer == msg.sender || escrow.seller == msg.sender,
            "Not a party"
        );
        require(escrow.state == EscrowState.Funded, "Not funded");

        escrow.state = EscrowState.Disputed;

        emit DisputeRaised(escrowId, msg.sender);
    }

    /// @notice Resolve dispute - arbiter decides winner
    /// @param escrowId ID of escrow
    /// @param favorBuyer True to refund buyer, false to release to seller
    function resolveDispute(uint256 escrowId, bool favorBuyer) external {
        Escrow storage escrow = _escrows[escrowId];
        require(escrow.arbiter == msg.sender, "Only arbiter");
        require(escrow.state == EscrowState.Disputed, "Not disputed");

        uint256 amount = escrow.depositedAmount;
        uint256 arbiterFee = (amount * arbiterFeePercent) / 100;
        uint256 payout = amount - arbiterFee;

        escrow.depositedAmount = 0;

        address winner;
        if (favorBuyer) {
            escrow.state = EscrowState.Refunded;
            winner = escrow.buyer;
        } else {
            escrow.state = EscrowState.Released;
            winner = escrow.seller;
        }

        // Pay arbiter fee
        if (arbiterFee > 0) {
            (bool feeSent, ) = escrow.arbiter.call{value: arbiterFee}("");
            require(feeSent, "Arbiter fee failed");
        }

        // Pay winner
        (bool sent, ) = winner.call{value: payout}("");
        require(sent, "Payout failed");

        emit DisputeResolved(escrowId, winner);
    }

    /// @notice Get escrow details
    function getEscrow(
        uint256 escrowId
    )
        external
        view
        returns (
            address buyer,
            address seller,
            address arbiter,
            uint256 depositedAmount,
            EscrowState state,
            uint256 createdAt,
            uint256 deadline
        )
    {
        Escrow storage escrow = _escrows[escrowId];
        return (
            escrow.buyer,
            escrow.seller,
            escrow.arbiter,
            escrow.depositedAmount,
            escrow.state,
            escrow.createdAt,
            escrow.deadline
        );
    }

    /// @notice Get encrypted amount handle (for permitted parties)
    function getEncryptedAmount(
        uint256 escrowId
    ) external view returns (euint64) {
        Escrow storage escrow = _escrows[escrowId];
        require(
            msg.sender == escrow.buyer ||
                msg.sender == escrow.seller ||
                msg.sender == escrow.arbiter,
            "Not authorized"
        );
        return escrow.encryptedAmount;
    }

    /// @notice Check if deadline has passed
    function isDeadlinePassed(uint256 escrowId) external view returns (bool) {
        return block.timestamp > _escrows[escrowId].deadline;
    }
}

Last updated