What are Escrows?
Escrows are smart contracts that temporarily hold tokens during cross-chain swaps. They ensure that:
- Funds are locked until the swap completes
- Only authorized parties can withdraw
- Funds can be refunded if the swap fails
Every cross-chain swap involves two escrow contracts:
- Source escrow - Holds maker’s tokens on the source chain
- Destination escrow - Holds resolver’s tokens on the destination chain
Source and Destination Escrows
Source Escrow
Created on the blockchain where the swap originates:
// From Resolver.sol:54-81
function deploySrc(
IBaseEscrow.Immutables calldata immutables,
IOrderMixin.Order calldata order,
bytes32 r,
bytes32 vs,
uint256 amount,
TakerTraits takerTraits,
bytes calldata args
) external payable onlyOwner {
IBaseEscrow.Immutables memory immutablesMem = immutables;
immutablesMem.timelocks = TimelocksLib.setDeployedAt(
immutables.timelocks,
block.timestamp
);
address computed = _FACTORY.addressOfEscrowSrc(immutablesMem);
(bool success, ) = address(computed).call{
value: immutablesMem.safetyDeposit
}('');
if (!success) revert IBaseEscrow.NativeTokenSendingFailure();
_LOP.fillOrderArgs(order, r, vs, amount, takerTraits, argsMem);
}
Key properties:
- Holds maker’s source tokens
- Locked with hash-lock from order
- Has 5 time-locked stages
- Requires resolver safety deposit
Destination Escrow
Created on the target blockchain:
// From Resolver.sol:86-98
function deployDst(
IBaseEscrow.Immutables calldata dstImmutables,
uint256 srcCancellationTimestamp
) external payable onlyOwner {
IERC20(dstImmutables.token.get()).forceApprove(
address(_FACTORY),
type(uint256).max
);
_FACTORY.createDstEscrow{value: msg.value}(
dstImmutables,
srcCancellationTimestamp
);
}
Key properties:
- Holds resolver’s destination tokens
- Shares same hash-lock as source escrow
- Has 4 time-locked stages (no public cancellation)
- Requires resolver safety deposit
Escrow Lifecycle
Stage 1: Finality Lock
Purpose: Wait for blockchain finality to prevent reorg attacks
// From src-time-locks.ts:131-133
public isFinalityLock(time = BigInt(now())): boolean {
return time < this.privateWithdrawal
}
- No withdrawals allowed
- No cancellations allowed
- Ensures transactions are truly confirmed
Stage 2: Private Withdrawal
Purpose: Exclusive window for the taker (resolver)
// From src-time-locks.ts:140-142
public isPrivateWithdrawal(time = BigInt(now())): boolean {
return time >= this.privateWithdrawal && time < this.publicWithdrawal
}
- Only taker can withdraw with secret
- Prevents front-running by other parties
- Rewards the resolver who deployed escrows
Stage 3: Public Withdrawal
Purpose: Allow anyone to complete the swap if taker doesn’t
// From src-time-locks.ts:149-151
public isPublicWithdrawal(time = BigInt(now())): boolean {
return time >= this.publicWithdrawal && time < this.privateCancellation
}
- Anyone can withdraw with the secret
- Ensures swap completes even if resolver fails
- Maker can withdraw their own funds on destination
Stage 4: Private Cancellation
Purpose: Allow maker to cancel and recover funds
// From src-time-locks.ts:158-161
public isPrivateCancellation(time = BigInt(now())): boolean {
return (
time >= this.privateCancellation && time < this.publicCancellation
)
}
- Only maker can cancel
- Recovers original tokens + safety deposit
- Penalizes resolver for not completing swap
Stage 5: Public Cancellation (Source Only)
Purpose: Emergency recovery if maker doesn’t cancel
// From src-time-locks.ts:169-171
public isPublicCancellation(time = BigInt(now())): boolean {
return time >= this.publicCancellation
}
Destination escrows don’t have public cancellation - only private cancellation. This is because the destination escrow should already be resolved if we reach cancellation stage.
Safety Deposits
Safety deposits are native tokens (ETH, MATIC, BNB, etc.) locked by the resolver in both escrows.
Purpose
- Incentivize completion: Resolver loses deposit if they abandon swap
- Compensate users: Makers can claim deposit if resolver fails
- Economic security: Makes attacks expensive
Amounts
// From escrow-extension.ts:52-53
public readonly srcSafetyDeposit: bigint,
public readonly dstSafetyDeposit: bigint,
Safety deposits are specified separately for each chain and encoded together:
// From escrow-extension.ts:199-201
private encodeCrossChainData(): string {
const packedSafetyDeposit =
(this.srcSafetyDeposit << 128n) | this.dstSafetyDeposit
Safety deposits are packed into a single uint256: upper 128 bits for source, lower 128 bits for destination.
Claiming Safety Deposits
If a resolver fails to complete a swap, the maker can cancel during private cancellation and receive:
- Their original tokens back
- The resolver’s safety deposit as compensation
Contract Addresses and Deployments
Computing Escrow Addresses
Escrow addresses are deterministically computed using CREATE2:
// From escrow-factory.ts:29-51
public getEscrowAddress(
immutablesHash: string,
implementationAddress: Address
): Address {
assert(
isHexBytes(immutablesHash) && getBytesCount(immutablesHash) === 32n,
'invalid hash'
)
return Address.fromString(
getCreate2Address(
this.address.toString(),
immutablesHash,
EscrowFactory.calcProxyBytecodeHash(implementationAddress)
)
)
}
Benefits of deterministic addresses:
- Makers can verify escrow address before secret reveal
- Escrows can be deployed permissionlessly
- No need for escrow registry
Source Escrow Address
// From escrow-factory.ts:58-72
public getSrcEscrowAddress(
srcImmutables: Immutables<Address>,
implementationAddress: Address
): Address {
return this.getEscrowAddress(
srcImmutables.hash(),
implementationAddress
)
}
Destination Escrow Address
// From escrow-factory.ts:79-109
public getDstEscrowAddress(
srcImmutables: Immutables<Address>,
complement: DstImmutablesComplement<Address>,
blockTime: bigint,
taker: Address,
implementationAddress: Address
): Address {
return this.getEscrowAddress(
srcImmutables
.withComplement(complement)
.withTaker(taker)
.withDeployedAt(blockTime)
.hash(),
implementationAddress
)
}
Destination escrow address depends on the taker (resolver) and deployment time, while source escrow address is fixed at order creation.
Immutables
Each escrow is configured with immutable parameters:
// From immutables.ts:22-33
static readonly Web3Type = `tuple(${
'bytes32 orderHash',
'bytes32 hashlock',
'address maker',
'address taker',
'address token',
'uint256 amount',
'uint256 safetyDeposit',
'uint256 timelocks',
'bytes parameters'
})`
Key Parameters
| Parameter | Description |
|---|
orderHash | Unique identifier from order signature |
hashlock | Hash lock commitment |
maker | Order creator’s address |
taker | Resolver’s address (set at deployment) |
token | Token contract address |
amount | Token amount locked |
safetyDeposit | Native token deposit amount |
timelocks | Encoded time-lock stages |
parameters | Additional parameters (fees, etc.) |
Immutables Hash
The hash of immutables determines the escrow address:
// From immutables.ts:188-218
hash(): string {
const coder = AbiCoder.defaultAbiCoder()
const parametersHash = keccak256(this.encodeFees())
const encoded = coder.encode(
[
'bytes32', 'bytes32', 'address', 'address', 'address',
'uint256', 'uint256', 'uint256', 'bytes32'
],
[
add0x(this.orderHash.toString('hex')),
this.hashLock.toString(),
this.maker.toHex(),
this.taker.toHex(),
this.token.nativeAsZero().toHex(),
this.amount,
this.safetyDeposit,
this.timeLocks.build(),
parametersHash
]
)
return keccak256(encoded)
}
Withdrawal Process
// From Resolver.sol:100-106
function withdraw(
IEscrow escrow,
bytes32 secret,
IBaseEscrow.Immutables calldata immutables
) external {
escrow.withdraw(secret, immutables)
}
To withdraw from an escrow:
- Must be in withdrawal stage (private or public)
- Must provide the correct secret
- Must provide matching immutables
- Secret hash must match the hash lock
Cancellation Process
// From Resolver.sol:108-113
function cancel(
IEscrow escrow,
IBaseEscrow.Immutables calldata immutables
) external {
escrow.cancel(immutables)
}
To cancel an escrow:
- Must be in cancellation stage
- Must provide matching immutables
- Must be the maker (private) or anyone (public, source only)
- Receives tokens + safety deposit
Example: Working with Escrows
import { EscrowFactory, Immutables, EvmAddress } from '@1inch/cross-chain-sdk'
// Initialize escrow factory
const factory = new EscrowFactory(
EvmAddress.fromString('0x...')
)
// Get source escrow address (before deployment)
const srcEscrowAddress = factory.getSrcEscrowAddress(
srcImmutables,
implementationAddress
)
console.log('Source escrow will be deployed at:', srcEscrowAddress.toString())
// Verify this matches expected address before revealing secret!
if (srcEscrowAddress.toString() !== expectedAddress) {
throw new Error('Escrow address mismatch!')
}
// Get destination escrow address (after deployment)
const dstEscrowAddress = factory.getDstEscrowAddress(
srcImmutables,
dstComplement,
blockTime,
takerAddress,
implementationAddress
)
console.log('Destination escrow deployed at:', dstEscrowAddress.toString())
Best Practices
-
Always verify escrow addresses before revealing secrets
const fills = await sdk.getReadyToAcceptSecretFills(orderHash)
for (const fill of fills.fills) {
// Verify escrow addresses and parameters
const isValid = await verifyEscrowDeployment(fill)
if (isValid) {
await sdk.submitSecret(orderHash, secrets[fill.idx])
}
}
-
Monitor escrow stages to know when actions are possible
const srcTimeLocks = order.timeLocks.toSrcTimeLocks(deployedAt)
const stage = srcTimeLocks.getStage()
if (stage === SrcStage.PrivateCancellation) {
// Can cancel now
}
-
Set appropriate safety deposits based on order value
- Higher value orders need higher deposits
- Consider gas costs on both chains
- Account for price volatility
-
Track escrow events to detect issues early
- Listen for SrcEscrowCreated events
- Listen for DstEscrowCreated events
- Monitor withdrawal and cancellation events