Skip to main content
Milestone escrow lets two parties execute a multi-tranche deal without trusting each other upfront. The payer deposits stablecoin funds into a smart contract; those funds are locked until the payee marks each milestone delivered and the payer confirms the work. If something goes wrong, either party can raise a dispute and an optional arbiter steps in to resolve it — or, if no arbiter is set, a configurable timelock allows force-release after the dispute window expires. This makes escrow particularly useful for RWA deals involving construction draws, trade finance tranches, or multi-phase land development.

How it works

Each milestone in the array is funded and settled independently — the payer can pay in installments rather than depositing the entire contract value up front.

Deploy an escrow

Call TokenFactory.deployEscrow() with a DeployEscrowOptions object. The token field must be a stablecoin address whitelisted on the EscrowFactory contract for your network.
deployEscrow() returns an EscrowDeployResult:
The descriptionHash field is a bytes32 on-chain. Use ethers.id() to hash a human-readable string, or pass ethers.ZeroHash if you’re tracking descriptions off-chain.

EscrowManager operations

Instantiate EscrowManager with the adapter from your TokenFactory and the escrowAddress from the deploy result. All write methods return the transaction hash as a string.

Fund a milestone

The payer deposits tokens for a single milestone. On EVM, approve the escrow contract to spend the milestone amount before calling fund():
On Stellar, the nested token transfer is authorized within the same Soroban invocation — no separate approval step is needed.

Mark a milestone delivered

The payee calls this when the work for a milestone is complete:

Approve a milestone

The payer reviews the deliverable and releases the funds to the payee:

Raise a dispute

Either the payer or payee can freeze a delivered milestone pending arbiter review:

Resolve a dispute

The arbiter resolves the frozen milestone. Pass true to release to the payee, or false to refund the payer:

Claim a timelock release

If no arbiter is set (or the arbiter is unresponsive), the payee can force-release a delivered, non-disputed milestone once the timelock window has elapsed:

Read activity history

getActivity() returns the full lifecycle event log for this escrow, sorted oldest-first:
Each EscrowActivityEvent has the shape:

Read milestone state

Cancel a deal

Both parties must call voteCancel() for the deal to be cancelled — a single vote is not enough:

CLI escrow commands

The Ankara Chain CLI provides commands for every escrow lifecycle action. All commands read the network and signer from ankara.config.json.

Stellar

On Stellar, pass a StellarAdapter (from a TokenFactory configured with network: "stellar-testnet") to EscrowManager. The API is identical — same methods, same parameter types. The testnet EscrowFactory contract ID is: