Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
87 changes: 87 additions & 0 deletions .github/workflows/evm-solidity-escrow-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: EVM Solidity Escrow CI

# Standard build and test for the Solidity escrow family, the "Test suite with GitHub Actions"
# deliverable. Scoped to the smart-contracts folder so no other framework folder is affected.
# The matrix covers each escrow variant; add a folder to the matrix when a new variant lands.
on:
push:
paths:
- "EVM/Solidity/smart-contracts/**"
- ".github/workflows/evm-solidity-escrow-ci.yml"
pull_request:
paths:
- "EVM/Solidity/smart-contracts/**"
- ".github/workflows/evm-solidity-escrow-ci.yml"

jobs:
build-test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
dir: [escrow, deposit-escrow, arbitrated-escrow, conditional-escrow]
defaults:
run:
working-directory: EVM/Solidity/smart-contracts/${{ matrix.dir }}
steps:
- uses: actions/checkout@v4

# Pinned to the upstream Foundry named in SPEC.md section 11. Not "stable".
- name: Install Foundry v1.7.1
uses: foundry-rs/foundry-toolchain@v1
with:
version: v1.7.1

- name: Show toolchain
run: forge --version

- name: Build
run: forge build

- name: Test
run: forge test -vvv

# Formal verification. Halmos proves the core properties symbolically, for every input
# in range rather than a fuzzed sample. See each variant's tests/halmos/README.md.
formal:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
include:
- dir: escrow
contracts: "CascEscrowSymbolic NaiveEscrowSymbolic"
- dir: deposit-escrow
contracts: "DepositEscrowSymbolic"
- dir: arbitrated-escrow
contracts: "ArbitratedEscrowSymbolic"
- dir: conditional-escrow
contracts: "ConditionalEscrowSymbolic"
defaults:
run:
working-directory: EVM/Solidity/smart-contracts/${{ matrix.dir }}
steps:
- uses: actions/checkout@v4

- name: Install Foundry v1.7.1
uses: foundry-rs/foundry-toolchain@v1
with:
version: v1.7.1

- uses: actions/setup-python@v5
with:
python-version: "3.12"

# System z3, so the run does not depend on a solver download at test time.
- name: Install z3
run: sudo apt-get update && sudo apt-get install -y z3

- name: Install Halmos
run: pip install halmos==0.3.3

- name: Prove properties
run: |
Z3="$(command -v z3)"
for c in ${{ matrix.contracts }}; do
halmos --function check --contract "$c" --solver-command "$Z3"
done
86 changes: 74 additions & 12 deletions EVM/Solidity/README.md
Original file line number Diff line number Diff line change
@@ -1,19 +1,81 @@
# Solidity (EVM)

## Overview
The high-level, statically-typed language for the Ethereum Virtual Machine.
On-chain escrow for the EVM: a family of four contracts, one per trust model, built to a single
safety standard and verified with tests, invariants, and formal proofs.

## What lives here
This folder is the lab for building, testing, and integrating smart contracts with **Solidity** on **EVM**.
Two parties settle value on-chain. What they trust to release it differs by use case, so the escrow
is a family rather than one contract with every option bolted on.

| Section | Purpose |
## The family at a glance

```mermaid
flowchart TD
Q{"What authorizes release?"}
Q -->|"the code alone"| E["escrow<br/>two-party atomic swap"]
Q -->|"a review clock"| D["deposit-escrow<br/>confirm or timeout"]
Q -->|"a neutral party"| A["arbitrated-escrow<br/>ERC-792 milestones"]
Q -->|"an external fact"| C["conditional-escrow<br/>oracle-gated fill"]
E --> S["One safety model:<br/>observed-delta, CEI, SafeERC20, uint128"]
D --> S
A --> S
C --> S
```

Each variant is a self-contained Foundry project. Read
[smart-contracts/VARIANTS.md](smart-contracts/VARIANTS.md) to choose one.

## Verification bar

Every variant is held to the same standard, not just compiled and shipped.

| Variant | Tests | Coverage | Formal proofs (Halmos) |
| --- | --- | --- | --- |
| escrow | 81 | 100% | 14 |
| deposit-escrow | 29 | 100% | 9 |
| arbitrated-escrow | 40 | 100% | 11 |
| conditional-escrow | 45 | 100% | 6 |

Each also carries invariant tests, differential tests, and mutation testing. Assessed against each
`SPEC.md`; not independently audited.

## Quickstart

```bash
cd smart-contracts/escrow
forge test # 81 passing
forge coverage # 100% lines and branches
```

Foundry `v1.7.1`, solc `0.8.36`. `forge-std` is vendored per variant, so a fresh clone builds with no
extra install.

## Extension surfaces

The escrow stays small. Everything else plugs in behind one seam each, documented as a menu with
recipes and a security checklist.

```mermaid
flowchart LR
Core["escrow core<br/>create / fill / cancel"]
Core -->|"ICondition seam"| O["oracles/<br/>price, event, proof"]
Core -->|"canTransfer gate"| CO["compliance/<br/>identity, jurisdiction, sanctions"]
Core -->|"client and account layer"| W["wallet-connect/<br/>permit, Permit2, AA, passkeys"]
```

- [oracles/](oracles/): gate release on a price or event through the `ICondition` seam. [Guide](oracles/CUSTOMIZATION.md)
- [compliance/](compliance/): permission transfers through an ERC-3643-style `canTransfer` gate. [Guide](compliance/CUSTOMIZATION.md)
- [wallet-connect/](wallet-connect/): remove the two-approval friction with permit, Permit2, account abstraction, passkeys. [Guide](wallet-connect/CUSTOMIZATION.md)

## Documentation map

| To | Go to |
| --- | --- |
| `smart-contracts/` | Contract implementations (starting with an escrow), their tests, and performance notes. |
| `client/` | How to consume these contracts from a client / frontend. |
| `oracles/` | Push vs. pull oracle patterns and provider integrations. |
| `wallet-connect/` | Wallet connection flows: passkeys and social (OAuth2) login. |
| See the contracts and choose a variant | [smart-contracts/](smart-contracts/), [VARIANTS.md](smart-contracts/VARIANTS.md) |
| Integrate from a client or frontend | [client/](client/) |
| Add an oracle, compliance, or wallet flow | [oracles/](oracles/CUSTOMIZATION.md), [compliance/](compliance/CUSTOMIZATION.md), [wallet-connect/](wallet-connect/CUSTOMIZATION.md) |

## Reference
- Official docs: _TODO — add the canonical Solidity documentation link._
- Key repos / examples: _TODO._
- Toolchain & versions: _TODO._

- [Solidity documentation](https://docs.soliditylang.org/) (pinned compiler `0.8.36`)
- [Foundry Book](https://book.getfoundry.sh/) (upstream `v1.7.1`)
- [forge-std](https://github.com/foundry-rs/forge-std) (pinned `v1.16.2`)
135 changes: 127 additions & 8 deletions EVM/Solidity/client/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,127 @@
# Client / Frontend Integration (Solidity)

## What should go here
How to consume the Solidity contracts from a client or frontend:
- SDK / RPC setup and connection.
- Building, signing, and sending transactions.
- Reading contract state.
- A minimal example (script or FE snippet).
# Client and frontend integration

How to drive the escrow from a client. The contract exposes four functions
(`create`, `fill`, `cancel`, `getOrder`) and three events (`OrderCreated`, `OrderFilled`,
`OrderCancelled`). Everything below is those, and the ERC20 approvals they require.

The snippets use [viem](https://viem.sh). The same flow works with ethers or any JSON-RPC
client; only the surface syntax changes.

## The flow

An escrow trade is two parties and two token approvals:

1. Maker approves the escrow to pull tokenA, then calls `create(tokenA, tokenB, amountA, amountB)`.
`create` returns an order id.
2. Taker approves the escrow to pull tokenB, then calls `fill(id)`. tokenB goes to the maker,
the escrowed tokenA goes to the taker.
3. Before a fill, the maker may call `cancel(id)` to recover tokenA.

```mermaid
sequenceDiagram
actor Maker
actor Taker
participant Escrow
Maker->>Escrow: approve tokenA, then create()
Escrow-->>Maker: OrderCreated(id)
Taker->>Escrow: approve tokenB, then fill()
Escrow->>Maker: pays tokenB
Escrow->>Taker: releases tokenA
```

The two approvals are the part new integrators miss: an ERC20 `transferFrom` needs a prior
`approve`, so `create` and `fill` are each two transactions unless you remove the approval step
(see `../wallet-connect/`).

## Maker: approve and create

```ts
import { createWalletClient, createPublicClient, http, parseAbi } from "viem";

const escrowAbi = parseAbi([
"function create(address tokenA, address tokenB, uint128 amountA, uint128 amountB) returns (uint256 id)",
"function fill(uint256 id)",
"function cancel(uint256 id)",
"function getOrder(uint256 id) view returns ((address maker, uint8 status, address tokenA, address tokenB, uint128 amountA, uint128 amountB))",
"event OrderCreated(uint256 indexed id, address indexed maker, address indexed tokenA, address tokenB, uint128 amountA, uint128 amountB)",
"event OrderFilled(uint256 indexed id, address indexed taker)",
"event OrderCancelled(uint256 indexed id)",
]);
const erc20Abi = parseAbi([
"function approve(address spender, uint256 amount) returns (bool)",
]);

const wallet = createWalletClient({ transport: http(RPC_URL) /* account, chain */ });
const pub = createPublicClient({ transport: http(RPC_URL) });

// 1. approve tokenA to the escrow
await wallet.writeContract({
address: tokenA, abi: erc20Abi, functionName: "approve",
args: [ESCROW, amountA],
});

// 2. create the order; read the id back from the receipt's OrderCreated log
const hash = await wallet.writeContract({
address: ESCROW, abi: escrowAbi, functionName: "create",
args: [tokenA, tokenB, amountA, amountB],
});
const receipt = await pub.waitForTransactionReceipt({ hash });
```

`create` returns the id to on-chain callers, but a client reads it from the `OrderCreated`
event in the receipt rather than from the return value, which a transaction does not surface:

```ts
import { decodeEventLog } from "viem";

const id = receipt.logs
.map((log) => {
try { return decodeEventLog({ abi: escrowAbi, ...log }); } catch { return null; }
})
.find((e) => e?.eventName === "OrderCreated")?.args.id;
```

## Taker: approve and fill

```ts
await wallet.writeContract({
address: tokenB, abi: erc20Abi, functionName: "approve", args: [ESCROW, amountB],
});
await wallet.writeContract({
address: ESCROW, abi: escrowAbi, functionName: "fill", args: [id],
});
```

## Reading state

`getOrder(id)` returns the full order tuple. `status` is `0 None`, `1 Open`, `2 Filled`,
`3 Cancelled`. An id that was never issued reads as a zeroed order with status `0`.

```ts
const order = await pub.readContract({
address: ESCROW, abi: escrowAbi, functionName: "getOrder", args: [id],
});
// order.status === 1 => still open and fillable
```

One correctness note from the specification: between the status write and the token transfers
inside `fill`, `getOrder` returns `Filled` while balances are still moving. Do not treat a
`Filled` read from inside a token callback as proof the transfers settled. This is the
disclosed read-only reentrancy window in `../smart-contracts/escrow/SECURITY.md`.

## Watching events

Drive UI from the three events rather than by polling `getOrder`:

```ts
const unwatch = pub.watchContractEvent({
address: ESCROW, abi: escrowAbi, eventName: "OrderFilled",
onLogs: (logs) => {
for (const l of logs) console.log("filled", l.args.id, "by", l.args.taker);
},
});
```

`OrderCreated` is indexed by `id`, `maker`, and `tokenA`, so a client can filter for one
maker's orders or one token's orders without scanning every log. For historical orders, index
`OrderCreated` with a subgraph or a log-range query rather than reading ids one by one.
Loading
Loading