Skip to content

Audit readiness for the Seismic shielded SRC-20 branch#118

Merged
Khrafts merged 27 commits into
feat/seismicfrom
seismic-audit-readiness
Jun 16, 2026
Merged

Audit readiness for the Seismic shielded SRC-20 branch#118
Khrafts merged 27 commits into
feat/seismicfrom
seismic-audit-readiness

Conversation

@Khrafts

@Khrafts Khrafts commented Jun 15, 2026

Copy link
Copy Markdown
Member

Base is feat/seismic, not main. feat/seismic is the long-lived branch carrying MYieldToOne onto Seismic as a shielded SRC-20; it never merges to main. This PR takes that work to audit readiness.

What this PR does

Brings the Seismic branch to an auditable state: freezes the contract diff with the pre-audit fixes, makes the test suite genuinely runnable and comprehensive, migrates the whole build/CI to the sforge/ssolc toolchain, and assembles the deploy tooling + auditor-facing docs.

Contract fixes & decisions (freeze the audit diff)

  • Validate key material in setContractKey / registerPublicKey (zero-key revert, 33-byte compressed-secp256k1 prefix check).
  • ContractKeyNotSet checked before the unregistered-recipient empty-ciphertext fallback.
  • Encrypted Approval(…,bytes) overload on the shielded approve path (native infra approve stays plaintext).
  • balanceOf read-gate extended to compliance roles (FREEZE_MANAGER_ROLE, and FORCED_TRANSFER_MANAGER_ROLE on the forced-transfer subclass).
  • Inline // NOTE: annotations on the three accepted ssolc-10311 shielded-branching side channels.

Tests

  • Migrated the JMIExtension suite to shielded semantics and re-enabled it in the seismic profile (no longer silently excluded).
  • Closed shielded coverage gaps on MYieldToOne / MYieldToOneForcedTransfer (encrypted-event routing, precompile-failure paths, gating matrix, dual-emit regressions, zero-amount branches).
  • Added a seed-based shielded-accounting simulation (Σ balances == totalSupply, M-backing ≥ supply, nonce monotonicity).
  • Added an in-process Seismic integration suite (real 0x65/0x66/0x67/0x68 precompiles, no mocks), a sanvil E2E (TxSeismic 0x4A key install, signed-read gating, on-chain → off-chain decryption), and a read-only live-testnet checker.

Tooling / CI

  • [profile.seismic] (mercury, optimizer_runs = 800, force = true) is the default; make build/tests/coverage/gas-report run under sforge.
  • make integration and make slither fail loudly with accurate explanations instead of passing vacuously.
  • CI runs the seismic unit + in-process integration suites under a pinned toolchain; dead cross-chain deploy targets removed.

Scripts / deploy

  • set-contract-key.sh (uses scast send --seismic — never a Foundry script, which would leak the private key in plaintext calldata), ConfigureSeismicExtension.s.sol, off-chain decryptor, verify-seismic.py, and the 5124 deployment record.

Docs

  • README rewritten for this branch (toolchain pins, build instructions, 5124 deployment table, in-repo audit-scope + ERC-20-deviations sections). Internal/generated planning docs are untracked.

Test status

  • Unit: 468/468, Seismic in-process integration: 11/11 — both green, 0 skipped, no vm.skip anywhere.
  • The 7 mainnet-fork integration suites (test/integration/*.t.sol) compile and are shielded-aware but are excluded from the default run: shielded reads need eth_getFlaggedStorageAt, which fork RPCs don't serve. They require a Seismic devnet (make integration gates on SEISMIC_DEVNET_RPC_URL and fails loudly). Standing up a devnet RPC for CI is a tracked follow-up.

Build & test

source scripts/seismic-env.sh   # repo-local sforge/ssolc on PATH (FOUNDRY_PROFILE=seismic)
make build
make tests
make integration-seismic        # real-precompile E2E

Known follow-ups (not blocking this PR)

  • Wire a SEISMIC_DEVNET_RPC_URL so the fork-integration tier executes in CI.
  • Operational/on-chain: set-contract-key on the live 5124 proxy, the freeze tag, and mainnet role custody.

Khrafts added 27 commits June 11, 2026 21:34
…rPublicKey

A zero private key bypassed the one-shot guard (which compares the stored
key to zero), emitting ContractKeySet while leaving the contract key-less
and a second call able to succeed. Reject it with ZeroPrivateKey, and
reject any 33-byte public key whose first byte is not the compressed-point
prefix 0x02/0x03 with InvalidPublicKeyPrefix in both entry points.
Compliance operators previously had no sanctioned way to learn how much
to seize: forceTransfer takes an explicit amount but neither manager role
was in the balanceOf gate. Allow FREEZE_MANAGER_ROLE holders to read any
balance, and FORCED_TRANSFER_MANAGER_ROLE holders on the ForcedTransfer
subclass via a balanceOf override.
…path

The shielded approve stored the allowance in shielded space but emitted
the standard Approval with the exact amount, making every allowance
public. Mirror the Transfer treatment: refactor the encrypted-emit crypto
core into a shared _encryptAmount helper, emit an encrypted-bytes
Approval(address,address,bytes) overload to the spender's registered key
on the suint256 approve path (empty-ciphertext fallback if unregistered),
and keep the plaintext Approval on the native infra path where the amount
is already public in plain calldata. The nonce counter is shared across
both event kinds.
… fallback

Before setContractKey, shielded transfers to unregistered recipients
succeeded (empty-ciphertext fallback) while transfers to registered ones
reverted -- inconsistent partial availability that leaked who is
registered via reverts. Check the contract key first so ALL user-path
shielded transfers/approves revert ContractKeyNotSet uniformly pre-key;
wrap/unwrap/mint/burn/native-infra paths are unaffected (plaintext emits,
no key needed). The key is deliberately not folded into initialize():
initializer calldata travels in plaintext inside the proxy deployment,
which would leak the private key.
Add NOTE comments at the three ssolc 10311 sites (allowance check,
sender-balance check, _revertIfInsufficientBalance) documenting the
accepted 1-bit revert-vs-success leak inherent to ERC20
insufficient-balance/allowance semantics, and the missing unchecked
justification in yield().
Comment-only sweep: one-line @dev + full @param lists on internal
functions (restores _update's params), terse interface error/event docs,
bespoke section dividers folded into the standard sections. Verified
zero behavioral change (diff is comments-only; 151/151 yieldToOne unit
tests unchanged).
Precompile failure paths (0x65/0x68/0x66), ciphertext fidelity and
expectCall input pinning, nonce monotonicity across Transfer/Approval
emits, zero-amount both branches, self-transfer and zero-recipient,
native-path freeze matrix, de-listing re-blocks, residual-allowance pin,
shielded transferFrom fuzz, unwrap insufficient-balance payload.
forceTransfer to a registered recipient stays plaintext-only with the
encrypted-event nonce untouched; shielded-overload smoke tests; balanceOf
gate for both compliance roles; seizure flow via production-visible
interfaces; forceTransfer works while paused.
Gated balanceOf asserts moved to a harness getter, user transfers to the
suint256 overloads, pause coverage via the shielded path, inherited
gate/allowlist/native-revert tests, and a wrap dual-emit regression.
59/59 green under the seismic profile (suite was previously excluded
with zero runnable tests).
Randomized op sequences over wrap/unwrap/transfers/claims/forceTransfer/
freeze/pause asserting sum-of-balances == totalSupply, M-backing
solvency, and encrypted-event nonce monotonicity after every step.
suint256 overloads for user-path calls, gated balanceOf reads via the
harness, contract-key install in setUp for the pre-key revert semantics,
and infra-allowlisting of the swap adapter where it spends natively.
Compile-verified; the suite still requires a Seismic-aware RPC to run.
…migration

profile ?= seismic with a repo-local toolchain PATH prepend and fail-loud
install guard; shared FORGE_BIN selector for build/sizes/coverage/
gas-report; integration fails loudly without a Seismic devnet RPC;
slither replaced with a documented limitation; dead non-Seismic deploy/
upgrade/propose targets removed; force=true in the seismic profile kills
the OZ upgrades-core partial-build-info flake; JMIExtension.t.sol
re-admitted to the seismic test set; pre-commit fails fast with an
install hint; package.json scripts pruned to match.
New test-seismic.yml installs the pinned toolchain and runs the seismic
profile suite plus a sizes check; the three stock-forge workflows are
disabled on this branch (they cannot parse suint256 and were permanently
red).
set-contract-key.sh wraps scast send --seismic (TxSeismic 0x4A) so the
contract encryption key never appears in plaintext calldata — a Foundry
script cannot do a shielded broadcast and would leak it permanently.
ConfigureSeismicExtension.s.sol approves the extension on SwapFacility
and allowlists the infra addresses. .env.example gains the Seismic block.
deployments/5124.json with the live SwapFacility and USDS extension
addresses, plus the recovered broadcast record so the re-verify target
and ScriptBase address resolution work from a fresh clone.
The lock still pinned lib/common at v1.5.0 while the submodule points at
a1fbf37 (feat/erc20-virtual, v1.5.1 + the 12-line virtual-marker delta);
a forge-driven dependency materialization could have silently rolled it
back.
README rewritten for this standalone branch (shielded MYieldToOne
description, Seismic build instructions, 5124 deployment table);
AUDIT-SCOPE.md with the in-scope list, system context, trust-model
table, ERC-20 deviations, and accepted risks; TOOLCHAIN.md pins the
Seismic toolchain the deployed bytecode was built with; RUNBOOK.md
orders deploy -> verify -> configure -> set-contract-key; audits/README
maps prior reports to their actual coverage; the Seismic design docs
are un-ignored and tracked.
…nsion

It is not yet deployed on Seismic testnet; the allowlist starts with
Portal only and LOP is added via the same target once live.
SwapFacility.setAdminApprovedExtension(USDS, true) and
setAllowlisted([Portal], true) executed on Seismic testnet; verified
on-chain (isApprovedExtension and isAllowlisted both true).
…tnet checker

sforge's EVM implements the real Seismic precompiles, so the 11-test
suite pins the 0x65/0x66/0x67/0x68 vectors and proves real-key
encrypt/decrypt round-trips with no mocks. run-sanvil-e2e.sh drives the
full stack over RPC: setContractKey as TxSeismic 0x4A (private key absent
from on-chain calldata), four-way signed-read gating, and off-chain
decryption of a captured Transfer(bytes) event. check-live-testnet.sh is
a read-only chain-5124 checklist. New make targets integration-seismic /
e2e-sanvil / check-live-seismic-testnet; CI runs the in-process suite.
Pure-stdlib secp256k1 ECDH + double HKDF-SHA256 (libsecp256k1 shared
secret with info 'aes-gcm key', then 'seismic_hkdf_105') + keccak-derived
nonce + AES-256-GCM, matching the pinned precompile semantics; fast-paths
through the cryptography package when available. --self-test asserts the
cross-validated vectors.
Generated with the pinned toolchain: yieldToOne contracts at 100% line/
branch/function unit coverage (src total 94.13% lines); EIP-170 margins
(JMIExtension tightest at 1,040 B); shielded-path gas medians.
Portal provenance + remaining suite-deployment addresses footnoted in
AUDIT-SCOPE.md; RUNBOOK/AUDIT-READINESS reflect the executed configure
step (extension approved, Portal allowlisted) with set-contract-key as
the one remaining on-chain action.
The audit-readiness plan, scope, runbook, toolchain pins, and the
docs/audit/ design notes were internal/generated material that should
not be git-tracked. Remove them from tracking (kept locally) and ignore
all of docs/.

Fold the auditor-facing essentials those files were referenced for into
the tracked tree so a fresh clone has no dead links:
- README: add the pinned-toolchain table and an "Audit scope" section
  (in-scope files + ERC-20 deviations)
- repoint audits/README, reports/README, the slither Makefile target,
  ConfigureSeismicExtension, and the seismic integration breadcrumbs to
  in-repo content / the real make targets
…tegration

Unit (test/unit/projects/yieldToOne): +15 tests — encrypted-event fuzz
(exactly-one-nonce-per-emit, ciphertext == precompile output, bytes-overload
topic0 distinct from the uint256 overload), zero-amount shielded transferFrom
to a registered recipient, allowance-boundary fuzz, and type(uint256).max
boundaries; strengthened the simulation invariants (infra + unregistered
holders, per-holder no-underflow, live balanceOf-gate check at each checkpoint).

Integration: new test/integration/seismic/MExtensionSystemSeismic.t.sol — a
non-forking, in-process system suite on the seismic EVM (real 0x65/0x66/0x68
precompiles, no mocks) covering wrap, cross-extension swap via SwapFacility,
shielded transfer/transferFrom with real ciphertexts, yield accrue/claim,
freeze + forceTransfer, and permissioned-extension gating. `make
integration-seismic` now runs 22 tests (was 11). The mainnet-fork integration
suites stay unrunnable under mercury (no usable eth_getFlaggedStorageAt fork
source), so this restores live multi-contract SRC-20 coverage without forking.
…s find it

sfoundryup installs `ssolc` to /usr/local/bin (install_ssolc ignores
FOUNDRY_BIN_DIR), which is outside the cached .seismic-toolchain dir. The
first (cache-miss) run compiles fine because /usr/local/bin is on PATH, but
the cache only stores .seismic-toolchain — so every cache-hit run starts on a
fresh runner with no ssolc and fails at compile time with
"`ssolc` not found in PATH" (the Makefile only prepends
.seismic-toolchain/bin to PATH).

Fix: in the install step, export FOUNDRY_BIN_DIR and copy the installed ssolc
into .seismic-toolchain/bin so it's cached and restored alongside sforge.
Roll the cache key via TOOLCHAIN_CACHE_VERSION to evict the existing
ssolc-less caches, and assert ssolc presence up front so a regression fails
fast instead of deep in compilation.
@Khrafts Khrafts merged commit d1a1565 into feat/seismic Jun 16, 2026
2 checks passed
@Khrafts Khrafts deleted the seismic-audit-readiness branch June 16, 2026 13:07
@Khrafts Khrafts mentioned this pull request Jun 17, 2026
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