Development of Crypto Escrow System
In a transaction between two strangers without escrow — one pays first and hopes. It works until the first scammer. Traditional escrow services solve this through a trusted intermediary, but in crypto "trusted intermediary" is either a centralized platform (breaks with regulatory issues) or a smart contract (doesn't sleep, takes no bribes, executes conditions deterministically).
Basic Mechanics and Where It Breaks
Simplest escrow contract: buyer deposits funds → seller fulfills condition → buyer confirms → funds released. Problem — what if buyer doesn't confirm? Funds are locked forever.
Basic contract without timeout isn't an escrow, it's a trap. Minimal correct scheme requires:
- Timeout with automatic refund — if buyer doesn't confirm in N days, seller can request refund. Or vice versa — if seller doesn't fulfill condition, buyer withdraws deposit.
- Arbitration — third party with right to override the decision. Can be specific arbiter (address), multisig, or DAO.
- Dispersive model — arbiter doesn't receive funds, only decides proportion of return (75% to buyer, 25% to seller).
In Depth: Arbitration Model and Collusion Prevention
The most complex part of escrow design is not basic mechanics but arbitration model. Problem: if arbiter has absolute power over funds, they become attack target (bribes, key compromise). If arbiter is chosen by parties — collusion risk appears.
Practical patterns we apply:
Commit-reveal arbitration. Both parties independently send encrypted decision to arbiter, arbiter reveals decision only after receiving both. Doesn't eliminate collusion but complicates it.
Claimant-opponent arbitration (ERC-792 style). Each party provides evidence (document hashes, IPFS CID), arbiter votes publicly. Decision is recorded in contract and auditable.
Random arbiter from pool. Kleros Protocol implements this via decentralized court — random juror selection from stakers, economic incentive to vote honestly. Integrate via IArbitrable / IArbitrator interfaces.
Contract structure with Kleros support:
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; // arbiter decision
}
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: Non-obvious Differences
Escrow with native ETH is simpler — send msg.value, return via call. Escrow with ERC-20 requires approve before deposit. This creates two attack scenarios:
Token approval front-running. Classic attack: user does approve(spender, 100), then approve(spender, 200). Attacker-spender manages to withdraw 100 twice in between. Solution: always do approve(spender, 0) before new approve, or use permit (EIP-2612) — signature instead of on-chain approve.
Fee-on-transfer tokens. Deflationary tokens deduct fee on every transfer. Contract receives less than was deposited. Need to verify actually received amount: uint256 before = token.balanceOf(address(this)); token.transferFrom(...); uint256 received = token.balanceOf(address(this)) - before;
Multi-Currency Escrow
If system must support both ETH and ERC-20 — create unified interface via "zero address for ETH" pattern:
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 from OpenZeppelin is mandatory — standard transfer on some tokens (USDT) doesn't return bool, safeTransfer handles this correctly.
Development Process
Design mechanics (0.5-1 day). Determine: who is arbiter (EOA, multisig, Kleros), what timeouts, supported tokens, need partial release.
Development and tests (2-4 days). Foundry with fuzzing — test boundary cases for timeouts, edge cases with fee-on-transfer tokens, dispute resolution scenarios. Separate tests for reentrancy via ReentrancyGuard.
Audit and deployment. For systems with TVL > $100K — external audit from at least one provider. Audit timeline: 1-2 weeks. Deploy with verification on Etherscan/Polygonscan.







