AutoEscrow

Reference implementation of IAutoContract for escrowed milestone agreements.

REFSource
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.26;

import "./interfaces/IAutoEscrow.sol";

interface IERC20 {
    function transfer(address to, uint256 value) external returns (bool);
    function transferFrom(address from, address to, uint256 value) external returns (bool);
}

/// @title AutoEscrow
/// @notice A minimal escrow primitive for markdown-defined agreements with a designated resolver.
/// @dev Human-readable terms stay offchain. The contract stores only hashes, parties, and state.
contract AutoEscrow is IAutoEscrow {

    struct Escrow {
        address buyer;
        address seller;
        address resolver;
        address token;
        uint128 amount;
        uint64 deadline;
        State state;
        Decision decision;
        bytes32 termsHash;
        bytes32 submissionHash;
        bytes32 resolutionHash;
    }

    error NotBuyer();
    error NotSeller();
    error NotResolver();
    error InvalidParty();
    error InvalidToken();
    error InvalidAmount();
    error InvalidDeadline();
    error InvalidState();
    error InvalidDecision();
    error TransferFailed();

    uint256 public nextEscrowId;

    mapping(uint256 => Escrow) public escrows;

    /// @notice Create an unfunded escrow shell.
    function create(
        address seller,
        address resolver,
        address token,
        uint128 amount,
        uint64 deadline,
        bytes32 termsHash
    ) external returns (uint256 escrowId) {
        if (seller == address(0) || resolver == address(0) || seller == msg.sender) {
            revert InvalidParty();
        }
        if (token == address(0)) revert InvalidToken();
        if (amount == 0) revert InvalidAmount();
        if (deadline <= block.timestamp) revert InvalidDeadline();

        escrowId = nextEscrowId++;

        escrows[escrowId] = Escrow({
            buyer: msg.sender,
            seller: seller,
            resolver: resolver,
            token: token,
            amount: amount,
            deadline: deadline,
            state: State.Created,
            decision: Decision.None,
            termsHash: termsHash,
            submissionHash: bytes32(0),
            resolutionHash: bytes32(0)
        });

        emit EscrowCreated(
            escrowId,
            msg.sender,
            seller,
            resolver,
            token,
            amount,
            deadline,
            termsHash
        );
    }

    function fund(uint256 escrowId) external {
        Escrow storage escrow = escrows[escrowId];

        if (msg.sender != escrow.buyer) revert NotBuyer();
        if (escrow.state != State.Created) revert InvalidState();

        escrow.state = State.Funded;
        _safeTransferFrom(escrow.token, escrow.buyer, address(this), escrow.amount);

        emit EscrowFunded(escrowId);
    }

    function submit(uint256 escrowId, bytes32 submissionHash) external {
        Escrow storage escrow = escrows[escrowId];

        if (msg.sender != escrow.seller) revert NotSeller();
        if (escrow.state != State.Funded) revert InvalidState();

        escrow.state = State.Submitted;
        escrow.submissionHash = submissionHash;

        emit EscrowSubmitted(escrowId, submissionHash);
    }

    function resolve(uint256 escrowId, Decision decision, bytes32 resolutionHash) external {
        Escrow storage escrow = escrows[escrowId];

        if (msg.sender != escrow.resolver) revert NotResolver();
        if (escrow.state != State.Submitted) revert InvalidState();
        if (decision == Decision.None) revert InvalidDecision();

        escrow.state = State.Resolved;
        escrow.decision = decision;
        escrow.resolutionHash = resolutionHash;

        if (decision == Decision.Release) {
            _safeTransfer(escrow.token, escrow.seller, escrow.amount);
        } else {
            _safeTransfer(escrow.token, escrow.buyer, escrow.amount);
        }

        emit EscrowResolved(escrowId, decision, resolutionHash);
    }

    function cancel(uint256 escrowId) external {
        Escrow storage escrow = escrows[escrowId];

        if (msg.sender != escrow.buyer) revert NotBuyer();
        if (escrow.state != State.Created) revert InvalidState();

        escrow.state = State.Cancelled;

        emit EscrowCancelled(escrowId);
    }

    function refundExpired(uint256 escrowId) external {
        Escrow storage escrow = escrows[escrowId];

        if (escrow.state != State.Funded || block.timestamp <= escrow.deadline) {
            revert InvalidState();
        }

        escrow.state = State.Cancelled;
        escrow.decision = Decision.Refund;
        _safeTransfer(escrow.token, escrow.buyer, escrow.amount);

        emit EscrowCancelled(escrowId);
    }

    function stateOf(uint256 escrowId) external view returns (State) {
        return escrows[escrowId].state;
    }

    function decisionOf(uint256 escrowId) external view returns (Decision) {
        return escrows[escrowId].decision;
    }

    function _safeTransfer(address token, address to, uint256 amount) internal {
        (bool ok, bytes memory data) =
            token.call(abi.encodeWithSelector(IERC20.transfer.selector, to, amount));
        if (!ok || (data.length > 0 && !abi.decode(data, (bool)))) revert TransferFailed();
    }

    function _safeTransferFrom(address token, address from, address to, uint256 amount) internal {
        (bool ok, bytes memory data) =
            token.call(abi.encodeWithSelector(IERC20.transferFrom.selector, from, to, amount));
        if (!ok || (data.length > 0 && !abi.decode(data, (bool)))) revert TransferFailed();
    }
}