EscrowManager Contract Documentation
Overview
The EscrowManager contract manages token escrow operations for marketplace modules in the DEUSS bond marketplace. It is module-type aware and uses an internal registry (moduleTypeOf + isAuthorizedModule) to authorize callers.
Escrows are keyed by a globally unique escrowId allocated by EscrowManager, which prevents orphaning when module addresses are rotated.
Supports ERC-20, ERC-721, ERC-1155, and ERC-6909 token standards. Asset type validation is delegated to AssetManager.
Surplus Recovery
EscrowManager maintains aggregate reserved-by-asset accounting so that stray balances (tokens sent directly to the contract outside the normal escrow flow) can be safely recovered via the admin-only sweep function without ever touching assets that are genuinely reserved by active escrows.
Prerequisites
- Contract must be initialized with:
owner_marketplace_(optional)orderbookMarketplace_(optional)
- Proper roles must be assigned:
ADMIN: For module registry management (registerModule,deactivateModule), AssetManager wiring (setAssetManager), and surplus asset recovery (sweep)
- Calling module must be registered and active for its module type
- Any module passed as zero at initialization must be registered later via
registerModule(...) - Depositor must approve (or operator-authorize)
EscrowManageron the ERC-6909 token for escrow deposits
Contract Architecture
The EscrowManager is a core component of the bond marketplace system:
- Inherits from
OwnableRolesExtensionfor role-based access control. Read more in the Roles docs page. - Inherits from
Initializablefor proxy initialization - Implements
IEscrowManagerand ERC-165 support (supportsInterface) - Integrates with ERC-20, ERC-721, ERC-1155, and ERC-6909 tokens for transfers
- Stores escrows as
mapping(uint256 => Escrow) - Maintains aggregate reserved-by-asset accounting:
mapping(bytes32 assetKey => uint256 reservedAmount) _reservedByAsset- Key:
keccak256(assetType, token, tokenId) - Invariant: for each key,
reservedAmount == sum of escrow.amountacross all active escrows for that asset - Maintained automatically by
createEscrow(increment) and_transferFromEscrow(decrement)
- Maintains active escrow counts per creating module:
mapping(address moduleAddress => uint256 activeEscrows) _activeEscrowsByModule- Incremented when a module creates an escrow and decremented when that escrow is fully drained
- Maintains module registry:
mapping(address => bytes32) moduleTypeOfmapping(bytes32 => mapping(address => bool)) isAuthorizedModule
- Exposes built-in module type constants:
MARKETPLACE_MODULEORDERBOOK_MARKETPLACE_MODULE
- Uses monotonic
nextEscrowIdfor escrow ID allocation
Trust Assumption: Authorized ERC20 Depositor
Arbitrary from in ERC20 transferFrom (trusted-module risk)
EscrowManager supports module-driven escrow creation. For ERC20 deposits, _transferToEscrow executes:
IERC20(token).safeTransferFrom(from, address(this), amount)
Here, from is passed by an authorized module, not forced to msg.sender. This is intentional to let marketplace
modules create escrows on behalf of depositors.
Risk model:
- This is safe only under the trusted-module assumption.
- If an authorized module is compromised, buggy, or incorrectly integrated, it may attempt to pull tokens from any
address that has granted allowance to
EscrowManager.
Impact:
- Unauthorized token movement from approved depositors can occur through a compromised authorized module.
Mitigations / operational controls:
- Strict governance over module registration (
registerModule) and immediate deactivation (deactivateModule) on incident. - Authorize only audited modules; avoid broad third-party module onboarding.
- Encourage minimal allowances (exact amount / short-lived approvals) from depositors.
- Monitor module behavior and escrow creation activity.
Core Functions
createEscrow(uint256 amount, address depositor, address tokenAddress, uint256 tokenId)
Creates a new escrow and deposits tokens into escrow.
Prerequisites:
- Caller must be registered (
moduleTypeOf[msg.sender] != 0) - Caller must be active for its module type (
isAuthorizedModule[moduleType][msg.sender] == true) amountmust be non-zerodepositormust be non-zerotokenAddressmust be non-zero- The escrow contract's actual balance increase after the transfer must equal the requested
amount(tokens whose on-chain balance changes differ from transfer amounts are rejected)
Parameters:
amount: The amount of tokens to deposit into escrowdepositor: The address of the token depositortokenAddress: The address of the token contracttokenId: The ID of the specific token
Returns:
escrowId: The newly allocated escrow ID
Events:
EscrowCreated(escrowId, moduleType, depositor, tokenAddress, tokenId, amount, assetType)
Errors:
EscrowManager__ModuleNotRegistered(address moduleAddress)EscrowManager__ModuleNotAuthorized(address moduleAddress, bytes32 moduleType)EscrowManager__ZeroAmount()EscrowManager__InvalidDepositor()EscrowManager__InvalidTokenAddress()EscrowManager__TokensTransferFailed()EscrowManager__DepositAmountMismatch()— the balance increase observed after the transfer does not equalamount
Important Notes:
EscrowManagerallocatesescrowIdinternally (++nextEscrowId)- Escrow stores caller's derived
moduleTypeand the caller module address - Authorization is module-type based, not hardcoded to a single address
- Increments
_reservedByAssetfor the asset key to maintain sweep safety invariant - Increments
_activeEscrowsByModule[msg.sender]; this prevents deactivation while that module still has live escrow balances - Deposit integrity is enforced by comparing the contract's balance before and after the transfer; any token whose received amount differs from the requested amount (regardless of cause) is rejected with
EscrowManager__DepositAmountMismatch
Sequence Diagram:
withdraw(uint256 escrowId, uint256 amount)
Withdraws escrowed tokens back to the escrow depositor.
Prerequisites:
- Escrow must exist
- Caller must be authorized for
escrow.moduleType amountmust be non-zero- Escrow must have sufficient balance
Parameters:
escrowId: The escrow IDamount: The amount of tokens to withdraw from escrow
Events:
Withdrawn(escrowId, beneficiary, amount, tokenAddress, tokenId, assetType): Withdrawal asset snapshot
Errors:
EscrowManager__EscrowNotFound(uint256 escrowId)EscrowManager__ModuleNotAuthorized(address moduleAddress, bytes32 moduleType)EscrowManager__ZeroAmount()EscrowManager__InsufficientBalance()EscrowManager__InvalidBeneficiary()EscrowManager__TokensTransferFailed()
Important Notes:
- Recipient is always
escrow.depositor - Module authorization is checked against
escrow.moduleType - Token transfer failures propagate to the caller. If an ERC20 blocks the depositor as recipient, for example through a blacklist, the withdrawal for that escrow item reverts until the recipient can receive tokens or the upstream module uses its compliance recovery path.
Sequence Diagram:
claim(uint256 escrowId, uint256 amount, address beneficiary)
Claims tokens from escrow to a beneficiary.
Prerequisites:
- Escrow must exist
- Caller must be authorized for
escrow.moduleType amountmust be non-zero- Escrow must have sufficient balance
beneficiarymust be non-zero
Parameters:
escrowId: The escrow IDamount: The amount of tokens to claim from escrowbeneficiary: The address receiving claimed tokens
Events:
Claimed(escrowId, beneficiary, amount, tokenAddress, tokenId, assetType)
Errors:
EscrowManager__EscrowNotFound(uint256 escrowId)EscrowManager__ModuleNotAuthorized(address moduleAddress, bytes32 moduleType)EscrowManager__ZeroAmount()EscrowManager__InsufficientBalance()EscrowManager__InvalidBeneficiary()EscrowManager__TokensTransferFailed()
Important Notes:
- Module authorization is checked against
escrow.moduleType - Token transfer failures propagate to the caller. If an ERC20 blocks the beneficiary as recipient, for example through a blacklist, the claim for that escrow item reverts. Batch-capable upstream modules should isolate that failure per item; permanent failures are handled operationally through freeze and seizure flows where available.
Sequence Diagram:
sweep(AssetType assetType, address token, uint256 tokenId, uint256 amount, address beneficiary)
Sweeps surplus (untracked) assets from the contract to a beneficiary. Only transfers tokens that are not reserved by any active escrow.
Prerequisites:
- Caller must have
ADMINrole amountmust be non-zerobeneficiarymust be non-zero- For ERC-721,
amountmust be1 - Sufficient surplus must exist (actual on-chain balance minus reserved amount)
Parameters:
assetType: The token standard of the asset (ERC20, ERC721, ERC1155, ERC6909)token: The token contract addresstokenId: The token identifier (0 for ERC-20)amount: The amount to sweep (must not exceed surplus)beneficiary: The address receiving the swept assets
Events:
Swept(assetType, token, tokenId, beneficiary, amount)
Errors:
EscrowManager__InvalidSweepAmount()— amount is zeroEscrowManager__InvalidBeneficiary()— beneficiary is zero addressEscrowManager__InvalidERC721Amount(uint256 amount)— ERC-721 amount != 1EscrowManager__SweepExceedsSurplus(uint256 requested, uint256 surplus)— requested amount exceeds available surplus
Important Notes:
- The sweep function computes surplus as:
actualBalance - reservedBalance(clamped to zero) actualBalanceis queried from the token contract using the appropriate balance method for the asset typereservedBalanceis read from_reservedByAsset[assetKey]- Reuses
_transferAssetfor token dispatch — no duplicated transfer logic - Does not modify
_reservedByAsset— only unreserved surplus is moved
Sequence Diagram:
Internal Logic
_transferFromEscrow(Escrow storage escrow, uint256 amount, address recipient)
Shared internal helper used by both withdraw and claim.
Parameters:
escrow: Storage pointer to the escrow record to debitamount: Amount of tokens to transferrecipient: Token receiver
Behavior:
- Validates
escrow.amount >= amount - Validates
recipient != address(0) - Decrements escrow amount
- Decrements
_reservedByAssetfor the asset key - Dispatches transfer via
_transferAssetbased onescrow.assetType - Reverts on failed transfer
- Does not emit events (external function emits domain event)
Errors:
EscrowManager__InsufficientBalance()EscrowManager__InvalidBeneficiary()EscrowManager__TokensTransferFailed()
Called by:
withdraw(escrowId, amount)claim(escrowId, amount, beneficiary)
Sequence Diagram:
Administrative Functions
initialize(address owner_, address marketplace_, address orderbookMarketplace_)
Initializes owner and registers default marketplace modules.
Prerequisites:
- Contract must not be already initialized
marketplace_may be zero (optional dependency at init time)orderbookMarketplace_may be zero (optional dependency at init time)
Parameters:
owner_: Address that will own the contractmarketplace_: Marketplace module addressorderbookMarketplace_: OrderbookMarketplace module address
Errors:
EscrowManager__InvalidModuleAddress()
Important Notes:
- Internally registers:
MARKETPLACE_MODULE -> marketplace_when non-zeroORDERBOOK_MARKETPLACE_MODULE -> orderbookMarketplace_when non-zero
- If one or more module addresses are zero at initialization, register them later with:
registerModule(MARKETPLACE_MODULE, marketplaceAddress)registerModule(ORDERBOOK_MARKETPLACE_MODULE, orderbookMarketplaceAddress)
registerModule(bytes32 moduleType, address moduleAddress)
Registers (or reactivates) a module address for a module type.
Prerequisites:
- Caller must have
ADMINrole moduleTypemust be non-zeromoduleAddressmust be non-zero
Parameters:
moduleType: Module type keymoduleAddress: Module contract address
Events:
ModuleRegistered(moduleType, moduleAddress)
Errors:
EscrowManager__InvalidModuleType()EscrowManager__InvalidModuleAddress()EscrowManager__ModuleTypeMismatch(bytes32 existingModuleType, bytes32 requestedModuleType)EscrowManager__ModuleAlreadyRegistered(address moduleAddress, bytes32 moduleType)
Important Notes:
- Reverts on duplicate active registration (same address + same type)
- Allows re-registration after prior deactivation
setAssetManager(address assetManager)
Updates the AssetManager used to validate assets during createEscrow.
Prerequisites:
- Caller must have
ADMINrole assetManagermust be non-zero
Events:
AssetManagerSet(previousAssetManager, assetManager)
Errors:
Unauthorized(): When caller lacksADMINZeroAddress(): WhenassetManageris zero
Important Notes:
- This is a live dependency rotation path. A misconfigured AssetManager can block new escrow creation, but existing escrow records keep their stored asset type and balances.
deactivateModule(bytes32 moduleType, address moduleAddress)
Deactivates a module address for a module type.
Prerequisites:
- Caller must have
ADMINrole moduleTypemust be non-zeromoduleAddressmust be non-zero- Module must be currently registered and match the provided module type
- Module must have no active escrows
Parameters:
moduleType: Module type keymoduleAddress: Module contract address
Events:
ModuleDeactivated(moduleType, moduleAddress)
Errors:
EscrowManager__InvalidModuleType()EscrowManager__InvalidModuleAddress()EscrowManager__ModuleNotRegistered(address moduleAddress)EscrowManager__ModuleTypeMismatch(bytes32 existingModuleType, bytes32 requestedModuleType)EscrowManager__ModuleHasActiveEscrows(address moduleAddress, uint256 activeEscrows)
Important Notes:
- Sets
isAuthorizedModule[moduleType][moduleAddress] = false - Clears
moduleTypeOf[moduleAddress] - Reverts while
_activeEscrowsByModule[moduleAddress] > 0; all escrows created by the module must be fully withdrawn or claimed first
View Functions
getEscrow(uint256 escrowId)
Returns escrow details for escrowId.
Parameters:
escrowId: Escrow identifier
Returns:
Escrow: Full escrow struct
previewNextEscrowId()
Returns the escrow ID expected to be allocated by the next createEscrow(...) call.
Parameters:
- None
Returns:
uint256:nextEscrowId + 1
getSweepableAmount(AssetType assetType, address token, uint256 tokenId)
Returns the amount of surplus (untracked) assets currently available for sweeping.
Parameters:
assetType: The token standard of the assettoken: The token contract addresstokenId: The token identifier
Returns:
uint256: The sweepable surplus, computed asmax(0, actualBalance - reservedBalance)
Important Notes:
- Queries the actual on-chain balance held by
EscrowManagerfor the given asset - Subtracts the aggregate reserved amount tracked by
_reservedByAsset - Returns zero if the reserved amount meets or exceeds the actual balance
- Useful for off-chain tooling to preview how much can be swept before calling
sweep
supportsInterface(bytes4 interfaceId)
Checks if the contract supports a specific interface.
Parameters:
interfaceId: Interface identifier to check
Returns:
bool: True forIEscrowManagerand interfaces supported by the inherited ERC-1155 receiver stack, includingIERC165andIERC1155Receiver; false otherwise
Notes:
- The contract accepts ERC-721 safe transfers through
ERC721Holder.onERC721Received, but does not advertiseIERC721Receiverthrough ERC-165.