Polkadot Hub → Arbitrum Sepolia → Hyperliquid Testnet
All deposits are anonymous by default. Destination addresses are ECIES-encrypted and only decryptable inside a TEE. Settlement via TEE attestation (Phala dstack, Intel TDX, ~5-10s).
Velora is a privacy-first intent-based cross-chain bridge between Polkadot Asset Hub and Hyperliquid Testnet (via Arbitrum Sepolia).
- PrivacyPool — Users deposit into a time-locked mixing contract on Polkadot Hub. The TEE filler triggers withdrawal automatically.
- TEE-only settlement (~5-10s) — The filler runs in a Phala dstack CVM (Intel TDX). It decrypts the destination inside the enclave, fills with an unlinkable
fillIdvia a single-use fresh wallet, and settles via TDX attestation. - Standardized amounts — 6, 7, 8, 9, or 10 USDC. Prevents amount-based correlation.
- Delivery delay — Privacy lock from instant to 1 hour. Breaks timing correlation.
- Route — Polkadot Hub → Arbitrum Sepolia → Hyperliquid Testnet. The filler automatically deposits to Hyperliquid via Bridge2 +
usdSend.
No fallback mechanism. The TEE filler is required for all operations.
Comparison with other bridges:
| Criteria | Classic bridge | Optimistic bridge | Velora (privacy-first) |
|-----------------------|------------------------|------------------------|-------------------------------|
| User wait time | 15-45 min | 2-10 min | ~2s (after privacy lock) |
| Privacy | None | None | 6 layers of protection |
| Trust assumption | Validator committee | Honest watcher | TEE attestation (TDX) |
| Capital efficiency | Low (mint/burn) | Medium (bonding) | High (competitive filler) |
| Mixing | No | No | PrivacyPool time-locked |
| Destination visible | Yes | Yes | No (ECIES encrypted) |
| IDs linkable | Yes | Yes | No (fillId unlinkable) |
The bridge works in 7 automated steps. After depositing into the PrivacyPool, the user does nothing — the TEE filler handles withdrawal, fill, Hyperliquid deposit, and settlement automatically.
- Connect wallet on Polkadot Hub Testnet (chain ID 420420417)
- Get Paseo testnet tokens via Faucet Paseo button (links to faucet.polkadot.io)
- Mint USDC via Faucet button (10 USDC, fast confirmation ~2-3s)
- Choose a standardized amount: 6, 7, 8, 9, or 10 USDC
- MAX button selects wallet balance (clamped 6-10 USDC)
- MIN button selects 6 USDC
- Invalid amounts trigger a toast notification (bottom-right)
- Optionally enter a different destination address for maximum privacy
- Configure privacy lock duration via the gear icon (Instant, 15s, 30s, 1 min, custom up to 1h)
- Click "Bridge"
The frontend:
- Generates a random secret (32 bytes) →
commitmentHash = keccak256(secret) - Reads the TEE public key from IntentGateway (
teeEncryptionPubKey()) - ECIES-encrypts the destination address →
encryptedDest - ECIES-encrypts the secret →
encryptedSecret - Calls
DepositRouter.deposit(amount, commitmentHash, encryptedDest, encryptedSecret, delaySeconds)- DepositRouter proxies to PrivacyPool, masking the user's address in Transfer events
- Stores the secret in
localStoragefor recovery
On-chain: Event PrivacyDeposit(commitmentHash, amount, delay, encryptedSecret) — no user address. Transfer shows from: DepositRouter, not the user.
Funds sit in the PrivacyPool for the chosen delay. The contract enforces block.timestamp >= releaseTime. During this time, funds mix with other deposits of the same amount.
The PoolWatcher inside the TEE:
- Detects the
PrivacyDepositevent - Waits for
isReady(commitmentHash) == true - Decrypts the secret inside the TEE enclave (ECIES)
- Calls
PrivacyPool.withdraw(secret, feeOffer, deadline)from the TEE wallet
The PrivacyPool verifies keccak256(secret) == commitmentHash, approves USDC to IntentGateway, and calls createPrivateOrder() — msg.sender = PrivacyPool (not the user).
The TEE Executor:
- Detects
PrivateOrderPlacedon IntentGateway - Decrypts the destination address (ECIES)
- Generates unlinkable
fillId = keccak256(sourceOrderId, teeSecret) - Creates a fresh single-use wallet (random key, used once then discarded)
- Transfers USDC2 + ETH gas to the fresh wallet
- Fresh wallet bridges USDC2 to Hyperliquid via
USDC2.transfer(HL_BRIDGE_ADDRESS, amount) - Waits for HL credit (~30-60s)
- Executes
usdSendfrom fresh wallet to the decrypted destination - Verifies funds arrived — fresh wallet discarded
Relay: Detects OrderFilled(fillId) on Arbitrum Sepolia → maps fillId → sourceOrderId → calls markOrderFilled() on source.
Settlement: TEE signs attestation for sourceOrderId → releaseEscrow() on IntentGateway. TDX quote recorded on-chain. Escrow released on Polkadot Hub. (~5-10s total)
The completion screen displays:
- Amount sent and received (fee-deducted, 2 decimal places)
- Order ID with copy button
- Withdraw TX → Polkadot Blockscout link
- Deposit TX → Polkadot Blockscout link
- HL Bridge TX → Arbiscan link
- Fresh Wallet address with copy button
- TEE Settlement TX → Arbiscan link
- Escrow Release TX → Polkadot Blockscout link
- TEE Trace section: green "Verified by TEE (dstack)" badge with Fill ID, single-use wallet, attestation TX
| Layer | Mechanism | Protection |
|---|---|---|
| 1. PrivacyPool mixing | Time-locked mixing contract on Polkadot Hub | Order created by pool contract (user = PrivacyPool, not the user). DepositRouter masks address in Transfer events |
| 2. Encrypted destination | ECIES secp256k1 with TEE public key | Destination address is an opaque blob on-chain. Only TEE can decrypt |
| 3. Encrypted secret | ECIES secp256k1 with TEE public key | Withdrawal secret encrypted. Only TEE can trigger withdraw. User address never in withdraw TX |
| 4. Unlinkable fillId | keccak256(sourceOrderId, teeSecret) |
Different IDs on source and destination chains. No cross-chain correlation |
| 5. Fresh single-use wallet | Random wallet created per fill | Bridges to HL and executes usdSend. Discarded after use. No pattern of deposits |
| 6. Standardized amounts | 6, 7, 8, 9, 10 USDC | All deposits of the same amount are indistinguishable in the pool |
On Polkadot Hub:
# Deposit (user TX via DepositRouter)
Transfer(from: DepositRouter, to: PrivacyPool, 10 USDC)
PrivacyDeposit(commitmentHash: 0xABC..., amount: 10, delay: 30, encryptedSecret: 0x04fa...)
→ No user address in event
# Withdraw (TEE filler TX)
PrivacyWithdraw(commitmentHash: 0xABC...)
PrivateOrderPlaced(orderId: 0x3414..., user: PrivacyPool, encryptedDest: 0x04d8...)
→ user = PrivacyPool, not Alice
→ TX from TEE wallet, not Alice
On Arbitrum Sepolia:
OrderFilled(
fillId: 0xc034..., ← Different from 0x3414 (unlinkable)
filler: 0x9417..., ← TEE wallet (same for all fills)
recipient: 0xBob..., ← Visible, but not linked to Alice
amount: 10 USDC
)
On Hyperliquid:
usdSend(
from: 0xFresh..., ← Fresh single-use wallet (discarded after)
to: 0xBob..., ← Destination
amount: 9.99 USDC ← Net amount (fee deducted, truncated to 2 decimals)
)
Conclusion: An observer sees that Alice deposited 10 USDC into the pool, and that Bob received ~9.99 USDC on Hyperliquid. But they cannot prove the link:
- The deposit event has no user address (DepositRouter masks Alice)
- The order was created by the PrivacyPool (not Alice)
- The withdraw was triggered by the TEE wallet (not Alice)
fillId ≠ sourceOrderId- The HL deposit comes from a fresh single-use wallet (not the TEE wallet)
- With other 10 USDC deposits in the pool, correlation is impossible
┌──────────────────────────────────────────────────────────────────────────┐
│ Polkadot Asset Hub (Source Chain) │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ DepositRouter (Privacy Proxy) │ │
│ │ deposit(amount, commitmentHash, encryptedDest, encryptedSecret) │ │
│ │ → Forwards to PrivacyPool (masks user address in Transfer events) │ │
│ └───────────────────────────┬────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────────▼────────────────────────────────────────┐ │
│ │ PrivacyPool (Mixing Contract) │ │
│ │ │ │
│ │ deposit() → Lock USDC + register commitment, delay, secrets │ │
│ │ withdraw() → Verify secret, approve → createPrivateOrder() │ │
│ │ Standard amounts: 6, 7, 8, 9, 10 USDC │ │
│ │ Time-lock: 0 to 3600 seconds │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ IntentGateway (Escrow) │ │
│ │ │ │
│ │ createPrivateOrder() → Order with encryptedDest │ │
│ │ markOrderFilled() → Mark as filled (relay) │ │
│ │ releaseEscrow() → Release funds (TEE attestation) │ │
│ │ teeEncryptionPubKey() → TEE ECIES public key │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ Events: │
│ PrivacyDeposit(commitmentHash, amount, delay) — NO user address │
│ PrivateOrderPlaced(orderId, user=PrivacyPool, encryptedDest) │
│ EscrowReleased(orderId, filler, amount) │
└──────────────────────┬───────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ TEE Filler (Phala dstack CVM — Intel TDX) │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ PoolWatcher │ │ Watcher │ │ Executor │ │
│ │ Detect deposits │ │ Detect orders │ │ Decrypt dest │ │
│ │ Decrypt secrets │ │ from Gateway │ │ Fresh wallet │ │
│ │ Trigger withdraw│ │ │ │ Bridge to HL │ │
│ └──────────────────┘ └──────────────────┘ │ usdSend to dest │ │
│ └──────────────────────┘ │
│ ┌──────────────────┐ ┌──────────────────┐ ┌──────────────────────┐ │
│ │ Settler │ │ Relay │ │ API (port 3001) │ │
│ │ TEE attestation │ │ fillId→sourceId │ │ /api/fill/:orderId │ │
│ │ releaseEscrow │ │ markOrderFilled │ │ fillTxHash, fillId │ │
│ └──────────────────┘ └──────────────────┘ │ freshWallet, │ │
│ │ settleTxHash │ │
│ 3 derived keys inside enclave: └──────────────────────┘ │
│ • Wallet (sign TX + attestations) │
│ • Encryption (ECIES decrypt dest + secret) │
│ • FillId secret (keccak256 for unlinkable fillId) │
└──────────────────────┬───────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Arbitrum Sepolia (Destination Chain) │
│ │
│ ┌────────────────────────────────────────────────────────────────────┐ │
│ │ IntentSettler (Fill + Settlement) │ │
│ │ fillOrder(fillId, recipient, token, amount) │ │
│ │ verifyTEESettlement(orderId, attestation, signature) │ │
│ └────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────┐ ┌───────────────────────────────────┐ │
│ │ TEEVerifier │ │ FillerRegistry │ │
│ │ verifyAttestation() │ │ register / stake / slash │ │
│ │ verifyAttestationWithQuote │ │ reputation tracking │ │
│ │ TDX quote recording │ │ │ │
│ └─────────────────────────────┘ └───────────────────────────────────┘ │
│ │
│ Events: │
│ OrderFilled(fillId, filler, recipient, amount) — fillId ≠ sourceOrderId│
│ TEESettlementVerified(orderId, filler, enclaveSigner) │
└──────────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────────────────────────────────────────────────────────┐
│ Hyperliquid Testnet │
│ │
│ Bridge2: 0x08cfc1B6b2dCF36A1480b99353A354AA8AC56f89 │
│ Deposit: USDC2.transfer(bridge, amount) — credited in < 1 min │
│ usdSend: L1 API — internal wallet → wallet transfer (no gas) │
│ Minimum: 5 USDC │
└──────────────────────────────────────────────────────────────────────────┘
Time-locked mixing pool. Standardized amounts enforced on-chain. TEE filler triggers withdrawal automatically.
| Function | Description |
|---|---|
deposit(amount, commitmentHash, encryptedDest, encryptedSecret, delaySeconds) |
Lock standardized USDC (6-10) with time-lock. Event PrivacyDeposit without user address |
withdraw(secret, feeOffer, deadline) |
Called by TEE filler — verifies secret, approves USDC, calls createPrivateOrder(). msg.sender = PrivacyPool |
isReady(commitmentHash) |
View: checks if time-lock has expired |
remainingLockTime(commitmentHash) |
View: remaining seconds before withdrawal is possible |
Forwards deposits to PrivacyPool while masking the user's address in ERC-20 Transfer events. TX shows from: DepositRouter instead of the user.
| Function | Description |
|---|---|
createPrivateOrder(token, amount, destChain, encryptedDest, feeOffer, deadline) |
Creates order with encrypted destination. Called by PrivacyPool. Emits PrivateOrderPlaced |
markOrderFilled(orderId) |
Called by relay after fill detection on destination |
releaseEscrow(orderId, fillerAddress, attestation) |
Verifies TEE attestation and releases funds to filler |
cancelOrder(orderId) |
Cancel by user after deadline if order not filled |
teeEncryptionPubKey() |
View: TEE ECIES public key for frontend encryption |
getOrder(orderId) |
View: full order details |
| Function | Description |
|---|---|
fillOrder(fillId, recipient, token, amount) |
Filler fronts funds with unlinkable fillId. Emits OrderFilled |
verifyTEESettlement(orderId, attestation, signature) |
Verifies TEE attestation signed by enclave |
verifyTEESettlementWithQuote(orderId, attestation, signature, tdxQuote) |
Same + records TDX quote hash on-chain |
getFill(orderId) |
View: fill details |
| Function | Description |
|---|---|
verifyAttestation(orderId, attestation, signature) |
Verifies ECDSA signature from trusted enclave |
verifyAttestationWithQuote(orderId, attestation, signature, tdxQuote) |
Verifies + records TDX quote hash |
addTrustedEnclave(enclaveAddress) |
Adds trusted enclave address (owner only) |
| Function | Description |
|---|---|
register(stake) |
Stake USDC as solvency guarantee |
recordFill(filler, orderId) |
Record a successful fill |
recordSettlement(filler, orderId) |
Record a successful settlement |
slash(filler, orderId) |
Slash for malicious behavior |
| Contract | Description |
|---|---|
OrderLib.sol |
Order encoding/decoding, computeOrderId(), computePrivateOrderId(), status transitions |
Constants.sol |
MIN_FEE_BPS=5, MAX_FEE_BPS=100, FILLER_WINDOW=30, MIN_ORDER_AMOUNT=1e6, MAX_ORDER_AMOUNT=1_000_000e6 |
The filler runs in a Confidential VM (CVM) via Phala dstack on Intel TDX.
┌─────────────────────────────────────────────────────────────────┐
│ TEE SETTLEMENT (~5-10s) │
│ │
│ 3 independent keys derived inside the enclave: │
│ • Wallet: sign TX, attestations, trigger pool withdrawals │
│ • Encryption: decrypt dest + secret (ECIES secp256k1) │
│ • FillId secret: keccak256(sourceOrderId, secret) = unlinkable │
│ │
│ T+0s Filler detects PrivateOrderPlaced │
│ T+1s Decrypt dest with ECIES key inside TEE │
│ T+2s Generate unlinkable fillId │
│ T+3s Create fresh wallet, transfer USDC2 + gas │
│ T+4s Fresh wallet bridges to Hyperliquid │
│ T+5s usdSend from fresh wallet → destination │
│ T+7s dstack tdxQuote() → TDX attestation │
│ T+8s Sign attestation with TEE wallet key │
│ T+9s releaseEscrow() on IntentGateway │
│ T+10s Escrow released, order complete │
│ │
│ On-chain attestation: │
│ • TEEVerifier.AttestationVerified(orderId, signer) │
│ • TEEVerifier.TDXQuoteRecorded(orderId, tdxQuoteHash) │
│ • IntentSettler.TEESettlementVerified(orderId, filler, enclave)│
└─────────────────────────────────────────────────────────────────┘
| Mode | Transport | Keys | Usage |
|---|---|---|---|
dstack |
Unix socket /var/run/dstack.sock |
Derived by dstack KMS (tied to app-id) | Production (Phala Cloud) |
simulator |
HTTP localhost:8090 |
Derived by simulator | Local dev with Docker |
mock |
None | Derived from TEE_PRIVATE_KEY via keccak256 |
Hackathon / tests |
| Module | Description |
|---|---|
| pool-watcher | Detects PrivacyDeposit events, decrypts secrets inside TEE, triggers withdraw() after time-lock expiry |
| watcher | Listens for PrivateOrderPlaced events on source chain |
| executor | Decrypts destination (ECIES), generates unlinkable fillId, creates fresh single-use wallet, bridges to Hyperliquid, executes usdSend to destination |
| settler | TEE attestation settlement: dual attestation (sourceOrderId on source) + releaseEscrow() |
| relay | Cross-chain relay: detects OrderFilled on dest → maps fillId → sourceOrderId → calls markOrderFilled() on source |
| tee | Phala dstack TEE client: 3 derived keys (wallet, encryption, fillId secret), TDX attestation, ECIES decryption |
| state | Full lifecycle tracking: DETECTED → FILLING → FILLED → SETTLING → SETTLED → RELEASED |
| api | HTTP API (port 3001): /api/fill/:orderId returns fillTxHash, freshWalletAddress, settleTxHash, releaseTxHash, fillId, status |
| config | Chain configuration, RPC endpoints, multi-destination support |
Built with React 19, Vite, wagmi v3, viem, motion/react, and eciesjs.
- Full state machine: input → approve → deposit to pool → privacy lock countdown → filler delivery → settlement → completed
- Amount input: only whole numbers 6-10 USDC. Toast notification (bottom-right, auto-dismiss 4s) for invalid amounts
- MAX button: sets amount to wallet balance (clamped 6-10 USDC). MIN button: sets 6 USDC
- Privacy lock config via gear icon: Instant, 15s, 30s, 1 min, or custom up to 1 hour
- Optional destination address input for maximum privacy
- Route card showing receive amount (fee-deducted, 2 decimals), estimated time, settlement mode
- Track tab: monitor any order by ID with live status polling on both chains
- "Bridge Complete" with green checkmark
- All TX hash links: Withdraw TX, Deposit TX, HL Bridge TX (Polkadot Blockscout + Arbiscan)
- Fresh Wallet address with copy button
- TEE Settlement TX → Arbiscan link
- Escrow Release TX → Polkadot Blockscout link
- TEE Trace section: green shield badge "Verified by TEE (dstack)", Fill ID (unlinkable) with copy, single-use wallet with copy, attestation TX link
- Faucet Paseo: header button linking to
faucet.polkadot.iofor Paseo testnet gas tokens - Faucet USDC: header button minting 10 USDC on Polkadot Hub with fast polling (~2-3s) and explicit gas parameter
| Contract | Address | Chain |
|---|---|---|
| PrivacyPool | 0x12569697a1A20a9e850Cce09F2D5e517CDB7a42F |
Polkadot Hub Testnet |
| IntentGateway | 0x10507B42691793b2E93179300D8008B849189665 |
Polkadot Hub Testnet |
| IntentSettler | 0x821d957c3cBE99ED4144Ae675AE44fd80E32223d |
Arbitrum Sepolia |
| TEEVerifier | 0xC2DdC138A80136a30E0f5731954d765099ba270A |
Arbitrum Sepolia |
| FillerRegistry | 0xF1cdf11e0919dE9a2978dF58C621f4487Af24EdF |
Arbitrum Sepolia |
| USDC (source) | 0xb5C8B011f9EBDBB73cd9EF89aCdb05d64Cd972b2 |
Polkadot Hub Testnet |
| USDC2 (dest) | 0x1baAbB04529D43a73232B713C0FE471f7c7334d5 |
Arbitrum Sepolia |
| HL Bridge2 | 0x08cfc1B6b2dCF36A1480b99353A354AA8AC56f89 |
Arbitrum Sepolia |
- Foundry (forge, cast)
- Node.js >= 18
- Docker (for dstack simulator)
cd contracts
forge install
forge build
forge test -vvv83 tests: unit, fuzz, TEE verification, TDX quote recording, private orders, privacy pool, access control, end-to-end.
cd filler
npm install
cp .env.example .env # Fill in RPC endpoints, private key, contract addresses
npm run build
npm startSet TEE_MODE in .env: mock (default), simulator, or dstack.
cd frontend
npm install
cp .env.example .env # Fill in contract addresses
npm run dev # http://localhost:5173Source: Polkadot Hub Testnet (chain 420420417). Destination: Arbitrum Sepolia (chain 421614).
cd contracts
cp .env.example .env # Fill PRIVATE_KEY, RPC_SOURCE, RPC_DESTINATION
# Deploy source contracts (Polkadot Hub Testnet)
foundryup --version nightly
forge script script/DeployAssetHub.s.sol --chain polkadot-testnet --rpc-url https://eth-rpc-testnet.polkadot.io/ --broadcast
# Deploy destination contracts (Arbitrum Sepolia)
forge script script/DeployArbitrumSepolia.s.sol --rpc-url https://sepolia-rollup.arbitrum.io/rpc --broadcast
# Deploy TEE verifier
forge script script/DeployTEEVerifier.s.sol --rpc-url https://sepolia-rollup.arbitrum.io/rpc --broadcastfeeAmount = (amountWei * feeBps) / 10000
rawReceive = amountWei - feeAmount
receiveAmount = truncate(rawReceive, 2 decimals) // match HL usdSend precision
| Parameter | Value |
|---|---|
| Default fee | 10 bps (0.10%) |
| Fee range | 5 bps (min) to 100 bps (max) |
| Recipient | TEE filler who fronts the anonymous fill |
| Precision | Truncated to 2 decimal places (e.g., 9.99 USDC) |
| Amounts | 6, 7, 8, 9, 10 USDC (whole numbers, enforced on-chain) |
| User gas | Approve + deposit on Polkadot Hub only |
| Filler gas | Withdraw, fill, bridge, usdSend, settlement, escrow release |
After bridging, verify on-chain using the TX links on the completion screen:
- Polkadot Hub — Find
PrivacyDepositevent: it has no user address, onlycommitmentHashand encrypted blobs - Polkadot Hub — ERC-20
Transferevent showsfrom: DepositRouter(not your wallet) - Polkadot Hub —
PrivateOrderPlacedevent showsuser = PrivacyPool(0x1256...), not your address - Polkadot Hub — Withdraw TX shows
from: TEE filler wallet, not you - Arbitrum Sepolia —
OrderFilledevent:fillIdis different fromsourceOrderId. No cross-chain link - Hyperliquid —
usdSendcomes from a fresh single-use wallet (shown on completion screen), not the TEE wallet
velora/
├── contracts/
│ ├── src/
│ │ ├── source/
│ │ │ ├── IntentGateway.sol # Escrow, createPrivateOrder, releaseEscrow
│ │ │ └── PrivacyPool.sol # Mixing pool (6-10 USDC), deposit/withdraw
│ │ ├── destination/
│ │ │ ├── IntentSettler.sol # Fill (fillId unlinkable), TEE settlement
│ │ │ └── FillerRegistry.sol # Filler staking, reputation
│ │ ├── settlement/
│ │ │ └── TEEVerifier.sol # TEE attestation + TDX quotes
│ │ ├── libs/
│ │ │ ├── OrderLib.sol # Order encoding, hash, status transitions
│ │ │ └── Constants.sol # Protocol parameters
│ │ └── interfaces/
│ │ ├── IIntentGateway.sol
│ │ ├── IIntentSettler.sol
│ │ ├── IFillerRegistry.sol
│ │ └── ITEEVerifier.sol
│ ├── test/ # 83 tests
│ └── script/ # Deploy scripts
│
├── filler/
│ └── src/
│ ├── index.ts # Entry point
│ ├── pool-watcher.ts # PrivacyDeposit → decrypt secret → withdraw
│ ├── watcher.ts # PrivateOrderPlaced events
│ ├── executor.ts # Decrypt dest, fresh wallet, bridge HL, usdSend
│ ├── settler.ts # TEE attestation + releaseEscrow
│ ├── relay.ts # Cross-chain relay
│ ├── state.ts # Lifecycle tracking
│ ├── tee.ts # Phala dstack client (3 keys, attestation)
│ ├── api.ts # HTTP API
│ └── config.ts # Chain config
│
├── frontend/
│ └── src/
│ ├── App.jsx # Router, layout, header (Faucet Paseo + Faucet + Connect)
│ ├── components/
│ │ ├── BridgeWidget.jsx # Bridge state machine + validation toast + TEE trace
│ │ ├── FaucetButton.jsx # Mint 10 USDC (fast polling)
│ │ ├── ConnectButton.jsx # Wallet connection
│ │ ├── ResourcesPage.jsx # Documentation page
│ │ └── ... # Plasma, Menu, Team, etc.
│ └── config/
│ ├── contracts.js # Deployed addresses, ABIs
│ └── wagmi.js # Chain + transport config
│
└── README.md # This file
| Package | Usage |
|---|---|
| Solidity 0.8.24 | Smart contract language |
| Foundry | Build, test, deploy |
| OpenZeppelin 5.x | ERC-20, SafeERC20, ReentrancyGuard, Pausable, Ownable |
| Package | Usage |
|---|---|
| TypeScript | Static typing |
| ethers v6 | EVM interaction (Polkadot Hub + Arbitrum Sepolia) |
| @phala/dstack-sdk | TEE client (TappdClient, deriveKey, tdxQuote) |
| eciesjs | ECIES encryption/decryption secp256k1 |
| Package | Usage |
|---|---|
| React 19 | UI framework |
| Vite | Bundler + dev server |
| wagmi v3 + viem | EVM interaction, contract reads/writes |
| @tanstack/react-query | Data fetching (wagmi dependency) |
| eciesjs | ECIES encryption (destination + secret) |
| motion/react | Animations (Framer Motion) |
| gsap + ogl | Background WebGL + menu animations |
| react-router-dom | Routing |
1. PrivacyPool mixing — Funds mixed with other deposits of same amount. Time-lock enforced.
2. ECIES encryption — Only TEE can decrypt destination and withdrawal secret.
3. Unlinkable fillId — keccak256(sourceOrderId, teeSecret). No cross-chain link.
4. Fresh single-use wallet — Random wallet per fill. Discarded after use.
5. TEE attestation (TDX) — Hardware-backed proof of correct execution. TDX quote on-chain.
6. Double-fill protection — An orderId can only be filled once. fillOrder() reverts on duplicate.
7. Deadline enforcement — Cancel after deadline. No cancel before deadline (filler protection).
8. ReentrancyGuard — All mutative functions protected.
9. Standardized amounts — 6-10 USDC enforced on-chain. Frontend toast for invalid input.
- Phala dstack — Confidential VMs
- ERC-7683 — Cross-Chain Intents Standard
- Hyperbridge — Interoperability Protocol
- ISMP — Interoperable State Machine Protocol
- Polkadot BEEFY
- Hyperliquid Documentation
- OpenZeppelin Contracts 5.x
- Foundry Book
MIT