x402EscrowSolidity:
v0.8.22+License: MIT
Inheritance: Initializable, UUPSUpgradeable, Ownable2StepUpgradeable, AccessControlUpgradeable, ReentrancyGuard
Constants
| Name | Type | Value | Description |
|---|---|---|---|
VERSION | uint256 | 1 | Contract version for upgrade tracking. |
FACILITATOR_ROLE | bytes32 | keccak256("FACILITATOR_ROLE") | Role identifier for facilitator addresses. |
State Variables
| Name | Type | Visibility | Description |
|---|---|---|---|
usdc | address | public | USDC token address, set once at initialization. |
timeoutSecs | uint256 | public | Default timeout for new escrows (seconds). Range: 300–86400. |
activeEscrows | mapping(bytes32 => Escrow) | public | Active escrows indexed by escrow ID. |
Types
Escrow (Internal Storage)
Packed into a single 32-byte storage slot.EscrowView (Returned by getEscrow)
Full-width types for off-chain consumption.Functions
initialize
| Name | Type | Description |
|---|---|---|
_usdc | address | USDC token address. Must be a contract. |
_facilitator | address | Initial facilitator. Receives FACILITATOR_ROLE. |
_admin | address | Initial admin. Receives DEFAULT_ADMIN_ROLE. |
_owner | address | Contract owner. Authorizes UUPS upgrades. |
ZeroAddress()— if any parameter isaddress(0).NotContract()— if_usdchas no code.
settle
| Name | Type | Description |
|---|---|---|
client | address | Client wallet that signed the authorization. |
maxAmount | uint256 | Maximum USDC to lock (6 decimals). Must fit uint56. |
validAfter | uint256 | Earliest timestamp the authorization can be used. |
validBefore | uint256 | Latest timestamp the authorization can be used. |
nonce | bytes32 | Unique nonce for this authorization. |
v, r, s | uint8, bytes32, bytes32 | Client’s ECDSA signature. |
| Name | Type | Description |
|---|---|---|
escrowId | bytes32 | keccak256(abi.encodePacked(client, nonce)) |
InvalidAmount()— ifmaxAmountis zero or exceedstype(uint56).max.NotYetValid()— ifblock.timestamp < validAfter.TimeoutExpired()— ifblock.timestamp >= validBefore.EscrowAlreadyExists()— if an active escrow with this ID already exists.TransferMismatch()— if the actual USDC received differs frommaxAmount.
Deposited(escrowId, client, maxAmount)
release
| Name | Type | Description |
|---|---|---|
escrowId | bytes32 | ID of the escrow to release. |
facilitatorAmount | uint256 | USDC to pay the facilitator. Must be ≤ escrow amount. Can be 0. |
- 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.
EscrowNotFound()— if the escrow does not exist or was already settled/refunded.InvalidAmount()— iffacilitatorAmount > escrow.amount.
Released(escrowId, msg.sender, facilitatorAmount, clientRefund)
refundAfterTimeout
| Name | Type | Description |
|---|---|---|
escrowId | bytes32 | ID of the escrow to refund. |
- The full amount transfers to the stored
clientaddress regardless of who calls this function. - Escrow storage is cleared before the transfer.
EscrowNotFound()— if the escrow does not exist.TimeoutNotReached()— ifblock.timestamp < escrow.refundAt.
Refunded(escrowId, client, amount)
setTimeout
| Name | Type | Description |
|---|---|---|
_timeoutSecs | uint256 | New timeout in seconds. Must be between 300 (5 min) and 86400 (24 hours). |
InvalidTimeout()— if outside the valid range.
TimeoutUpdated(_timeoutSecs)
getEscrow
| Name | Type | Description |
|---|---|---|
escrowId | bytes32 | ID of the escrow to query. |
EscrowView with:
client— depositor address (zero if the escrow doesn’t exist).amount— USDC locked.refundAt— timestamp when refund becomes available.canRefund—trueifrefundAfterTimeout()can be called now.timeUntilRefund— seconds remaining until refund is available (0 if eligible).
Events
Deposited
settle().
Released
release().
Refunded
refundAfterTimeout().
TimeoutUpdated
setTimeout().
Errors
| Error | Thrown by | Condition |
|---|---|---|
ZeroAddress() | initialize | Any parameter is address(0). |
NotContract() | initialize | _usdc has no deployed code. |
EscrowNotFound() | release, refundAfterTimeout | Escrow does not exist or has already been settled. |
EscrowAlreadyExists() | settle | Escrow ID is already active. |
TimeoutNotReached() | refundAfterTimeout | Current time is before refundAt. |
TimeoutExpired() | settle | EIP-3009 authorization has expired. |
NotYetValid() | settle | EIP-3009 authorization is not yet valid. |
InvalidAmount() | settle, release | Amount is zero, exceeds uint56, or exceeds the escrow balance. |
InvalidTimeout() | setTimeout | Timeout is outside the [300, 86400] range. |
TransferMismatch() | settle | Actual USDC received differs from expected. |