Search projects, hackathons, sponsors and tech

    ← All projects

    attesta

    Marketplace for trading strategies whose returns are attested inside a TEE, not self-reported

    Demo

    About

    attesta is a marketplace for automated trading strategies where the track record is attested rather than claimed. The problem it attacks is simple. Anyone can publish a 40% APY. The number is produced by whoever profits from it, and a strategy with a real edge will not reveal the code behind it — so an investor is asked to trust a screenshot. attesta removes the need to trust either side. HOW IT WORKS A creator writes a strategy in TypeScript against a fixed six-function contract (describe, onTick, balanceOf, totalAssets, onDeposit, onWithdraw) and submits it in the browser. The platform compiles it and deploys it as a Chainlink CRE confidential workflow, so the strategy's identity becomes its binary hash: binary hash + config hash -> workflow hash, registered on-chain in a StrategyRegistry. Swap the script after building a track record and the hash changes, in public. Creators need no CRE account of their own; the platform deploys under its owner identity, and anyone can recompile the published source and check the hash matches what is registered. When a tick fires, the strategy does not run on a node — it runs inside a TEE (AWS Nitro Enclaves, us-west-2). It fetches prices over HTTP from inside the enclave, reads its creator's parameters as Vault DON secrets that are released only into an attested enclave, and returns a target allocation in basis points. Risk guardrails are evaluated in the enclave too, so the rules stay private while remaining verifiably applied. Strategy parameters never reach the platform in the clear. They are encrypted in the creator's own browser before they leave the machine; the backend relays ciphertext, stores it in a table with no plaintext column, and holds no key that opens it. There is no read-back API at all. So a creator can publish a verifiable track record without revealing the thresholds and signals that produce it — which is the thing that makes a real quant willing to list at all. Investors sign in with a wallet, browse the marketplace, and allocate USDC. Each strategy has its own on-chain vault with ERC-4626 share math. Deposits and withdrawals are signed in the browser and settled on-chain into that vault; the backend records what the transaction receipt says happened, not what the client claims, so a forged or replayed hash records nothing. The strategy page shows NAV per share, the investor's own position series, every trade, every execution log line, total return, max drawdown and — once the history is long enough to state it honestly — APY. WHY THE NUMBERS HOLD Nothing about performance is hardcoded; that is the point of the whole build. Each tick runs the real strategy against a live price series, prices the decision it returned (target weights x price delta since the last tick, minus venue cost in bps), and applies the resulting profit or loss on-chain with vault.applyPnl(int256), which emits PnlApplied. Performance is then read back from the vault's own settlement log — PnlApplied, Deposited, Withdrawn — rather than from a table the backend wrote. The number on the screen is the chain's account of what happened, not the server's, and one getLogs call covers a whole listing. Total return, max drawdown and APY are derived from that series and stored nowhere as inputs. APY is deliberately withheld until the series is long enough to annualise honestly. Annualising twenty minutes of history turns a 1.4% move into a meaningless exponent and turns every loss into exactly -100%, so below that threshold the metric returns null and the card leads with since-inception total return instead. That is a real figure at any span, and it still separates a strategy down 1.4% from one down 13.4%, which saturated annualisation does not. A strategy that picks badly shows a negative return because its weights lost money, not because a field says so. Three seeded strategies — momentum, mean-reversion, and a deliberately bad overtrader that churns itself into fees — demonstrate exactly that, side by side in the same marketplace. The verifiable chain runs end to end: TypeScript workflow -> registered workflow identity -> TEE attestation -> signed reports -> on-chain settlement record -> reported returns. Break any link — a different workflow, a different enclave, an unsigned record — and the performance claim stops verifying. WHAT IS HONEST ABOUT THE LIMITS This build integrates two partner technologies: Chainlink CRE for confidential compute, and Privy for investor onboarding. Settlement is USDC vaults on a local chain, and a strategy's operator — the only account allowed to settle a tick — is a local EOA. Circle Agent Wallets on Arc are the intended production form for that operator, and the design is written against them: one policy-capped wallet per strategy, its control credential held as a Vault secret so only the attested workflow could direct it. That integration is out of scope here and nothing in the demo depends on it. Saying so plainly matters more than the extra logo would. Two further limits. CRE Vault secrets are scoped to the owner rather than to a single workflow, so isolation between creators rests on every workflow under the owner being enumerable and hash-verifiable rather than on the Vault itself. And phase 1 does not audit strategy code — "verified" means "this exact code produced these results", not "this code is safe". All of these are stated in the docs and in the UI rather than glossed over, because a product whose entire claim is verifiability cannot be vague about what is not verified. Everything runs locally against an anvil chain, which is why the demo link points at the repository: ./scripts/dev.sh brings up anvil, deploys the contracts, seeds the marketplace and serves the backend on :4000 and the frontend on :3000.

    How it's made

    Five packages in one repo: contracts/ (Foundry), backend/ (Fastify + Drizzle + better-sqlite3 + viem), frontend/ (Next.js 15 App Router, React 19, Tailwind v4), chainlink/ (the CRE workflow), and shared/ — two files, types.ts and strategy-contract.ts, that every other layer builds against so the DTOs and the strategy interface can never drift between frontend, backend and enclave. CHAINLINK CRE CONFIDENTIAL WORKFLOWS — the compute layer The workflow is TypeScript against @chainlink/cre-sdk 1.18.0, compiled through bun to WASM and driven by the CRE CLI v1.32.0. The cron callback takes a TeeRuntime rather than a Runtime, and that type is what moves execution off the node and into the enclave. Inside it, runtime.getSecret({ id, namespace }) pulls a Vault DON secret that is decrypted at the moment the call runs and only inside an attested enclave, and cre.capabilities.HTTPClient().sendRequest() has a TeeRuntime overload, so the outbound price fetch keeps its request and response payloads confidential from node operators. (ConfidentialHTTPClient is the wrong tool here — it has no TeeRuntime overload and is not meant to be called from a TEE handler; that cost us an afternoon.) Simulation prints "Trigger requested TEE Execution ... AWS Nitro in us-west-2", and that is the only attestation claim this project makes anywhere — no SGX, no MRENCLAVE, no zero-knowledge proofs, in code, copy or docs. Two CRE gotchas worth passing on. @chainlink/cre-sdk declares engines: { bun: ">=1.2.21" }, and on an older bun the workflow still compiles, then dies at engine start with "wasm trap: wasm unreachable instruction executed" rather than a version complaint — the toolchain goes TypeScript -> JavaScript -> WASM through bun, so an old bun silently emits a broken binary. And the CLI does not read .env on its own: without -e .env, simulation fails with a missing-secret error and silently falls back to a default private key. CONTRACTS — Foundry, with unit and invariant tests StrategyVault holds one strategy's capital with ERC-4626 share math, plus applyPnl(int256) and recordTrade(), both restricted to the strategy's operator account, and a reserve prefunded at publish time so a gain is actually payable. The invariant is usdc.balanceOf(vault) >= totalManagedAssets. Four revert cases shaped the whole scheduler, and none of them are visible in the ABI: a gain with no depositors (NoSharesOutstanding — otherwise the next depositor mints 1:1 against zero supply and redeems the stranded amount), a gain larger than the reserve (ReserveExhausted — the vault refuses to promise value it cannot pay), a loss larger than the book (LossExceedsManagedAssets), and settling from the wrong account (NotOperator). So a tick caps its delta at the reserve, caps a loss at totalManagedAssets, and skips settlement rather than losing the tick — the strategy still decides, still records trades and still snapshots NAV, so an unfunded strategy is visibly running rather than silently absent. navPerShare() returns par when totalShares == 0 instead of dividing by zero, so the UI reads NAV as a claim per share and totalShares as the evidence of capital. StrategyRegistry anchors keccak256(strategyId) -> vault + binary hash on-chain; the platform id is a uuid, and the hashing lives in exactly one file with the chain layer taking the bytes32 pre-hashed, so nothing can ever anchor a strategy under keccak(keccak(id)) — a key nothing looks up, with nothing anywhere to report it. THE SUBMISSION PIPELINE — nine stages, in order, first failure stops the rest parses -> required-exports (all six) -> forbidden-imports (and no dynamic import()) -> no-side-effects (no forbidden globals, no top-level statements beyond imports, declarations and exports) -> tsc --noEmit against the shared strategy contract -> describe-valid (metadata and declared assets) -> deterministic (onTick twice on an identical context must return identical output) -> cre workflow build, capturing the binary hash -> cre workflow simulate, yielding a parseable decision. Only after stage nine can a submission publish, which deploys its vault, funds the reserve and registers the measurement. Results stream into the create screen as each stage lands. THE SCHEDULER — where returns actually come from cre workflow simulate fires a cron trigger once and exits; the CLI has no scheduled mode and rejects --listen for cron, so the scheduler owns the interval (60s default, bounded concurrency). Each tick refreshes a seeded, deterministic price oracle, runs the workflow for that strategy, parses the decision, prices the target weights against the price delta since the last tick, charges venue cost in bps on turnover, sends applyPnl and recordTrade from the strategy's operator account, and writes an execution row. The composition root builds one ChainPort and one OraclePort and is called after listen() — because the enclave fetches prices over HTTP from this same process, so the oracle route has to be answering before the first tick can complete. The whole loop (publish, deposit, four ticks, metrics) is proved end to end by a single npm run verify:loop. METRICS — read from the chain, not from our own table Performance is quoted from the vault's own settlement log rather than from a table the backend wrote: lib/chain-metrics.ts reconstructs the NAV series from PnlApplied, Deposited and Withdrawn events and the strategies routes serve that, falling back to the stored snapshots only if a chain read fails. One getLogs covers a whole listing — 63ms cold, 5ms warm. It matters for the product's claim, not just for correctness: the figure on the card is the chain's account of what happened, which is exactly the thing a sceptical investor can independently recompute. lib/metrics.ts is pure — same series in, same value out, no clock, no database — and returns null rather than zero when history is too short, because zero reads as "flat" when the truth is "unknown". APY is the case that bites: annualising wall-clock over a twenty-minute demo turns a +1.4% move into an astronomic exponent and saturates every loss at exactly -100%. So the annualisation window is passed in through the MetricsOptions parameter the module already exposes rather than by editing the module, APY comes back null below it, and StrategyCard falls back to since-inception total return (headline = apy ?? totalReturn, label flipping to SINCE INCEPTION). A real figure at any span beats a saturated one. AUTH AND MONEY — the backend never holds a user key POST /auth/challenge issues a nonce inside a multi-line SIWE-style message whose domain line is derived from CORS_ORIGIN; the browser signs that string verbatim with viem and POST /auth/verify recovers the signer with recoverMessageAddress and compares. Nonces are single-use with a 5-minute TTL. Investing has the same shape: the browser sends approve + deposit itself, waits for receipts, then posts the tx hash, and the backend fetches the receipt, parses the Deposited event and verifies it came from the caller's address and targets this strategy's vault before recording a position. GET /api/chain/config is authoritative for chain id, RPC URL and every contract address — addresses must never live in env, because anvil redeploys move them and a stale .env pointing at a dead address fails in a way that looks exactly like a bug in the contract. CREATOR SECRETS — the part that took the most research The production path is TDH2 threshold encryption performed in the creator's browser against the Vault DON's P256 key, fetched from vault.publicKey.get on the CRE gateway, using Chainlink's own pure-JS TDH2 (src/tdh2.js in @chainlink/functions-toolkit) with the envelope matching the Go implementation field for field. Two things make it non-trivial: the toolkit's exported encrypt() hardcodes an empty label, while CRE requires the label to be the owner address left-padded to 32 bytes, so tdh2.js has to be vendored and the internal tdh2Encrypt called directly; and the gateway returns no Access-Control-Allow-Origin header and fails the OPTIONS preflight, so the browser cannot post to it and the backend has to relay. Locally neither the Vault DON nor the gateway is reachable from a machine with no CRE deployment, so EncryptedSecret.scheme is the seam: it carries "local-dev" (AES-256-GCM, key generated and kept in the creator's browser, HKDF-derived per submission) instead of "tdh2-p256-aesgcm", and the create screen states the limitation in plain words rather than glossing it — under local-dev the platform's ignorance of the plaintext is real, but no enclave can read it either. Everything either side of the cipher is already the production path: encryption happens in the browser, the backend stores ciphertext in a table with no plaintext column, and the endpoint accepts nothing else. Swapping schemes replaces one function. FRONTEND Hand-written React against Stitch-generated reference screens, in a dark "cryptographic quantitative terminal" system — Inter for text, JetBrains Mono with tnum and zero feature settings for every number, hash and ticker, 4px radii, 1px hairlines, no shadows outside modals. CodeMirror 6 for the strategy editor, Recharts for the NAV and position series, viem for all signing and broadcasting. TWO PARTNER TECHNOLOGIES, AND WHAT IS ACTUALLY WIRED Chainlink CRE is fully wired: the confidential workflow builds, simulates in the enclave and returns the decision the scheduler prices, and its binary hash is what lands in the registry. Privy is the investor surface. The signer sits behind one swappable interface in lib/wallet.ts — SignerKind is a union and BrowserWalletClient is typed over a generic Transport precisely so a Privy embedded wallet drops in where the local key is today without the challenge/verify exchange moving at all; Privy hands you a provider, you wrap it with custom(provider), and signMessage still produces a personal_sign signature that recoverMessageAddress verifies. Along the way we established by probing that Privy's JWKS keys are EC P-256 / ES256 — some Privy docs describe the verification key as Ed25519, and a verifier built on that does not work — and that the JWKS endpoint doubles as an app-id validator, 200 for a good id and 400 missing_or_invalid_privy_app_id for a bad one, which is the fastest way to catch a copy-paste artifact that otherwise presents as a mysteriously broken integration. Settlement is USDC vaults on a local chain, and a strategy's operator — the only account the vault will accept applyPnl and recordTrade from — is a local EOA. Circle Agent Wallets on Arc are the intended production form for that operator and the design is written against them (one policy-capped wallet per strategy, control credential held as a Vault secret so only the attested workflow could direct it), but that integration is not built here and nothing in the demo depends on it. We are saying so rather than implying otherwise, because the entire claim of this product is that you can check what is true; a project about verifiability does not get to be vague about which of its own parts are verified. TWO OPERATIONAL THINGS WORTH KNOWING Only one process may tick a given vault. Run several backends against the same anvil chain and each settles the same vaults on its own timer, so a block ends up carrying more than one PnlApplied and no single database sees the whole series — the stored snapshots then disagree with the chain for reasons that look exactly like a write bug. Related: every backend signs with the same anvil account unless a distinct deployer key is set, so concurrent processes race the same nonce and drop settlements. ONE LAST HACKY THING A global gitignore excluding [Ss]cripts and [Ll]ib silently hid scripts/ and src/lib/ from git status — the project .gitignore negates both, and git status -uall is now the rule before committing a new directory. An entire evening went to code that existed locally and simply was not in the repo.