Skip to content

feat: Wraith MVP — private conditional orders on Flare - #1

Merged
LSUDOKO merged 5 commits into
mainfrom
feat/wraith-mvp
Aug 11, 2026
Merged

LSUDOKO merged 5 commits into
mainfrom
feat/wraith-mvp

Conversation

@LSUDOKO

@LSUDOKO LSUDOKO commented Aug 11, 2026

Copy link
Copy Markdown
Owner

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

Component What it does
contracts/ WraithOrders.sol — escrow + ciphertext storage, TEE signature verification, swap/redeem settlement. 12 Foundry tests.
extension/ Pure, dependency-free trigger evaluator with 54 test assertions. Grafts onto the Flare FCC scaffold.
keeper/ Permissionless tick-and-relay loop.
frontend/ Next.js composer that encrypts terms client-side.
docs/TRUST.md Every trust assumption, stated plainly.

Two hardenings over the reference example

The Flare WeatherInsurance example is the template this follows, with two deliberate departures:

  • actionId is consumed once. Without it a signed result can be replayed to execute the same order twice.
  • TEE signers are an allowlist, not a single owner-set address. ITeeMachineRegistry exposes only getRandomTeeIds — 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.md covers the rest: on-chain ciphertext is durably exposed and needs an off-chain delivery channel in production, the demo runs SIMULATED_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 pass
  • extension: go vet clean, all tests pass under -race
  • frontend: typechecks and builds
  • Not verified: the frontend was never rendered in a browser — none was available in this environment.

Tooling

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-proxy cannot 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

  • New Features
    • Added encrypted, conditional cross-chain orders with escrow, threshold triggers, swaps, redemptions, cancellation, refunds, and replay protection.
    • Added a dark-themed web interface for connecting a Coston2 wallet, creating orders, and viewing order status.
    • Added automated keeper processing for eligible orders and execution results.
  • Documentation
    • Added setup, architecture, privacy, security, and trust guidance for the application and its components.
  • Chores
    • Added automated testing, releases, dependency updates, and project configuration.

LSUDOKO and others added 5 commits August 11, 2026 23:20
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>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Currently processing new changes in this PR. This may take a few minutes, please wait...

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 98ced77d-0749-4ef7-8b14-c034b51a77ca

📥 Commits

Reviewing files that changed from the base of the PR and between 2f1eeb7 and 0227c99.

⛔ Files ignored due to path filters (4)
  • contracts/foundry.lock is excluded by !**/*.lock
  • frontend/package-lock.json is excluded by !**/package-lock.json
  • keeper/package-lock.json is excluded by !**/package-lock.json
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (36)
  • .github/dependabot.yml
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • .gitignore
  • .gitmodules
  • .releaserc.json
  • README.md
  • contracts/foundry.toml
  • contracts/lib/forge-std
  • contracts/src/WraithOrders.sol
  • contracts/src/interfaces/ITeeExtensionRegistry.sol
  • contracts/src/interfaces/ITeeMachineRegistry.sol
  • contracts/src/interfaces/IWraithExternal.sol
  • contracts/test/WraithOrders.t.sol
  • docs/TRUST.md
  • extension/README.md
  • extension/go.mod
  • extension/internal/config/config.go
  • extension/internal/trigger/trigger.go
  • extension/internal/trigger/trigger_test.go
  • frontend/.env.example
  • frontend/README.md
  • frontend/app/api/info/route.ts
  • frontend/app/globals.css
  • frontend/app/layout.tsx
  • frontend/app/page.tsx
  • frontend/lib/wraith.ts
  • frontend/next-env.d.ts
  • frontend/next.config.mjs
  • frontend/package.json
  • frontend/tsconfig.json
  • frontend/tsconfig.tsbuildinfo
  • keeper/README.md
  • keeper/package.json
  • keeper/src/index.js
  • package.json
 __________________________________________________________________________________________
< Recursion is the root of computation since it trades description for time. - Alan Perlis >
 ------------------------------------------------------------------------------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/wraith-mvp

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@LSUDOKO
LSUDOKO merged commit 370a591 into main Aug 11, 2026
2 of 3 checks passed
@LSUDOKO
LSUDOKO deleted the feat/wraith-mvp branch August 11, 2026 19:28
@github-actions

Copy link
Copy Markdown

🎉 This PR is included in version 1.0.0 🎉

The release is available on GitHub release

Your semantic-release bot 📦🚀

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 17

🧹 Nitpick comments (6)
contracts/src/WraithOrders.sol (4)

390-394: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Reset the router allowance after the swap.

_swap sets the allowance to _o.amountIn and never clears it. Two problems follow. First, if the router pulls less than _o.amountIn, a residual allowance stays on an arbitrary user-supplied tokenIn. 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 win

Handle ERC-20 implementations that return no data.

IERC20.transfer, transferFrom, and approve are declared with bool returns. Tokens that omit the return value (USDT on several chains) make the ABI decoder revert, even though the transfer succeeded. This affects createOrder (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 win

Add a way to rotate the owner.

owner is set in the constructor and no function changes it. The owner controls setTeeAddress, setRouter, and setAssetManager. 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 in docs/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 value

Reject high-s signatures and non-canonical v.

_recover accepts both s values of a malleable pair and normalizes v from 0/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 win

Bind the test signing helper to the contract constant.

_sign hard-codes bytes32("TEE_ACTION_RESULT"). TEE_ACTION_RESULT_PREFIX is private in WraithOrders, 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 lift

Load a bounded order page instead of the full history.

loadOrders performs 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2f1eeb7 and 0227c99.

⛔ Files ignored due to path filters (4)
  • contracts/foundry.lock is excluded by !**/*.lock
  • frontend/package-lock.json is excluded by !**/package-lock.json
  • keeper/package-lock.json is excluded by !**/package-lock.json
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (36)
  • .github/dependabot.yml
  • .github/workflows/ci.yml
  • .github/workflows/release.yml
  • .gitignore
  • .gitmodules
  • .releaserc.json
  • README.md
  • contracts/foundry.toml
  • contracts/lib/forge-std
  • contracts/src/WraithOrders.sol
  • contracts/src/interfaces/ITeeExtensionRegistry.sol
  • contracts/src/interfaces/ITeeMachineRegistry.sol
  • contracts/src/interfaces/IWraithExternal.sol
  • contracts/test/WraithOrders.t.sol
  • docs/TRUST.md
  • extension/README.md
  • extension/go.mod
  • extension/internal/config/config.go
  • extension/internal/trigger/trigger.go
  • extension/internal/trigger/trigger_test.go
  • frontend/.env.example
  • frontend/README.md
  • frontend/app/api/info/route.ts
  • frontend/app/globals.css
  • frontend/app/layout.tsx
  • frontend/app/page.tsx
  • frontend/lib/wraith.ts
  • frontend/next-env.d.ts
  • frontend/next.config.mjs
  • frontend/package.json
  • frontend/tsconfig.json
  • frontend/tsconfig.tsbuildinfo
  • keeper/README.md
  • keeper/package.json
  • keeper/src/index.js
  • package.json

Comment thread .github/workflows/ci.yml
Comment on lines +11 to +39
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.yml

Repository: 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'
fi

Repository: 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.

Comment thread .github/workflows/ci.yml
Comment on lines +15 to +17
- uses: actions/checkout@v4
with:
submodules: recursive

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 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: false

Also 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

Comment on lines +186 to +205
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
})
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Suggested change
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.

Comment on lines +400 to +417
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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.

Suggested change
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.

Comment on lines +97 to +134
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);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Comment thread frontend/lib/wraith.ts
Comment on lines +85 to +88
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");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 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.

Suggested change
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.

Comment thread keeper/src/index.js
Comment on lines +41 to +42
/** instructionId -> orderId, for instructions whose result has not arrived yet. */
const pending = new Map();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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 unprocessed OrderTicked events 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.

Comment thread keeper/src/index.js
Comment on lines +59 to +70
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;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 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.

Comment thread keeper/src/index.js
Comment on lines +118 to +140
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}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ 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:


🏁 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' keeper

Repository: 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' || true

Repository: 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.

Comment thread README.md
Comment on lines +23 to +35
```
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
```

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 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.

Suggested change
```
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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant