Skip to main content
x402Escrow API Contract: x402Escrow
Solidity: v0.8.22+
License: MIT
Inheritance: Initializable, UUPSUpgradeable, Ownable2StepUpgradeable, AccessControlUpgradeable, ReentrancyGuard

Constants

NameTypeValueDescription
VERSIONuint2561Contract version for upgrade tracking.
FACILITATOR_ROLEbytes32keccak256("FACILITATOR_ROLE")Role identifier for facilitator addresses.

State Variables

NameTypeVisibilityDescription
usdcaddresspublicUSDC token address, set once at initialization.
timeoutSecsuint256publicDefault timeout for new escrows (seconds). Range: 300–86400.
activeEscrowsmapping(bytes32 => Escrow)publicActive escrows indexed by escrow ID.

Types

Escrow (Internal Storage)

Packed into a single 32-byte storage slot.
struct Escrow {
    address client;    // 20 bytes — depositor address
    uint40  refundAt;  // 5 bytes  — timestamp when refund becomes available
    uint56  amount;    // 7 bytes  — USDC locked (up to ~72B USDC)
}

EscrowView (Returned by getEscrow)

Full-width types for off-chain consumption.
struct EscrowView {
    address client;
    uint256 amount;
    uint256 refundAt;
    bool    canRefund;
    uint256 timeUntilRefund;
}

Functions

initialize

function initialize(
    address _usdc,
    address _facilitator,
    address _admin,
    address _owner
) external initializer
Initializes the proxy. Can only be called once. Parameters
NameTypeDescription
_usdcaddressUSDC token address. Must be a contract.
_facilitatoraddressInitial facilitator. Receives FACILITATOR_ROLE.
_adminaddressInitial admin. Receives DEFAULT_ADMIN_ROLE.
_owneraddressContract owner. Authorizes UUPS upgrades.
Reverts
  • ZeroAddress() — if any parameter is address(0).
  • NotContract() — if _usdc has no code.

settle

function settle(
    address client,
    uint256 maxAmount,
    uint256 validAfter,
    uint256 validBefore,
    bytes32 nonce,
    uint8 v,
    bytes32 r,
    bytes32 s
) external onlyRole(FACILITATOR_ROLE) nonReentrant returns (bytes32 escrowId)
Locks USDC in escrow using the client’s EIP-3009 signed authorization. Parameters
NameTypeDescription
clientaddressClient wallet that signed the authorization.
maxAmountuint256Maximum USDC to lock (6 decimals). Must fit uint56.
validAfteruint256Earliest timestamp the authorization can be used.
validBeforeuint256Latest timestamp the authorization can be used.
noncebytes32Unique nonce for this authorization.
v, r, suint8, bytes32, bytes32Client’s ECDSA signature.
Returns
NameTypeDescription
escrowIdbytes32keccak256(abi.encodePacked(client, nonce))
Reverts
  • InvalidAmount() — if maxAmount is zero or exceeds type(uint56).max.
  • NotYetValid() — if block.timestamp < validAfter.
  • TimeoutExpired() — if block.timestamp >= validBefore.
  • EscrowAlreadyExists() — if an active escrow with this ID already exists.
  • TransferMismatch() — if the actual USDC received differs from maxAmount.
Emits
  • Deposited(escrowId, client, maxAmount)

release

function release(
    bytes32 escrowId,
    uint256 facilitatorAmount
) external onlyRole(FACILITATOR_ROLE) nonReentrant
Settles an active escrow. Pays the facilitator and refunds the remainder to the client. Parameters
NameTypeDescription
escrowIdbytes32ID of the escrow to release.
facilitatorAmountuint256USDC to pay the facilitator. Must be ≤ escrow amount. Can be 0.
Behavior
  • Payment goes to msg.sender (the calling facilitator), not a stored address.
  • If facilitatorAmount == 0, the full amount returns to the client.
  • If facilitatorAmount == escrow.amount, nothing returns to the client.
  • Escrow storage is cleared before transfers.
Reverts
  • EscrowNotFound() — if the escrow does not exist or was already settled/refunded.
  • InvalidAmount() — if facilitatorAmount > escrow.amount.
Emits
  • Released(escrowId, msg.sender, facilitatorAmount, clientRefund)

refundAfterTimeout

function refundAfterTimeout(bytes32 escrowId) external nonReentrant
Refunds the full escrowed amount to the client after the timeout has passed. Permissionless — callable by any address. Parameters
NameTypeDescription
escrowIdbytes32ID of the escrow to refund.
Behavior
  • The full amount transfers to the stored client address regardless of who calls this function.
  • Escrow storage is cleared before the transfer.
Reverts
  • EscrowNotFound() — if the escrow does not exist.
  • TimeoutNotReached() — if block.timestamp < escrow.refundAt.
Emits
  • Refunded(escrowId, client, amount)

setTimeout

function setTimeout(uint256 _timeoutSecs) external onlyRole(DEFAULT_ADMIN_ROLE)
Updates the default timeout for new escrows. Does not affect existing escrows. Parameters
NameTypeDescription
_timeoutSecsuint256New timeout in seconds. Must be between 300 (5 min) and 86400 (24 hours).
Reverts
  • InvalidTimeout() — if outside the valid range.
Emits
  • TimeoutUpdated(_timeoutSecs)

getEscrow

function getEscrow(bytes32 escrowId) external view returns (EscrowView memory)
Returns the current state of an escrow. Parameters
NameTypeDescription
escrowIdbytes32ID of the escrow to query.
Returns: EscrowView with:
  • client — depositor address (zero if the escrow doesn’t exist).
  • amount — USDC locked.
  • refundAt — timestamp when refund becomes available.
  • canRefundtrue if refundAfterTimeout() can be called now.
  • timeUntilRefund — seconds remaining until refund is available (0 if eligible).

Events

Deposited

event Deposited(bytes32 indexed escrowId, address indexed client, uint256 amount);
Emitted when USDC is locked via settle().

Released

event Released(
    bytes32 indexed escrowId,
    address indexed facilitator,
    uint256 toFacilitator,
    uint256 toClient
);
Emitted when an escrow is settled via release().

Refunded

event Refunded(bytes32 indexed escrowId, address indexed client, uint256 amount);
Emitted when an escrow is refunded via refundAfterTimeout().

TimeoutUpdated

event TimeoutUpdated(uint256 newTimeoutSecs);
Emitted when the admin changes the default timeout via setTimeout().

Errors

ErrorThrown byCondition
ZeroAddress()initializeAny parameter is address(0).
NotContract()initialize_usdc has no deployed code.
EscrowNotFound()release, refundAfterTimeoutEscrow does not exist or has already been settled.
EscrowAlreadyExists()settleEscrow ID is already active.
TimeoutNotReached()refundAfterTimeoutCurrent time is before refundAt.
TimeoutExpired()settleEIP-3009 authorization has expired.
NotYetValid()settleEIP-3009 authorization is not yet valid.
InvalidAmount()settle, releaseAmount is zero, exceeds uint56, or exceeds the escrow balance.
InvalidTimeout()setTimeoutTimeout is outside the [300, 86400] range.
TransferMismatch()settleActual USDC received differs from expected.