feat: Wraith MVP — private conditional orders on Flare - #1
Conversation
Sets up the project skeleton and release automation before any feature work lands. - Dependabot across github-actions, npm (root/frontend/keeper) and gomod (extension), with Next/React and viem/wagmi grouped so related bumps land together rather than as build-breaking partial upgrades. - semantic-release on main: version, changelog and GitHub release are derived from Conventional Commit messages. - CI runs forge build/test for contracts and go vet/test -race for the extension. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Escrows an asset alongside an ECIES ciphertext of the trigger condition, so the condition is never readable on-chain. A keeper pokes the order with tick(); the TEE extension decrypts and evaluates in-enclave and returns a signed result only when the condition fires. execute() verifies that signature and settles via swap or FXRP redemption. Signature verification follows the FCC ActionResult scheme: rebuild ActionResult.Hash(), wrap it in the chain-scoped TEE_ACTION_RESULT payload, and ecrecover under the EIP-191 prefix. Two hardenings over the reference weather-insurance example: - actionId is consumed once, so a signed result cannot be replayed to execute the same order twice. - TEE signers are an allowlist rather than a single owner-set address. ITeeMachineRegistry exposes only getRandomTeeIds, a random selector rather than a membership query, so there is no published way to ask whether an address is a registered TEE. The allowlist is the honest substitute and is documented as such. 12 Foundry tests cover escrow, valid settlement, forged signatures, replay, cross-deployment result reuse, expiry, cancellation and tick rate limiting. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The enclave-side decision logic: given decrypted order terms and an FTSO reading, decide whether the private condition has fired. internal/trigger is pure and dependency-free. The decision that moves someone's money is the part most worth testing, and keeping it free of the TEE harness means it can be tested without a TEE, a chain, or a network. Behavioural choices that are easy to get wrong, so each has a test: - Threshold boundaries are inclusive. A stop set at exactly the traded price fires, which is what "stop at X" means to a trader. - Feed decimals are normalized to 1e18, so the same threshold works against any feed and the decision cannot depend on a feed's precision. - Stale prices refuse to fire rather than firing on old data. Missing a tick costs nothing; the keeper ticks again seconds later. An irreversible trade on a stale price cannot be undone. - Out-of-range feed decimals are rejected before exponentiation, so a corrupt int8 cannot induce a huge big.Int allocation. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Pokes live orders so the TEE re-evaluates their conditions, then relays TEE-signed results to execute(). The keeper is untrusted by construction. It forwards ciphertext it cannot read, and the TEE reads FTSO itself rather than trusting a price supplied here, so the worst a hostile keeper can do is withhold ticks — which is why ticking is open to anyone and any keeper can cover for another. Instruction ids come from decoding OrderTicked with viem's parseEventLogs rather than matching a hand-computed topic hash. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Encrypts an order's terms in the browser and stores only the ciphertext on-chain. The enclave public key is fetched through a server-side /api/info route rather than from the browser, because the extension proxy sets no CORS headers and its tunnel URL should not ship in a client bundle. Design is a darkroom safelight: violet-cast ink, one amber signal colour, and mono type reserved exclusively for cipher bytes so the unreadable material reads as the most privileged thing on the page. The seal — an order's hex block under a slow safelight sweep — is the signature element, and the layout's argument is the contrast around it: everything about an order is legible except the one thing that matters. Form fields whose values get encrypted carry an amber border so what stays secret is visible at a glance. Not visually verified — no browser was available in this environment. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (4)
📒 Files selected for processing (36)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
🎉 This PR is included in version 1.0.0 🎉 The release is available on GitHub release Your semantic-release bot 📦🚀 |
There was a problem hiding this comment.
Actionable comments posted: 17
🧹 Nitpick comments (6)
contracts/src/WraithOrders.sol (4)
390-394: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winReset the router allowance after the swap.
_swapsets the allowance to_o.amountInand never clears it. Two problems follow. First, if the router pulls less than_o.amountIn, a residual allowance stays on an arbitrary user-suppliedtokenIn. Second, tokens that require the allowance to be zero before a new non-zero approval (USDT-style) will revert on the next swap of the same token.Set the allowance to zero after the call.
♻️ Proposed allowance handling
+ require(IERC20(_o.tokenIn).approve(address(router), 0), "approve reset failed"); require(IERC20(_o.tokenIn).approve(address(router), _o.amountIn), "approve failed"); uint256[] memory amounts = router.swapExactTokensForTokens(_o.amountIn, _minOut, path, _o.owner, block.timestamp); + require(IERC20(_o.tokenIn).approve(address(router), 0), "approve clear failed"); return amounts[amounts.length - 1];🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@contracts/src/WraithOrders.sol` around lines 390 - 394, Update the _swap function to reset IERC20(_o.tokenIn)’s router allowance to zero immediately after router.swapExactTokensForTokens completes and before returning the output amount. Preserve the existing approval and swap flow while ensuring the reset also occurs when the swap succeeds with residual allowance.
191-191: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winHandle ERC-20 implementations that return no data.
IERC20.transfer,transferFrom, andapproveare declared withboolreturns. Tokens that omit the return value (USDT on several chains) make the ABI decoder revert, even though the transfer succeeded. This affectscreateOrder(Line 191),cancel(Line 343),_swap(Line 390), and_redeem(Line 413).Use a low-level call wrapper that accepts empty return data, or depend on OpenZeppelin
SafeERC20.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@contracts/src/WraithOrders.sol` at line 191, Update ERC-20 interactions in createOrder, cancel, _swap, and _redeem to support tokens that return no data by using SafeERC20 or a shared low-level wrapper that treats empty returndata as success while rejecting explicit false results. Replace the direct transferFrom, transfer, and approve calls consistently across these methods.
120-133: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick winAdd a way to rotate the owner.
owneris set in the constructor and no function changes it. The owner controlssetTeeAddress,setRouter, andsetAssetManager. If the deployer key is lost or compromised, the TEE allowlist can never be rotated and the deployment must be replaced. Add a two-step ownership transfer, or document the immutability as intentional indocs/TRUST.md.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@contracts/src/WraithOrders.sol` around lines 120 - 133, Implement two-step ownership transfer for WraithOrders: add a pending-owner state, an onlyOwner initiation method, and a pending-owner acceptance method that updates owner only after confirmation. Preserve the existing onlyOwner protection for setTeeAddress, setRouter, and setAssetManager, and emit appropriate ownership-transfer events if consistent with the contract’s conventions.
432-449: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low valueReject high-
ssignatures and non-canonicalv.
_recoveraccepts bothsvalues of a malleable pair and normalizesvfrom0/1. Replay protection here keys on_actionId, so malleability does not currently enable a second settlement. The guard is still worth adding, because it keeps the recovery canonical if a future change ever keys deduplication on the signature bytes.♻️ Proposed canonical-signature check
if (v < 27) { v += 27; } require(v == 27 || v == 28, "bad signature v"); + require( + uint256(s) <= 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0, + "bad signature s" + ); address signer = ecrecover(_digest, v, r, s);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@contracts/src/WraithOrders.sol` around lines 432 - 449, Update _recover to require canonical signature encoding: accept only v values 27 or 28 without normalizing 0/1, and reject high-s signatures by enforcing s is at or below the secp256k1 lower-half-order constant before calling ecrecover. Preserve the existing length, signer validity, and recovery behavior.contracts/test/WraithOrders.t.sol (1)
149-157: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winBind the test signing helper to the contract constant.
_signhard-codesbytes32("TEE_ACTION_RESULT").TEE_ACTION_RESULT_PREFIXisprivateinWraithOrders, so the test cannot read it. If the prefix changes in the contract, these tests keep passing against the stale value and the domain-separation regression goes undetected.Expose the prefix as a
public constant, or add a single assertion that the contract-side prefix matches the test literal.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@contracts/test/WraithOrders.t.sol` around lines 149 - 157, Update the contract’s TEE action-result prefix definition to be publicly accessible as the existing constant TEE_ACTION_RESULT_PREFIX, then change the test helper _sign to use that contract constant instead of hard-coding bytes32("TEE_ACTION_RESULT"). Preserve the current signing and hashing flow.frontend/app/page.tsx (1)
55-74: 🚀 Performance & Scalability | 🔵 Trivial | 🏗️ Heavy liftLoad a bounded order page instead of the full history.
loadOrdersperforms one RPC request per order, in sequence. Every new order increases initial page-load latency for every user. Request a bounded recent page and batch reads where the RPC supports it.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@frontend/app/page.tsx` around lines 55 - 74, Update loadOrders to fetch only a bounded page of the most recent orders rather than iterating from zero through the full orderCount. Calculate the recent order ID range using a fixed page-size limit, preserve newest-first ordering, and use the client’s supported batched-read mechanism for getOrder calls instead of issuing sequential RPC requests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ci.yml:
- Around line 15-17: Update both actions/checkout@v4 steps in the workflow to
set persist-credentials to false alongside the existing checkout options,
ensuring neither checkout retains the GitHub token in local Git configuration.
- Around line 11-39: Add a dedicated frontend CI job alongside contracts and
extension that checks out the repository, installs dependencies with npm ci, and
runs npm run build with frontend as the working directory.
In `@contracts/src/WraithOrders.sol`:
- Around line 400-417: Update _redeem to enforce that the AssetManager’s token
spend does not exceed _o.amountIn after calculating spent; revert when spent is
greater than the order’s escrow before handling any refund, ensuring one order
cannot consume pooled funds belonging to other orders.
- Around line 186-205: Update the order creation flow around the IERC20
transferFrom call to measure the contract’s token balance before and after
transfer, then store the observed balance delta as the order’s amountIn instead
of _amountIn. Use this received amount for the OrderCreated event and ensure
subsequent swap and cancel paths reference the stored value.
In `@contracts/test/WraithOrders.t.sol`:
- Around line 97-134: Add and initialize a MockAssetManager in WraithOrdersTest,
configure it through setAssetManager, and add redeem-settlement tests that
exercise ACTION_REDEEM. Configure the mock to burn a variable tokenIn amount,
then assert _redeem refunds the unconsumed escrow remainder and rejects a lot
count whose required amount exceeds the order escrow.
In `@extension/internal/trigger/trigger.go`:
- Around line 149-163: Update the comparison in the trigger evaluation flow
around NormalizeE18 so prices retain all raw precision when obs.Decimals exceeds
18; compare obs.Value against t.ThresholdE18 using exact decimal scaling rather
than the truncated priceE18 value, while preserving inclusive Below and Above
boundaries. Add a regression test in the trigger tests covering a fractional
price such as 2.00000000000000000001 against a 2e18 threshold.
- Around line 133-135: Update the observation validation in the trigger
evaluation flow around obs and ErrNilObservation to also reject values that are
zero or negative. Return the existing invalid-observation error path before
evaluating thresholds, ensuring non-positive FTSO values produce a failed action
rather than authorizing settlement.
In `@frontend/app/api/info/route.ts`:
- Around line 27-30: Update the catch handler in the API route to stop including
EXT_PROXY_URL in the client-facing NextResponse.json error; return a generic
proxy-unavailable message instead, and log the endpoint server-side within the
same failure path.
- Line 12: Update the fetch call in the info route to include an AbortSignal
timeout with a short duration, ensuring unresponsive extension-proxy requests
are aborted while preserving the existing no-store cache behavior.
In `@frontend/app/page.tsx`:
- Around line 190-215: The trigger controls in the form need accessible names:
update the direction select, threshold input, action select, and conditional
minOut input around the Trigger and Then fields to use associated labels or
precise aria-label values. Ensure each control is programmatically labeled while
preserving the existing visible text and action-dependent labeling.
- Around line 113-118: Update the decimals read in the swap flow around amountIn
and minOutOrLots to use TOKEN_OUT when action === "swap", while retaining
FXRP_ADDRESS for other actions. Ensure the minimum output is parsed with
TOKEN_OUT decimals so the encrypted minOutOrLots value uses the correct units.
In `@frontend/lib/wraith.ts`:
- Around line 85-88: Update priceToE18 to validate the entire trimmed input
before encoding: allow only a valid decimal form with at most one decimal
separator and no more than 18 fractional digits, rejecting malformed values such
as “1.2.3” and over-precision inputs. Preserve the existing bigint conversion
for valid values.
- Around line 71-74: Update sealTerms to use browser-safe Uint8Array conversions
for the public key and encoded ciphertext, and serialize the encrypt result
without relying on the global Buffer (or explicitly use an existing buffer
polyfill). In priceToE18, validate the input contains at most one decimal
separator and reject malformed values such as “1.2.3” or “1..2” before BigInt
conversion.
In `@keeper/src/index.js`:
- Around line 59-70: Update fetchResult to abort the extension proxy fetch after
the configured timeout, using an AbortController or the existing timeout
mechanism. Ensure timeout failures are handled as unavailable results so
relayResults continues processing the next pending instruction and the main loop
keeps ticking.
- Around line 41-42: Implement startup recovery for completed instructions in
keeper/src/index.js around the pending map: persist pending instruction IDs or
scan the chain for unprocessed OrderTicked events, then poll their TEE results
after restart before relying on later ticks. Update keeper/README.md line 40 to
describe the recovered behavior and clarify that re-ticking alone does not
preserve a prior firing decision.
- Around line 118-140: Move pending.delete(instructionId) out of the initial
result-processing path and perform it only after walletClient.writeContract and
publicClient.waitForTransactionReceipt complete with receipt.status ===
"success". Retain separate handling for TEE failures and no-op results, and
leave the pending entry intact when execution throws or the receipt indicates
failure.
In `@README.md`:
- Around line 23-35: Label the fenced architecture diagram in the README with
the text language by changing its opening fence to ```text, while leaving the
diagram content unchanged.
---
Nitpick comments:
In `@contracts/src/WraithOrders.sol`:
- Around line 390-394: Update the _swap function to reset IERC20(_o.tokenIn)’s
router allowance to zero immediately after router.swapExactTokensForTokens
completes and before returning the output amount. Preserve the existing approval
and swap flow while ensuring the reset also occurs when the swap succeeds with
residual allowance.
- Line 191: Update ERC-20 interactions in createOrder, cancel, _swap, and
_redeem to support tokens that return no data by using SafeERC20 or a shared
low-level wrapper that treats empty returndata as success while rejecting
explicit false results. Replace the direct transferFrom, transfer, and approve
calls consistently across these methods.
- Around line 120-133: Implement two-step ownership transfer for WraithOrders:
add a pending-owner state, an onlyOwner initiation method, and a pending-owner
acceptance method that updates owner only after confirmation. Preserve the
existing onlyOwner protection for setTeeAddress, setRouter, and setAssetManager,
and emit appropriate ownership-transfer events if consistent with the contract’s
conventions.
- Around line 432-449: Update _recover to require canonical signature encoding:
accept only v values 27 or 28 without normalizing 0/1, and reject high-s
signatures by enforcing s is at or below the secp256k1 lower-half-order constant
before calling ecrecover. Preserve the existing length, signer validity, and
recovery behavior.
In `@contracts/test/WraithOrders.t.sol`:
- Around line 149-157: Update the contract’s TEE action-result prefix definition
to be publicly accessible as the existing constant TEE_ACTION_RESULT_PREFIX,
then change the test helper _sign to use that contract constant instead of
hard-coding bytes32("TEE_ACTION_RESULT"). Preserve the current signing and
hashing flow.
In `@frontend/app/page.tsx`:
- Around line 55-74: Update loadOrders to fetch only a bounded page of the most
recent orders rather than iterating from zero through the full orderCount.
Calculate the recent order ID range using a fixed page-size limit, preserve
newest-first ordering, and use the client’s supported batched-read mechanism for
getOrder calls instead of issuing sequential RPC requests.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 98ced77d-0749-4ef7-8b14-c034b51a77ca
⛔ Files ignored due to path filters (4)
contracts/foundry.lockis excluded by!**/*.lockfrontend/package-lock.jsonis excluded by!**/package-lock.jsonkeeper/package-lock.jsonis excluded by!**/package-lock.jsonpackage-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (36)
.github/dependabot.yml.github/workflows/ci.yml.github/workflows/release.yml.gitignore.gitmodules.releaserc.jsonREADME.mdcontracts/foundry.tomlcontracts/lib/forge-stdcontracts/src/WraithOrders.solcontracts/src/interfaces/ITeeExtensionRegistry.solcontracts/src/interfaces/ITeeMachineRegistry.solcontracts/src/interfaces/IWraithExternal.solcontracts/test/WraithOrders.t.soldocs/TRUST.mdextension/README.mdextension/go.modextension/internal/config/config.goextension/internal/trigger/trigger.goextension/internal/trigger/trigger_test.gofrontend/.env.examplefrontend/README.mdfrontend/app/api/info/route.tsfrontend/app/globals.cssfrontend/app/layout.tsxfrontend/app/page.tsxfrontend/lib/wraith.tsfrontend/next-env.d.tsfrontend/next.config.mjsfrontend/package.jsonfrontend/tsconfig.jsonfrontend/tsconfig.tsbuildinfokeeper/README.mdkeeper/package.jsonkeeper/src/index.jspackage.json
| jobs: | ||
| contracts: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| with: | ||
| submodules: recursive | ||
| - uses: foundry-rs/foundry-toolchain@v1 | ||
| - name: Build | ||
| run: forge build | ||
| working-directory: contracts | ||
| - name: Test | ||
| run: forge test -vvv | ||
| working-directory: contracts | ||
|
|
||
| extension: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - uses: actions/checkout@v4 | ||
| - uses: actions/setup-go@v5 | ||
| with: | ||
| go-version-file: extension/go.mod | ||
| cache-dependency-path: extension/go.sum | ||
| - name: Vet | ||
| run: go vet ./... | ||
| working-directory: extension | ||
| - name: Test | ||
| run: go test ./... -race | ||
| working-directory: extension |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Inspect the frontend commands that CI must invoke.
test -f frontend/package.json
rg -n -C 3 '"(build|typecheck|lint)"' frontend/package.json
# Confirm that no existing CI step validates the frontend.
rg -n -C 3 'frontend|npm ci|npm run (build|typecheck)' .github/workflows/ci.ymlRepository: LSUDOKO/Wraith
Length of output: 367
🏁 Script executed:
#!/bin/bash
set -euo pipefail
printf '%s\n' '--- frontend files ---'
git ls-files frontend | sed -n '1,80p'
printf '%s\n' '--- frontend package metadata ---'
cat frontend/package.json
printf '%s\n' '--- lockfiles ---'
find frontend -maxdepth 1 -type f \( -name 'package-lock.json' -o -name 'npm-shrinkwrap.json' -o -name 'yarn.lock' -o -name 'pnpm-lock.yaml' \) -print
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/ci.yml
printf '%s\n' '--- frontend references in workflow ---'
if rg -n -C 3 'frontend|npm|yarn|pnpm|bun' .github/workflows/ci.yml; then
:
else
printf '%s\n' 'No frontend or package-manager commands found in .github/workflows/ci.yml'
fiRepository: LSUDOKO/Wraith
Length of output: 2567
Add frontend validation to CI.
CI does not install dependencies or build frontend/. Add a job that runs npm ci and npm run build in frontend/.
🧰 Tools
🪛 zizmor (1.29.0)
[warning] 15-17: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
[warning] 29-29: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci.yml around lines 11 - 39, Add a dedicated frontend CI
job alongside contracts and extension that checks out the repository, installs
dependencies with npm ci, and runs npm run build with frontend as the working
directory.
| - uses: actions/checkout@v4 | ||
| with: | ||
| submodules: recursive |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Disable persisted checkout credentials.
Both checkout steps leave the read-scoped GitHub token in local Git configuration for later steps. Set persist-credentials: false on both actions.
Proposed fix
- uses: actions/checkout@v4
with:
submodules: recursive
+ persist-credentials: false
@@
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v4
+ with:
+ persist-credentials: falseAlso applies to: 29-29
🧰 Tools
🪛 zizmor (1.29.0)
[warning] 15-17: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false
(artipacked)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In @.github/workflows/ci.yml around lines 15 - 17, Update both
actions/checkout@v4 steps in the workflow to set persist-credentials to false
alongside the existing checkout options, ensuring neither checkout retains the
GitHub token in local Git configuration.
Source: Linters/SAST tools
| require(_encrypted.length > 0, "empty ciphertext"); | ||
| require(_tokenIn != address(0), "zero token"); | ||
| require(_amountIn > 0, "zero amount"); | ||
| require(_expiry > block.timestamp, "expiry in the past"); | ||
|
|
||
| require(IERC20(_tokenIn).transferFrom(msg.sender, address(this), _amountIn), "escrow transfer failed"); | ||
|
|
||
| orderId = _orders.length; | ||
| _orders.push( | ||
| Order({ | ||
| owner: msg.sender, | ||
| tokenIn: _tokenIn, | ||
| amountIn: _amountIn, | ||
| expiry: _expiry, | ||
| nextTickAt: 0, | ||
| executed: false, | ||
| cancelled: false, | ||
| encrypted: _encrypted | ||
| }) | ||
| ); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Record the amount actually received, not the requested amount.
_tokenIn is arbitrary caller input. For a fee-on-transfer or rebasing token, the contract receives less than _amountIn, but the order stores _amountIn. The swap then approves and requests more than the escrow holds, and cancel tries to refund more than was received. Both paths revert, and the order owner cannot recover the escrow.
Measure the balance delta, or restrict _tokenIn to an owner-curated allowlist.
🛡️ Proposed fix using the observed balance delta
- require(IERC20(_tokenIn).transferFrom(msg.sender, address(this), _amountIn), "escrow transfer failed");
+ uint256 balanceBefore = IERC20(_tokenIn).balanceOf(address(this));
+ require(IERC20(_tokenIn).transferFrom(msg.sender, address(this), _amountIn), "escrow transfer failed");
+ uint256 received = IERC20(_tokenIn).balanceOf(address(this)) - balanceBefore;
+ require(received > 0, "no tokens received");
orderId = _orders.length;
_orders.push(
Order({
owner: msg.sender,
tokenIn: _tokenIn,
- amountIn: _amountIn,
+ amountIn: received,Update the OrderCreated emit to use received as well.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| require(_encrypted.length > 0, "empty ciphertext"); | |
| require(_tokenIn != address(0), "zero token"); | |
| require(_amountIn > 0, "zero amount"); | |
| require(_expiry > block.timestamp, "expiry in the past"); | |
| require(IERC20(_tokenIn).transferFrom(msg.sender, address(this), _amountIn), "escrow transfer failed"); | |
| orderId = _orders.length; | |
| _orders.push( | |
| Order({ | |
| owner: msg.sender, | |
| tokenIn: _tokenIn, | |
| amountIn: _amountIn, | |
| expiry: _expiry, | |
| nextTickAt: 0, | |
| executed: false, | |
| cancelled: false, | |
| encrypted: _encrypted | |
| }) | |
| ); | |
| require(_encrypted.length > 0, "empty ciphertext"); | |
| require(_tokenIn != address(0), "zero token"); | |
| require(_amountIn > 0, "zero amount"); | |
| require(_expiry > block.timestamp, "expiry in the past"); | |
| uint256 balanceBefore = IERC20(_tokenIn).balanceOf(address(this)); | |
| require(IERC20(_tokenIn).transferFrom(msg.sender, address(this), _amountIn), "escrow transfer failed"); | |
| uint256 received = IERC20(_tokenIn).balanceOf(address(this)) - balanceBefore; | |
| require(received > 0, "no tokens received"); | |
| orderId = _orders.length; | |
| _orders.push( | |
| Order({ | |
| owner: msg.sender, | |
| tokenIn: _tokenIn, | |
| amountIn: received, | |
| expiry: _expiry, | |
| nextTickAt: 0, | |
| executed: false, | |
| cancelled: false, | |
| encrypted: _encrypted | |
| }) | |
| ); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@contracts/src/WraithOrders.sol` around lines 186 - 205, Update the order
creation flow around the IERC20 transferFrom call to measure the contract’s
token balance before and after transfer, then store the observed balance delta
as the order’s amountIn instead of _amountIn. Use this received amount for the
OrderCreated event and ensure subsequent swap and cancel paths reference the
stored value.
| function _redeem(Order storage _o, uint256 _lots, string memory _underlyingAddress) private returns (uint256) { | ||
| require(address(assetManager) != address(0), "asset manager not set"); | ||
| require(_lots > 0, "zero lots"); | ||
| require(bytes(_underlyingAddress).length > 0, "empty underlying address"); | ||
|
|
||
| IERC20 token = IERC20(_o.tokenIn); | ||
| uint256 balanceBefore = token.balanceOf(address(this)); | ||
|
|
||
| uint256 redeemed = assetManager.redeem(_lots, _underlyingAddress, payable(address(0))); | ||
|
|
||
| uint256 balanceAfter = token.balanceOf(address(this)); | ||
| uint256 spent = balanceBefore - balanceAfter; | ||
| if (_o.amountIn > spent) { | ||
| require(token.transfer(_o.owner, _o.amountIn - spent), "remainder refund failed"); | ||
| } | ||
|
|
||
| return redeemed; | ||
| } |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🔴 Critical | 🏗️ Heavy lift
Bound the redemption to the order's own escrow.
_lots comes from the TEE result and is not tied to _o.amountIn. balanceBefore is the contract's total tokenIn balance, which pools the escrow of every open order in that token. If the AssetManager consumes more than _o.amountIn, spent > _o.amountIn, the refund branch is skipped silently, and the excess is taken from other orders' escrow. Those orders then fail to settle or refund.
Add an explicit upper bound on spent so one order can never consume another order's escrow.
🛡️ Proposed fix to cap redemption spend
uint256 balanceAfter = token.balanceOf(address(this));
uint256 spent = balanceBefore - balanceAfter;
+ require(spent <= _o.amountIn, "redeem exceeded escrow");
if (_o.amountIn > spent) {
require(token.transfer(_o.owner, _o.amountIn - spent), "remainder refund failed");
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function _redeem(Order storage _o, uint256 _lots, string memory _underlyingAddress) private returns (uint256) { | |
| require(address(assetManager) != address(0), "asset manager not set"); | |
| require(_lots > 0, "zero lots"); | |
| require(bytes(_underlyingAddress).length > 0, "empty underlying address"); | |
| IERC20 token = IERC20(_o.tokenIn); | |
| uint256 balanceBefore = token.balanceOf(address(this)); | |
| uint256 redeemed = assetManager.redeem(_lots, _underlyingAddress, payable(address(0))); | |
| uint256 balanceAfter = token.balanceOf(address(this)); | |
| uint256 spent = balanceBefore - balanceAfter; | |
| if (_o.amountIn > spent) { | |
| require(token.transfer(_o.owner, _o.amountIn - spent), "remainder refund failed"); | |
| } | |
| return redeemed; | |
| } | |
| function _redeem(Order storage _o, uint256 _lots, string memory _underlyingAddress) private returns (uint256) { | |
| require(address(assetManager) != address(0), "asset manager not set"); | |
| require(_lots > 0, "zero lots"); | |
| require(bytes(_underlyingAddress).length > 0, "empty underlying address"); | |
| IERC20 token = IERC20(_o.tokenIn); | |
| uint256 balanceBefore = token.balanceOf(address(this)); | |
| uint256 redeemed = assetManager.redeem(_lots, _underlyingAddress, payable(address(0))); | |
| uint256 balanceAfter = token.balanceOf(address(this)); | |
| uint256 spent = balanceBefore - balanceAfter; | |
| require(spent <= _o.amountIn, "redeem exceeded escrow"); | |
| if (_o.amountIn > spent) { | |
| require(token.transfer(_o.owner, _o.amountIn - spent), "remainder refund failed"); | |
| } | |
| return redeemed; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@contracts/src/WraithOrders.sol` around lines 400 - 417, Update _redeem to
enforce that the AssetManager’s token spend does not exceed _o.amountIn after
calculating spent; revert when spent is greater than the order’s escrow before
handling any refund, ensuring one order cannot consume pooled funds belonging to
other orders.
| contract WraithOrdersTest is Test { | ||
| WraithOrders internal wraith; | ||
| MockERC20 internal fxrp; | ||
| MockERC20 internal usdt; | ||
| MockExtensionRegistry internal extRegistry; | ||
| MockMachineRegistry internal machineRegistry; | ||
| MockRouter internal router; | ||
|
|
||
| uint256 internal constant TEE_PK = 0xA11CE; | ||
| address internal teeAddr; | ||
|
|
||
| address internal alice = address(0xA1); | ||
|
|
||
| string internal constant TAG = "submit"; | ||
| uint64 internal constant EXPIRY = 1_000_000; | ||
| uint256 internal constant ESCROW = 100 ether; | ||
|
|
||
| function setUp() public { | ||
| vm.warp(1000); | ||
|
|
||
| teeAddr = vm.addr(TEE_PK); | ||
|
|
||
| fxrp = new MockERC20(); | ||
| usdt = new MockERC20(); | ||
| extRegistry = new MockExtensionRegistry(); | ||
| machineRegistry = new MockMachineRegistry(); | ||
| router = new MockRouter(); | ||
|
|
||
| wraith = new WraithOrders(ITeeExtensionRegistry(address(extRegistry)), ITeeMachineRegistry(address(machineRegistry))); | ||
|
|
||
| extRegistry.setSender(address(wraith)); | ||
| wraith.setExtensionId(); | ||
| wraith.setTeeAddress(teeAddr, true); | ||
| wraith.setRouter(address(router)); | ||
|
|
||
| fxrp.mint(alice, ESCROW); | ||
| usdt.mint(address(router), 1000 ether); | ||
| } |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟠 Major | 🏗️ Heavy lift
Add coverage for the redeem settlement path.
The suite has no MockAssetManager and never calls setAssetManager. ACTION_REDEEM, the lot validation, the balance-delta computation, and the remainder refund in _redeem are all untested. That path holds the escrow-accounting flaw flagged on contracts/src/WraithOrders.sol Lines 400-417.
Add a MockAssetManager that burns a configurable amount of tokenIn. Assert the remainder refund and assert that the contract rejects a lot count that would consume more than the order's escrow.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@contracts/test/WraithOrders.t.sol` around lines 97 - 134, Add and initialize
a MockAssetManager in WraithOrdersTest, configure it through setAssetManager,
and add redeem-settlement tests that exercise ACTION_REDEEM. Configure the mock
to burn a variable tokenIn amount, then assert _redeem refunds the unconsumed
escrow remainder and rejects a lot count whose required amount exceeds the order
escrow.
| export function priceToE18(input: string): bigint { | ||
| const [whole, frac = ""] = input.trim().split("."); | ||
| const padded = (frac + "0".repeat(18)).slice(0, 18); | ||
| return BigInt(whole || "0") * 10n ** 18n + BigInt(padded || "0"); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Reject malformed price strings before encoding.
"1.2.3" is encoded as 1.2 because split(".") ignores later segments. This can seal a threshold that differs from the value the user entered. Validate the complete decimal string and reject values with more than 18 fractional digits.
Proposed fix
export function priceToE18(input: string): bigint {
- const [whole, frac = ""] = input.trim().split(".");
+ const normalized = input.trim();
+ if (!/^\d+(?:\.\d{1,18})?$/.test(normalized)) {
+ throw new Error("Price must be a non-negative decimal with at most 18 fractional digits");
+ }
+ const [whole, frac = ""] = normalized.split(".");
const padded = (frac + "0".repeat(18)).slice(0, 18);
return BigInt(whole || "0") * 10n ** 18n + BigInt(padded || "0");
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export function priceToE18(input: string): bigint { | |
| const [whole, frac = ""] = input.trim().split("."); | |
| const padded = (frac + "0".repeat(18)).slice(0, 18); | |
| return BigInt(whole || "0") * 10n ** 18n + BigInt(padded || "0"); | |
| export function priceToE18(input: string): bigint { | |
| const normalized = input.trim(); | |
| if (!/^\d+(?:\.\d{1,18})?$/.test(normalized)) { | |
| throw new Error("Price must be a non-negative decimal with at most 18 fractional digits"); | |
| } | |
| const [whole, frac = ""] = normalized.split("."); | |
| const padded = (frac + "0".repeat(18)).slice(0, 18); | |
| return BigInt(whole || "0") * 10n ** 18n + BigInt(padded || "0"); |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@frontend/lib/wraith.ts` around lines 85 - 88, Update priceToE18 to validate
the entire trimmed input before encoding: allow only a valid decimal form with
at most one decimal separator and no more than 18 fractional digits, rejecting
malformed values such as “1.2.3” and over-precision inputs. Preserve the
existing bigint conversion for valid values.
| /** instructionId -> orderId, for instructions whose result has not arrived yet. */ | ||
| const pending = new Map(); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
Add recovery for completed instructions after a keeper restart.
The in-memory pending map is the only record of an instruction that awaits a TEE result. If the keeper restarts after an order fires, a later re-tick can observe a reversed price and never settle the already-authorized trade.
keeper/src/index.js#L41-L42: persist pending instruction IDs or recover unprocessedOrderTickedevents from the chain and poll their results after startup.keeper/README.md#L40-L40: update the restart statement after recovery exists. Re-ticking alone does not preserve a prior firing decision.
📍 Affects 2 files
keeper/src/index.js#L41-L42(this comment)keeper/README.md#L40-L40
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@keeper/src/index.js` around lines 41 - 42, Implement startup recovery for
completed instructions in keeper/src/index.js around the pending map: persist
pending instruction IDs or scan the chain for unprocessed OrderTicked events,
then poll their TEE results after restart before relying on later ticks. Update
keeper/README.md line 40 to describe the recovered behavior and clarify that
re-ticking alone does not preserve a prior firing decision.
| async function fetchResult(instructionId) { | ||
| const response = await fetch(`${EXT_PROXY_URL}/action/result?id=${instructionId}`); | ||
| if (response.status === 404) return null; | ||
| if (!response.ok) { | ||
| throw new Error(`proxy returned ${response.status} for ${instructionId}`); | ||
| } | ||
|
|
||
| const body = await response.json(); | ||
| // Status >= 2 means the extension is still processing. | ||
| if (body?.status === undefined || body.status >= 2) return null; | ||
| return body; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
Add a timeout to the extension proxy request.
Line 60 can wait indefinitely when the proxy stalls. relayResults then blocks, and the main loop stops future ticks and result relays. Abort the request after a configured timeout and continue with the next pending instruction.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@keeper/src/index.js` around lines 59 - 70, Update fetchResult to abort the
extension proxy fetch after the configured timeout, using an AbortController or
the existing timeout mechanism. Ensure timeout failures are handled as
unavailable results so relayResults continues processing the next pending
instruction and the main loop keeps ticking.
| pending.delete(instructionId); | ||
|
|
||
| if (result.status !== 1) { | ||
| console.error(`order ${orderId}: TEE reported failure (status ${result.status})`); | ||
| continue; | ||
| } | ||
|
|
||
| // A no-op reply — the condition did not fire. This is the expected outcome | ||
| // for almost every tick, and it is the whole point: an observer, this keeper | ||
| // included, learns only "not yet", never how far away the trigger is. | ||
| if (!result.data || result.data === "0x") { | ||
| continue; | ||
| } | ||
|
|
||
| try { | ||
| const hash = await walletClient.writeContract({ | ||
| address: WRAITH_ADDRESS, | ||
| abi, | ||
| functionName: "execute", | ||
| args: [result.data, instructionId, result.submissionTag ?? SUBMISSION_TAG, result.status, result.signature], | ||
| }); | ||
| await publicClient.waitForTransactionReceipt({ hash }); | ||
| console.log(`order ${orderId} executed in ${hash}`); |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
For viem version 2.21.0, what does publicClient.waitForTransactionReceipt return when an included transaction reverts, and what receipt status value should callers check?
💡 Result:
In viem version 2.21.0, the publicClient.waitForTransactionReceipt action is intended to throw a TransactionExecutionError (or similar, depending on the context of the reversion) if the transaction reverts [1][2]. However, there have been community discussions and bug reports noting that behavior may sometimes result in a returned transaction receipt object with a status of 'reverted' instead of throwing an error [1][3]. When the transaction is included on-chain, the returned TransactionReceipt object contains a status field, which indicates whether the transaction was successful or reverted [4][5]. Callers should check the status property of the receipt: - A status of 'success' indicates the transaction was successful [4]. - A status of 'reverted' indicates the transaction failed [1]. Because of the potential for inconsistent behavior regarding whether the function throws an error or returns a receipt with a 'reverted' status, it is best practice to verify the status property of the returned receipt regardless of whether you are wrapping the call in a try/catch block [3].
Citations:
- 1: bug: waitForTransactionReceipt does not throw on reverted transaction wevm/viem#1767
- 2: https://v1.viem.sh/docs/actions/public/waitForTransactionReceipt.html
- 3: Should contract revert errors be thrown on waitForTransactionReceipt rather than writeContract wevm/viem#3890
- 4: https://viem.sh/docs/actions/public/waitForTransactionReceipt
- 5: https://cdn.jsdelivr.net/npm/viem@2.47.6/actions/public/waitForTransactionReceipt.ts
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- keeper/src/index.js ---'
cat -n keeper/src/index.js | sed -n '1,220p'
printf '%s\n' '--- relevant package metadata ---'
fd -i 'package.json|package-lock.json|npm-shrinkwrap.json|yarn.lock|pnpm-lock.yaml' . -t f | sort | while read -r f; do
case "$f" in
*keeper*|./package.json|./package-lock.json|./npm-shrinkwrap.json|./yarn.lock|./pnpm-lock.yaml)
echo "### $f"
rg -n '"viem"|viem@|waitForTransactionReceipt|writeContract' "$f" || true
;;
esac
done
printf '%s\n' '--- pending and execution references ---'
rg -n -C 3 'pending|fetchResult|relayResults|writeContract|waitForTransactionReceipt|actionId|instructionId' keeperRepository: LSUDOKO/Wraith
Length of output: 12641
🏁 Script executed:
#!/bin/bash
set -e
printf '%s\n' '--- exact viem resolution ---'
python3 - <<'PY'
import json
for path in ("keeper/package-lock.json", "keeper/package.json"):
print(f"### {path}")
with open(path) as f:
data = json.load(f)
if path.endswith("package-lock.json"):
for key, value in data.get("packages", {}).items():
if key.endswith("/viem") or key == "node_modules/viem":
print(key, value.get("version"), value.get("resolved"))
else:
print(data.get("dependencies", {}).get("viem"))
PY
printf '%s\n' '--- actionId and execute definitions/usages ---'
rg -n -C 5 'function execute|execute\(|actionId|instructionId|used|consum' --glob '!keeper/src/index.js' --glob '!keeper/package-lock.json' --glob '!package-lock.json' .Repository: LSUDOKO/Wraith
Length of output: 50373
🏁 Script executed:
#!/bin/bash
set -e
python3 - <<'PY'
import json
p = "keeper/package-lock.json"
with open(p) as f:
d = json.load(f)
v = d["packages"]["node_modules/viem"]["version"]
print("resolved viem:", v)
PY
version="$(python3 - <<'PY'
import json
with open("keeper/package-lock.json") as f:
print(json.load(f)["packages"]["node_modules/viem"]["version"])
PY
)"
tmp="$(mktemp -d)"
trap 'rm -rf "$tmp"' EXIT
curl -fsSL "https://registry.npmjs.org/viem/-/viem-${version}.tgz" -o "$tmp/viem.tgz"
tar -xOf "$tmp/viem.tgz" "package/_esm/actions/public/waitForTransactionReceipt.js" 2>/dev/null \
| sed -n '1,240p' || tar -tzf "$tmp/viem.tgz" | rg 'waitForTransactionReceipt'
printf '%s\n' '--- type/status references ---'
tar -xOf "$tmp/viem.tgz" "package/_types/actions/public/waitForTransactionReceipt.d.ts" 2>/dev/null \
| sed -n '1,180p' || trueRepository: LSUDOKO/Wraith
Length of output: 16563
Delete pending only after a successful execution receipt.
Line 118 removes the instruction before execute settles. If execution reverts, the keeper loses a valid result even though the contract rolls back usedActionId. Store the receipt, require receipt.status === "success", then delete the entry. Remove terminal TEE failures and no-op results separately.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@keeper/src/index.js` around lines 118 - 140, Move
pending.delete(instructionId) out of the initial result-processing path and
perform it only after walletClient.writeContract and
publicClient.waitForTransactionReceipt complete with receipt.status ===
"success". Retain separate handling for TEE failures and no-op results, and
leave the pending entry intact when execution throws or the receipt indicates
failure.
| ``` | ||
| User → encrypt order to TEE pubkey → WraithOrders.createOrder(bytes) | ||
| ↓ ciphertext onchain | ||
| Keeper → WraithOrders.tick(orderId) → TeeExtensionRegistry.sendInstructions() | ||
| ↓ relayed | ||
| ext-proxy → TEE → extension POST /action | ||
| ↓ | ||
| decrypt in-enclave → read FTSO → evaluate threshold | ||
| ↓ triggered | ||
| TEE-signed ActionResult → polled from proxy | ||
| ↓ | ||
| Anyone → WraithOrders.execute(...) → ecrecover == teeAddress → swap / redeem FXRP | ||
| ``` |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Add a language label to the diagram block.
The unlabeled fence fails MD040. Use text because this block is an architecture diagram.
Proposed fix
-```
+```text
User → encrypt order to TEE pubkey → WraithOrders.createOrder(bytes)📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ``` | |
| User → encrypt order to TEE pubkey → WraithOrders.createOrder(bytes) | |
| ↓ ciphertext onchain | |
| Keeper → WraithOrders.tick(orderId) → TeeExtensionRegistry.sendInstructions() | |
| ↓ relayed | |
| ext-proxy → TEE → extension POST /action | |
| ↓ | |
| decrypt in-enclave → read FTSO → evaluate threshold | |
| ↓ triggered | |
| TEE-signed ActionResult → polled from proxy | |
| ↓ | |
| Anyone → WraithOrders.execute(...) → ecrecover == teeAddress → swap / redeem FXRP | |
| ``` |
🧰 Tools
🪛 markdownlint-cli2 (0.23.2)
[warning] 23-23: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@README.md` around lines 23 - 35, Label the fenced architecture diagram in the
README with the text language by changing its opening fence to ```text, while
leaving the diagram content unchanged.
Source: Linters/SAST tools
Builds the Wraith MVP end to end: contracts, TEE-side evaluator, keeper, frontend, and the release tooling around them.
What Wraith is
Every on-chain automation protocol today publishes the trigger condition in the clear. A standing order sits on-chain announcing exactly what you will do and when — which is what makes stop-loss hunting possible, and why serious traders keep stops off-exchange or mental.
Wraith encrypts the condition to a TEE. The enclave evaluates it against live FTSO prices and emits a signed result only when it fires. A contract verifies that signature and settles.
What's here
contracts/WraithOrders.sol— escrow + ciphertext storage, TEE signature verification, swap/redeem settlement. 12 Foundry tests.extension/keeper/frontend/docs/TRUST.mdTwo hardenings over the reference example
The Flare
WeatherInsuranceexample is the template this follows, with two deliberate departures:actionIdis consumed once. Without it a signed result can be replayed to execute the same order twice.ITeeMachineRegistryexposes onlygetRandomTeeIds— a random selector, not a membership query — so a contract cannot ask whether an address is a registered TEE. The allowlist is the honest substitute, documented as such rather than dressed up as registry-backed validation.Scope, stated honestly
Wraith hides standing intent, not execution. Once a trigger fires the resulting trade is an ordinary public transaction, and execution-moment MEV is out of scope. The claim is narrower than "MEV-proof" and it is one that survives questioning.
docs/TRUST.mdcovers the rest: on-chain ciphertext is durably exposed and needs an off-chain delivery channel in production, the demo runsSIMULATED_TEE=true, enclave state is volatile, and PMW and in-enclave FDC are both Flare system applications unavailable to third-party extensions.Verification
contracts: 12/12 Foundry tests passextension:go vetclean, all tests pass under-racefrontend: typechecks and buildsTooling
Dependabot across github-actions/npm/gomod with related packages grouped, semantic-release cutting versions from Conventional Commits on merge to
main, and CI running contract and extension tests.Next
Request Coston2 indexer DB credentials from Flare support —
ext-proxycannot start without them and they are issued by a human, so nothing in the TEE stack runs until they land.🤖 Generated with Claude Code
Summary by CodeRabbit