Private Escrow
Overview
Quick Start
# Create new project from this template
npx labz create escrow my-project
# Navigate and install
cd my-project
npm install
# Run tests
npx hardhat testContract
// 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 ============
}
FHE Operations Used
FHE Types Used
Tags
Related Examples
Prerequisites
Next Steps
Last updated
