Motivation
Any on-chain entity (protocol, DAO, user) that wants to hold assets in escrow faces three problems:
- Security concentration. Each self-custodied account is an independent audit surface. Vulnerabilities in custody logic are the leading cause of DeFi exploits.
- Custody separation. Entities can reduce direct custody exposure by delegating asset management to independently-audited shared infrastructure. This may clarify operational boundaries.
- Implementation duplication. Every entity reimplements deposit accounting, reentrancy protection, fee-on-transfer handling, and replay safeguards.
We propose ERC-ProtocolEscrow, a minimal escrow standard with three core functions.
Terminology Note
In this standard, “escrow” refers to asset segregation and balance accounting within a shared contract, not conditional or third-party enforced custody. This standard does not impose constraints on when or how a release must occur. Authorization, locking, and any conditional release logic must be implemented by the calling account or external wrappers.
Why Standardization Matters
Without a standard interface, each protocol builds its own custody logic, creating a fragmented ecosystem where:
- Wallets and frontends must implement protocol-specific integrations.
- Indexers cannot uniformly track asset flows.
- Infrastructure cannot compose across protocols.
- Security surface area multiplies per protocol.
Without a common interface, each integration requires protocol-specific adapters, increasing complexity and the risk of integration errors. A standard interface removes this need, enabling generic integrations across all conforming implementations. For example, a wallet can support any conforming escrow by integrating this interface once, without requiring protocol-specific logic. This reduces development overhead and security risk.
interface IProtocolEscrow {
event Deposited(address indexed account, address indexed asset, uint256 amount);
event Released(address indexed account, address indexed asset, address indexed to, uint256 amount, bytes32 releaseId);
// Deposit assets into the account (account = msg.sender)
function deposit(address asset, uint256 amount) external payable;
// Release assets from the account (only account owner can release)
function release(address asset, address to, uint256 amount, bytes32 releaseId) external;
// Query the account's balance for an asset
function escrowBalance(address asset) external view returns (uint256);
}
Key Design Principles
Account Identity = msg.sender
Implicit account ownership. Any address can call deposit() to establish an escrow account. Only that same address can call release() for its balance. There is no centralized registration, governance, or whitelist in the core standard.
This eliminates one source of complexity. Governance—if desired—can be implemented at a higher layer.
Simple Deposit Model
- For ERC-20: Caller must have approved the escrow contract. Tokens are transferred; balance increases by actual received amount (balance delta method). Supports fee-on-transfer tokens.
- For native token:
asset == address(0);msg.value == amount. - Prevention: If
msg.value > 0with an ERC-20 asset, the call reverts. Native token cannot be accidentally locked.
Simple Release Model
- Transfer via release(): The caller may transfer assets from its escrow balance via
release(). Only the account owner (msg.sender) can call this function. - releaseId prevents replay: Once consumed for an account, it cannot be reused for any asset under that account.
- Checks-effects-interactions (CEI): Mark releaseId as used, deduct balance, then transfer.
- Reentrancy protection: Both
depositandreleaseare nonReentrant.
No Enforced Release Conditions
This standard is intentionally minimal: it does not enforce locking, time-based releases, multi-party authorizations, or any other constraints on when funds can be withdrawn. The escrow is purely a custody and accounting layer. All release conditions, if any, are the responsibility of the calling account and must be implemented externally.
Application-Agnostic Design
This standard is intentionally application-agnostic and can be used in any system requiring isolated asset accounting and replay-protected transfers. The minimal three-function interface avoids baking application-specific assumptions into the standard.
Examples: ZK privacy protocols, bridges, payment systems, vesting contracts, and any future use case requiring asset segregation.
Optional Extensions
These extensions are not part of the core standard and are not required for interoperability. A minimal implementation supporting only the three core functions is fully compliant.
The standard also defines optional interfaces:
1. Extended Queries (IProtocolEscrowQueryable)
function escrowBalanceOf(address account, address asset) external view returns (uint256);
function isReleaseIdUsed(address account, bytes32 releaseId) external view returns (bool);
Allows off-chain tools to query any account’s balance and releaseId consumption status without calling from that account’s own address. Note: releaseId scope is global per account, not per-asset.
2. Deposit Hook (IEscrowDepositHook)
function onDeposit(address asset, address depositor, uint256 amount) external;
Optional notification after balance is credited. Hook reverts roll back the entire deposit.
3. Query + Reason Codes (IProtocolEscrowWithPreview)
function canRelease(
address account,
address asset,
address to,
uint256 amount,
bytes32 releaseId
) external view returns (bool ok, bytes32 reason);
Dry-run a release without execution. Reason codes are RECOMMENDED (not mandatory) and follow a standard taxonomy.
4. Governance Interface (IProtocolEscrowGovernance)
Optional. For deployments that include governance:
// Protocol registration
function registerProtocol(address protocolContract, uint8 depositHookMode) external;
// Pause mechanisms (global and per-protocol)
function pauseAllDeposits() external;
function unpauseAllDeposits() external;
function pauseProtocolDeposits(address protocolContract) external;
function unpauseProtocolDeposits(address protocolContract) external;
function pauseProtocolReleases(address protocolContract, uint64 untilTimestamp) external;
function unpauseProtocolReleases(address protocolContract) external;
// Asset whitelist
function setAssetAllowed(address asset, bool allowed) external;
// Emergency guardian (optional)
function setEmergencyGuardian(address newGuardian) external;
Key property: Pause and governance are optional. Minimal deployments can omit them entirely. Governed deployments can layer them on top of the core escrow.
Comparison to Existing Standards
| Standard | Purpose | Identity | Core Ops | Governance |
|---|---|---|---|---|
| ERC-20 | Fungible tokens | Token contract | transfer, approve | Not in scope |
| ERC-4626 | Yield vaults | Vault contract | deposit, withdraw, mint, redeem | Not in scope |
| ERC-ProtocolEscrow | Custody / escrow primitive | Account address (msg.sender) | deposit, release, escrowBalance | Not in scope |
Security Considerations
Reentrancy
deposit and release MUST be nonReentrant. Deposits call ERC-20 contracts; releases call safeTransfer and hooks. Both are external calls.
Fee-on-Transfer Tokens
Balance delta crediting is mandatory. Crediting the caller-provided amount creates phantom balances. Balance delta is the only robust approach.
Release ID Replay
releaseId MUST be globally unique per account. Once consumed by an account for any asset, it cannot be reused for any other release under that account. The consumed status of a releaseId is independent of asset, recipient, and amount. Per-asset scoping is not conformant with this standard.
Concentration Risk
A single escrow holds assets for multiple protocols. A critical vulnerability affects all simultaneously. This is the trade-off: one extensively audited shared escrow replaces many lower-quality per-protocol vaults.
No Protocol Registration or Governance
This standard does not define registration, pausing, or guardian roles. These belong to higher-level systems. A minimal escrow can operate without governance.
Separation of Custody and Authorization
The core design principle separates custody (escrow’s responsibility) from authorization (caller’s responsibility):
- Custody: Track balances accurately, execute transfers atomically, prevent replay attacks.
- Authorization: Caller decides when a release is valid—locking, conditions, multi-party approval, etc. must be implemented externally.
This separation enables the escrow to work with any authorization scheme, from simple (immediate release) to complex (timelock + multisig + oracle).
No Recovery Mechanism
The escrow does not provide recovery for locked or inaccessible balances. If an account owner loses the ability to call release() (due to contract bugs, lost keys, or compromised logic), the escrowed assets cannot be recovered by the escrow itself. Recovery must be handled at the account level or through external governance. Account owners are responsible for ensuring release() remains callable.
Test Checklist
Tier 1 — Core Conformance (MANDATORY)
- deposit succeeds for ERC-20 (with approval) and native token (msg.value == amount)
- release succeeds; balance decreases; funds transferred
escrowBalancereturns correct value- release with amount > balance reverts
- release by an address different from the account owner (msg.sender) reverts
- duplicate releaseId reverts (global scope across all assets)
- same releaseId used with a different asset also reverts (enforces global scoping)
- ERC-20 deposit with msg.value > 0 reverts
- native deposit with msg.value != amount reverts
- release to address(0) reverts
- amount == 0 in release reverts
- fee-on-transfer ERC-20: credited balance = actual received, not parameter amount
- reentrancy (in deposit and release) does not corrupt state
- multiple protocols have independent balances
Tier 2 — Optional Features (CONDITIONAL)
- (if extended queries)
escrowBalanceOfandisReleaseIdUsedwork correctly - (if hook)
onDepositcalled after balance credit; reverts roll back deposit - (if preview)
canReleasereturns (true, 0) iff release would succeed; reason codes consistent
Backwards Compatibility
This standard introduces a new interface and does not modify any existing standard. ERC-20 and native token mechanics are unchanged. Protocols adopt this standard by implementing the three core functions and calling them according to the specification.
Open Questions for Community Feedback
-
Fee-on-transfer handling. Is balance delta the right model, or should we allow opt-in alternatives?
-
Governance layers. Should the standard define a minimal governance interface (register, pause), or keep governance entirely out of scope?
-
Immutable vs. governed deployments. Should we explicitly support both zero-governance and multi-sig-governed instances?
-
Cross-chain identity. If a protocol deploys on multiple chains, should escrow instances on different chains be considered the same protocol or distinct?
Implementation Resources
A reference implementation for chain-level deployment is available (informative). Minimal immutable deployments are equally valid.
We look forward to feedback from teams building bridges, payment systems, privacy protocols, or any protocol that currently maintains its own fund pool.