Search projects, hackathons, sponsors and tech

    ← All projects

    Enclave Portfolio

    Private strategy, public trades: portfolio rebalancing and liquidation protection in a Chainlink TEE

    Demo

    About

    Enclave Portfolio is two Chainlink CRE Confidential Workflows built on one idea: an automated trading strategy has to live somewhere, and wherever it lives, someone can read it. In a bot, the host can. In a smart contract, everyone can. In an oracle node's config, every operator can. Once a strategy is readable, every future move is predictable — and predictable moves get front-run. Confidential Workflows put the strategy inside an AWS Nitro enclave: hardware-isolated memory the node operator physically cannot inspect, with secrets released only to an attested binary. Delete the enclave from either workflow and there is no decision left to make. Workflow 1 — Confidential Rebalancer (Best Confidential Workflow). Most "private rebalancers" hide a set of target weights. That doesn't work: a rebalance moves the book toward the target, so anyone reading balances before and after recovers it in one observation. We fixed this at the design level. The user's entire portfolio construction is one secret spec — which assets, the strategic base allocation, which market signals tilt it (risk parity, momentum, equal-weight, composable by strength), per-asset weight bounds, and the rebalancing policy. Each cycle the enclave composes target weights from that spec and live prices, sizes trades under a per-trade cap, checks price impact against a secret ceiling, and emits only the orders. The targets are never stored and never the same twice, so the trades don't converge on anything an observer can solve for. Two users on the same deployed workflow can run entirely different portfolios; the public code reveals only that "a spec is evaluated." Workflow 2 — Automated Liquidation Protection (the Challenge). Protects a vETH-collateral / vUSD-debt position on the challenge's ChallengeLending contract. Two facts from the contract shape the strategy: deposits don't reduce debt (so they cost nothing in the loan-continuity score), and liquidation only fires when the admin separately calls checkAllHF(). So: on ChallengeStarted, deposit exactly enough collateral to survive down to a secret floor price — one transaction, before any price moves, no race. On every PriceUpdate, recompute the health factor with the contract's exact integer math; if it falls under a secret trigger, top up to a secret target, then repay within a secret cap, then repay whatever it takes. The transaction is signed inside the enclave with a Vault-held key. Logs carry only held or acted and a tx hash — never a threshold, an amount, or a price. Engineering. No float ever touches money: token amounts are bigint base units, USD is fixed-point 1e8, health factors are computed bit-for-bit as the contract does. Every parser refuses rather than guesses. 102 tests, including a bit-exact offline model of ChallengeLending that replays all five published scenarios and harsher ones. A private copy of the challenge contracts on Sepolia, with our wallet as admin, serves as a live test harness — we've watched the enclave sign a deposit() between a price drop to $1,500 and the liquidation check, and survive.

    How it's made

    Stack. TypeScript on the Chainlink CRE SDK (@chainlink/cre-sdk 1.18), compiled to WASM by the CRE CLI and run in a Javy/QuickJS sandbox. viem for ABI encoding and for signing transactions with @noble/curves — pure JS, so it works inside the enclave. Bun for tests and tooling. Foundry for the Solidity side. No framework, no floats. How the enclave is wired. Every handler is registered with handlerInTee(trigger, fn, [{ tee: "nitro", regions: ["us-west-2"] }]) — the third argument is the TEE constraint, which is what pins execution to an enclave (Chainlink's own template has it commented out in the wrong argument slot). The callback receives TeeRuntime, a different type from Runtime: chain reads and writes won't accept it, so every crossing of the enclave boundary is a deliberate line of code. Secrets come from runtime.getSecrets(), released by the Vault DON only to the attested binary. Data enters over confidential HTTP (JSON-RPC eth_call to Sepolia with TLS terminating inside); the decision runs; a transaction is signed inside and leaves via eth_sendRawTransaction. The only other exits are return and runtime.log, and the logs carry one word plus a tx hash. Rebalancer. The strategy is a single JSON secret — a PortfolioSpec — parsed by a refuse-don't-guess validator (weights must sum to exactly 10,000 bps; bounds must be satisfiable; unknown signal types rejected with the list of known ones). Signals are pluggable blocks (signals/), composed as a convex combination with the base view, then water-filled into per-asset bounds and forced to an exact sum by largest-remainder assignment with ties broken on token id so two nodes agree bit for bit. Momentum, inverse-volatility and equal-weight ship; adding one is write-and-register. Liquidation protection. The policy is pure integer math (policy.ts) with the contract's calcHF reproduced exactly, including its floor division. The test file is a bit-exact model of ChallengeLending — partial liquidation, the 5% penalty, time-weighted debt — so scenarios replay offline in milliseconds. The live harness is a private copy of the three challenge contracts deployed to Sepolia with our wallet as admin, plus a runner that plays Chainlink: updatevETHPrice → wait → checkAllHF. What Chainlink gave us. CRE turned "put the strategy in a TEE" from an infrastructure project into a type signature. Attestation-gated secrets mean a modified binary can't get the policy; the log trigger on PriceUpdate fires inside the window between the price update and the liquidation check. Hacky bits worth admitting. - All money math is bigint. Volatility needs a square root, so there's a Newton's-method integer sqrt; log returns need a logarithm, so we use simple returns and say so. - The first version stored fixed target weights. Then we noticed one uncapped rebalance lands the book exactly on target — anyone reading balances before and after recovers the whole strategy. The fix became the design: derive targets from a signal so they move for reasons an observer can't see. - The price-impact check runs inside the enclave on purpose; emitting the cap with each trade would publish it. - runtime.log is public. Validation errors were stripped of values so a bad secret fails as policy.floor_price is invalid, never with the number. - The private key in .env lacked a 0x; cast didn't care, viem did. There is now a normaliser that never echoes the value. - The scenario runner parses cast's tuple output with a regex that first strips its [7e5] annotations. It works.