Разработка системы эскроу для крипто-сделок
В сделке между двумя незнакомыми участниками без эскроу — один платит первым и надеется. Это работает до первого мошенника. Классические эскроу-сервисы решают это через доверенного посредника, но в крипто «доверенный посредник» — это либо централизованная платформа (ломается при регуляторных проблемах), либо смарт-контракт (не спит, не берёт откаты, исполняет условия детерминировано).
Базовая механика и где она ломается
Простейший эскроу-контракт: покупатель депонирует средства → продавец выполняет условие → покупатель подтверждает → средства освобождаются. Проблема — что если покупатель не подтверждает? Средства заблокированы навсегда.
Базовый контракт без timeout — это не эскроу, это ловушка. Минимальная корректная схема требует:
- Timeout с автоматическим рефандом — если покупатель не подтвердил за N дней, продавец может запросить возврат средств. Или наоборот — если продавец не выполнил условие, покупатель забирает депозит.
- Арбитраж — третья сторона с правом override решения. Это может быть конкретный арбитр (адрес), мультисиг, или DAO.
- Дисперсионная модель — арбитр не получает средства, он только решает пропорцию возврата (75% покупателю, 25% продавцу).
Углублённо: арбитражная модель и предотвращение сговора
Самая сложная часть дизайна эскроу — не базовая механика, а модель арбитража. Проблема: если арбитр имеет абсолютную власть над средствами, он становится целью атаки (взятки, компрометация ключа). Если арбитр выбирается сторонами — появляется collusion risk.
Практические паттерны, которые мы применяем:
Commit-reveal арбитраж. Обе стороны независимо отправляют зашифрованное решение арбитру, арбитр раскрывает своё решение только после получения обоих. Не устраняет сговор, но усложняет его.
Клямэт-оппонент арбитраж (ERC-792 стиль). Каждая сторона предоставляет доказательства (хэши документов, IPFS CID), арбитр голосует публично. Решение записывается в контракт и аудируемо.
Случайный арбитр из пула. Kleros Protocol реализует это через decentralized court — случайный отбор jurors из стейкеров, экономический стимул голосовать честно. Интегрируем через IArbitrable / IArbitrator интерфейсы.
Структура контракта с поддержкой Kleros:
contract Escrow is IArbitrable {
IArbitrator public immutable arbitrator;
uint256 public disputeId;
enum Status { Pending, Active, Disputed, Resolved }
struct Deal {
address buyer;
address seller;
uint256 amount;
uint256 timeout;
Status status;
uint8 buyerPercent; // решение арбитра
}
function raiseDispute(uint256 dealId) external payable {
Deal storage deal = deals[dealId];
require(deal.status == Status.Active);
require(msg.value >= arbitrator.arbitrationCost(""));
deal.status = Status.Disputed;
disputeId = arbitrator.createDispute{value: msg.value}(
2, // NUMBER_OF_CHOICES: buyer wins / seller wins
""
);
emit Dispute(arbitrator, disputeId, dealId);
}
function rule(uint256 _disputeId, uint256 _ruling)
external override
{
require(msg.sender == address(arbitrator));
// _ruling: 1 = buyer wins, 2 = seller wins
_executeRuling(_disputeId, _ruling);
}
}
ERC-20 vs native ETH: неочевидные различия
Эскроу с native ETH проще — отправить msg.value, вернуть через call. Эскроу с ERC-20 требует approve перед депозитом. Это создаёт два сценария атаки:
Token approval front-running. Классическая атака: пользователь делает approve(spender, 100), потом approve(spender, 200). В промежутке атакующий-spender успевает дважды снять 100. Решение: всегда делать approve(spender, 0) перед новым approve, или использовать permit (EIP-2612) — подпись вместо on-chain approve.
Fee-on-transfer токены. Дефляционные токены списывают комиссию при каждом transfer. Контракт получает меньше, чем было задепонировано. Нужна проверка фактически полученного количества: uint256 before = token.balanceOf(address(this)); token.transferFrom(...); uint256 received = token.balanceOf(address(this)) - before;
Мультивалютный эскроу
Если система должна поддерживать и ETH и ERC-20 — делаем унифицированный интерфейс через паттерн «нулевой адрес для ETH»:
function deposit(address token, uint256 amount) external payable {
if (token == address(0)) {
require(msg.value == amount);
} else {
require(msg.value == 0);
IERC20(token).safeTransferFrom(msg.sender, address(this), amount);
}
}
SafeERC20 от OpenZeppelin обязателен — стандартный transfer некоторых токенов (USDT) не возвращает bool, safeTransfer обрабатывает это корректно.
Процесс разработки
Проектирование механики (0.5-1 день). Определяем: кто арбитр (EOA, мультисиг, Kleros), какие таймауты, поддерживаемые токены, нужен ли partial release.
Разработка и тесты (2-4 дня). Foundry с fuzzing — тестируем boundary cases для таймаутов, edge cases с fee-on-transfer токенами, сценарии dispute resolution. Отдельные тесты на reentrancy через ReentrancyGuard.
Аудит и деплой. Для систем с TVL > $100K — внешний аудит минимум от одного провайдера. Срок аудита: 1-2 недели. Деплой с верификацией на Etherscan/Polygonscan.







