AutoEscrow
Reference implementation of IAutoContract for escrowed milestone agreements.
Reference implementation of IAutoContract for escrowed milestone agreements.
// 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();
}
}