Skip to main content

Tokenized escrow for real estate

LCVEscrow.sol is a milestone escrow: a buyer deposits the full sale price up front, and a neutral arbiter releases funds to the seller milestone by milestone as delivery progresses — or refunds whatever's undisbursed if the deal falls through. This walks through a full lifecycle (create, deposit, release) with three real accounts; every step below was actually run against a live devnet.

Prerequisites

A running devnet with EVM RPC reachable, Foundry installed, and LCVEscrow deployed — see Deploy a Solidity contract if you haven't done that yet. This tutorial uses three separate accounts: buyer, seller, arbiter.

cast wallet new # buyer (fund it — see the previous tutorial)
cast wallet new # seller (no funds needed — receiving only)
cast wallet new # arbiter (fund it — pays gas to release milestones)

Create the escrow

The buyer calls createEscrow, listing each milestone's description and amount (in wei — 18-decimal, same as any EVM native-currency amount):

cast send $CONTRACT \
"createEscrow(address,address,string[],uint256[])" \
$SELLER $ARBITER \
'["Foundation complete","Final delivery"]' \
'[3000000000000000000,2000000000000000000]' \
--rpc-url http://localhost:8545 --private-key $BUYER_KEY

This is a 5 LCV total deal (3 + 2), across two milestones. escrowId starts at 0 and increments per escrow — this call returns escrowId 0.

Deposit

The buyer sends the full amount in one call (partial deposits across multiple calls also work — the contract just rejects anything that would push deposited past totalAmount):

cast send $CONTRACT "deposit(uint256)" 0 \
--value 5000000000000000000 \
--rpc-url http://localhost:8545 --private-key $BUYER_KEY

Check the escrow state:

cast call $CONTRACT \
"getEscrow(uint256)(address,address,address,uint256,uint256,uint256,bool)" 0 \
--rpc-url http://localhost:8545
# buyer, seller, arbiter, totalAmount=5e18, deposited=5e18, releasedAmount=0, cancelled=false

Release a milestone

Only the arbiter can do this, and only once the escrow is fully deposited:

cast balance $SELLER --rpc-url http://localhost:8545
# 0

cast send $CONTRACT "releaseMilestone(uint256,uint256)" 0 0 \
--rpc-url http://localhost:8545 --private-key $ARBITER_KEY

cast balance $SELLER --rpc-url http://localhost:8545
# 3000000000000000000 — exactly the first milestone's amount

Release the second milestone (releaseMilestone(0, 1)) the same way once its deliverable is confirmed. Each milestone can only be released once (MilestoneAlreadyReleased).

If the deal falls through

The arbiter can cancel and refund whatever hasn't been released back to the buyer:

cast send $CONTRACT "cancelAndRefund(uint256)" 0 \
--rpc-url http://localhost:8545 --private-key $ARBITER_KEY

This refunds deposited - releasedAmount to the buyer and marks the escrow cancelled — further deposits or releases against it then revert with EscrowIsCancelled.

What this doesn't handle

This is a reference contract, not a production-ready real-estate settlement system: there's no dispute resolution beyond "trust the arbiter," no partial-milestone amounts, and no integration with x/registry (LCV's on-chain asset registry) to actually transfer title alongside the funds — those would need to be composed on top, and any real deployment needs its own legal/regulatory review for the jurisdiction involved.