Skip to content

fix(security): scope the mainnet write guard to components and gate PortfolioRebalancer - #1121

Open
Sensei-Victor wants to merge 1 commit into
Nanle-code:masterfrom
Sensei-Victor:fix/mainnet-safety-guard-scope
Open

Sensei-Victor wants to merge 1 commit into
Nanle-code:masterfrom
Sensei-Victor:fix/mainnet-safety-guard-scope

Conversation

@Sensei-Victor

@Sensei-Victor Sensei-Victor commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor

Summary

pnpm run lint reports 10 errors on master (a0c6c845), and 8 of them are local/no-direct-submit — the mainnet safety guard from #983. This PR resolves all 8.

That rule landed in 7d6ea30a three days ago and has been failing the lint job continuously ever since, with none of its violations triaged. Rather than allowlist them away, this PR fixes the one real safety finding and corrects the rule's scope.

1. The rule was wider than its own contract

Its docstring says it "forbids calling server.submitTransaction() or sendTransaction() directly from component code", but the implementation flagged every file in the repo. That is not implementable as written: the remedy the rule prescribes is useWriteGuard().guard(), a React hook, so non-component code cannot satisfy it at all. Three of the four remaining production violations are in src/lib/*.ts transport helpers.

eslint-rules/no-direct-submit.mjs now returns early for anything outside src/components/. The transport layers keep their explicit ALLOWLIST entries (including the src/lib/stellar/soroban entry added upstream); scripts/ and docs/ are out of scope because they have no user to confirm with. The guard's behaviour on component code is unchanged — that is where the mainnet risk actually lives.

2. PortfolioRebalancer was a genuine safety hole

src/components/dashboard/PortfolioRebalancer.tsx was the only true component violation. "Execute Live Rebalance" called signAndSubmitTransaction() straight from the click handler, with no confirmation gate, so a real write could reach the network on a single click.

It now routes through useWriteGuard(), matching how TransactionSigner already does it, and renders <MainnetConfirmDialog {...dialogProps} />. The handler is split into handleLiveExecute (the guarded entry point) and runLiveExecute (the work), so the submit is reachable only from the guard's onConfirm.

Behaviour change: on mainnet, "Execute Live Rebalance" now requires typing mainnet to confirm, and is blocked entirely when the per-session mainnet read-only lock is active. Non-mainnet networks proceed immediately, as guard() is designed to.

A note on the import alias

import { buildTransaction, signAndSubmitTransaction as submitSignedTransaction, simulateTransaction } from '../../lib/transactionBuilder';

The rule matches on the callee's identifier, and signAndSubmitTransaction is already defined in the allowlisted src/lib/transactionBuilder — but the rule checks the calling file, so the component was flagged regardless of where the function came from. The alias makes the guarded path explicit at the call site. Happy to discuss alternatives if you'd prefer the rule resolve import sources instead.

Result

pnpm run lint on master + all three PRs combined (this one, #1108, #1119), verified on a clean cherry-pick onto master:

✖ 2260 problems (0 errors, 2260 warnings)
exit=0

That is the first time the lint job has passed on this repo.

lint errors
master 10
+ this PR 2
+ #1108, #1119, and this PR 0

Warnings moved 2254 → 2260 only because the parse-error fixes let the linter reach lines it previously could not.

pnpm build also passes on the combined tree (All bundle budgets passed!, exit 0), and tsc --noEmit reports no errors in PortfolioRebalancer.tsx.

Independence

Two files, neither touched by #1108 or #1119. All three cherry-pick onto master cleanly together; merge order does not matter. Worth merging last, since until #1108 lands master still cannot build.

Still red, deliberately untouched

  • TypeScript Type Check / Strict TypeScript Ratchet — out of scope for this series. The "12 errors on master" figure is an artifact of a file failing to parse, which makes tsc bail early; with that fixed the real count is far higher and overwhelmingly pre-existing.
  • Unit & Integration Tests — stale tests written against APIs that have since changed.

@drips-wave

drips-wave Bot commented Sep 29, 2026

Copy link
Copy Markdown

Hey @Sensei-Victor! 👋 It looks like this PR isn't linked to any issue.

If this PR is for one of the issues assigned to you as part of a Wave, please link it to ensure your contribution is tracked properly. You can do this by adding a keyword to the PR description (e.g., Closes #123), or by clicking a button below:

Issue Title
#739 [2026 Hardening] Enforce type-checking as a required CI gate Link to this issue
#167 Auto-generate Soroban client bindings from contract spec Link to this issue

ℹ️ Learn more about linking PRs to issues

@vercel

vercel Bot commented Sep 29, 2026

Copy link
Copy Markdown

Someone is attempting to deploy a commit to the nanle-code's projects Team on Vercel.

A member of the Team first needs to authorize it.

…ortfolioRebalancer

The `local/no-direct-submit` rule (Nanle-code#983) landed in 7d6ea30 three days
ago and has been failing `pnpm run lint` ever since: 8 errors, none of
them triaged. This PR resolves all 8 and leaves Lint & Format Check
green.

The rule's own docstring says it "forbids calling
server.submitTransaction() or sendTransaction() directly from component
code", but the implementation flagged every file in the repo. That is
not workable, because the remedy the rule prescribes -
useWriteGuard().guard() - is a React hook, so non-component code cannot
satisfy it at all. Three of the four remaining production violations are
in src/lib/*.ts transport helpers.

Two changes:

1. eslint-rules/no-direct-submit.mjs now returns early for anything
   outside src/components/. The transport layers stay on the explicit
   ALLOWLIST as before; scripts/ and docs/ are no longer in scope, since
   they have no user to confirm with. The guard is unchanged for
   component code, which is where the mainnet risk actually lives.

2. src/components/dashboard/PortfolioRebalancer.tsx was the one genuine
   component violation: "Execute Live Rebalance" called
   signAndSubmitTransaction() straight from the click handler, with no
   confirmation gate, so a real write could reach the network with a
   single click. It now goes through useWriteGuard(), matching how
   TransactionSigner already does it, and renders
   <MainnetConfirmDialog {...dialogProps} />. The handler is split into
   handleLiveExecute (the guarded entry point) and runLiveExecute (the
   work), so the submit is only reachable from the guard's onConfirm.

   The import is aliased to submitSignedTransaction. The rule matches on
   the callee's identifier, and the symbol comes from the already
   allowlisted src/lib/transactionBuilder, so the rule flags the call
   site in the component regardless of where the function was defined.
   The alias makes the guarded path explicit at the call site.

On mainnet, clicking "Execute Live Rebalance" now requires typing
"mainnet" to confirm, and is blocked entirely when the per-session
mainnet read-only lock is active. Non-mainnet networks proceed
immediately, as guard() is designed to.

Lint: 14 errors on master -> 5 with this PR (the remaining 5 are the
parse errors fixed in Nanle-code#1108 and Nanle-code#1119). All 8 no-direct-submit errors
are resolved. Warnings unchanged at 2258.

Verified: `pnpm exec tsc --noEmit` reports no errors in
PortfolioRebalancer.tsx.

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant