Skip to main content

Building an Oracle-Verified Escrow

Learn to create an escrow contract with external delivery verification using the oracle_attests predicate.

What We're Building

An escrow where:

  • Buyer deposits funds
  • Seller ships goods
  • Trusted oracle confirms delivery
  • Funds release only after oracle verification
  • Timeout protects buyer if oracle fails

Step 1: Create Identities

# Create participant identities
modal id create --name buyer
modal id create --name seller
modal id create --name delivery_oracle

Step 2: Create the Contract

mkdir escrow && cd escrow
modal contract create
modal c checkout

Step 3: Set Up State

# Add identities
modal c set-named-id /users/buyer.id --named buyer
modal c set-named-id /users/seller.id --named seller
modal c set-named-id /oracles/delivery.id --named delivery_oracle

# Set escrow terms
mkdir -p state/escrow
echo '{"price": 100, "currency": "USDC"}' > state/escrow/terms.json

# Set timeout deadline
echo "2026-03-01T00:00:00Z" > state/escrow/timeout.datetime

Step 4: Define the Rules

Create rules/escrow-auth.modality:

export default rule {
starting_at $PARENT
formula {
// All commits must be from a known party or oracle
signed_by(/users/buyer.id) | signed_by(/users/seller.id) | signed_by(/oracles/delivery.id)
}
}

Create rules/escrow-flow.modality — ordering constraints:

export default rule {
starting_at $PARENT
formula {
// Release requires prior delivery attestation
always([+RELEASE] implies <+oracle_attests(/oracles/delivery.id, "delivered", "true")> true)
}
}

Step 5: Synthesize the Model

Use the escrow template as a starting point:

modality model synthesize --template escrow --party-a buyer --party-b seller -o model/escrow.modality

Or describe your requirements in natural language:

modality model synthesize --describe "escrow where buyer deposits, seller ships, oracle confirms delivery before release"

Review and customize the generated model for oracle integration:

export default model {
initial awaiting_deposit

// Buyer deposits funds
awaiting_deposit -> funded [+signed_by(/users/buyer.id)]

// Seller ships goods
funded -> shipped [+signed_by(/users/seller.id)]

// Oracle confirms delivery -> release to seller
shipped -> completed [+oracle_attests(/oracles/delivery.id, "delivered", "true")]

// Oracle denies delivery -> refund buyer
shipped -> refunded [+oracle_attests(/oracles/delivery.id, "delivered", "false")]

// Timeout: buyer can reclaim after deadline
shipped -> refunded [+signed_by(/users/buyer.id), +after(/escrow/timeout.datetime)]

// Terminal states (self-loop)
completed -> completed [+signed_by(/users/buyer.id)]
completed -> completed [+signed_by(/users/seller.id)]
refunded -> refunded [+signed_by(/users/buyer.id)]
refunded -> refunded [+signed_by(/users/seller.id)]
}

Step 6: Commit the Setup

modal c commit --all --sign buyer -m "Initialize escrow"

Step 7: Execute the Contract

Happy Path: Delivery Confirmed

# 1. Buyer deposits
modal c commit --all --sign buyer -m "Buyer deposits"

# 2. Seller ships
modal c commit --all --sign seller -m "Seller ships"

# 3. Oracle attests delivery
modal c commit --all --sign delivery_oracle -m "Oracle confirms, funds released"

Dispute Path: Delivery Failed

modal c commit --all --sign delivery_oracle -m "Oracle denies delivery, buyer refunded"

Timeout Path: Oracle Unresponsive

After the timeout deadline:

modal c commit --all --sign buyer -m "Timeout refund"

Security Properties

PropertyProtection
AuthenticityOnly trusted oracle can attest
IntegritySignature covers all attestation data
FreshnessMax age prevents replay
BindingContract ID prevents cross-contract replay
TimeoutBuyer protected if oracle fails