Skip to main content

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:
  1. Source escrow - Holds maker’s tokens on the source chain
  2. 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

  1. Incentivize completion: Resolver loses deposit if they abandon swap
  2. Compensate users: Makers can claim deposit if resolver fails
  3. 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

ParameterDescription
orderHashUnique identifier from order signature
hashlockHash lock commitment
makerOrder creator’s address
takerResolver’s address (set at deployment)
tokenToken contract address
amountToken amount locked
safetyDepositNative token deposit amount
timelocksEncoded time-lock stages
parametersAdditional 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:
  1. Must be in withdrawal stage (private or public)
  2. Must provide the correct secret
  3. Must provide matching immutables
  4. 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:
  1. Must be in cancellation stage
  2. Must provide matching immutables
  3. Must be the maker (private) or anyone (public, source only)
  4. 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

  1. 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])
      }
    }
    
  2. 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
    }
    
  3. Set appropriate safety deposits based on order value
    • Higher value orders need higher deposits
    • Consider gas costs on both chains
    • Account for price volatility
  4. Track escrow events to detect issues early
    • Listen for SrcEscrowCreated events
    • Listen for DstEscrowCreated events
    • Monitor withdrawal and cancellation events

Build docs developers (and LLMs) love