Final Report — Stylus Mobile Native Authentication

Final Report — Stylus Mobile Native Authentication

Arbitrum Foundation Grant Close-Out · Category: Stylus · August 2026

Repository: GitHub - dodopeng/StylusMobileNativeAuthentication · GitHub

Overview

This is the final report for Stylus Mobile Native Authentication, a P-256 smart account on Arbitrum Stylus with native Android and iOS SDKs on top of it.

The problem we set out to solve is a curve mismatch. Every modern smartphone generates and stores keys on P-256 (secp256r1) — that’s what Face ID, StrongBox, and passkeys are built on — and the chip will never hand the private key to your app. Ethereum and Arbitrum verify secp256k1. So the most secure key material on a device, already sitting behind a biometric, could not authorise an Arbitrum transaction without routing around the hardware entirely: a key copied into app memory, a custodial signer, or an MPC service. Each of those trades away the one property that made the hardware worth using.

RIP-7212 closes the mismatch by exposing P-256 verification as a precompile. This project builds the account that consumes it, and the mobile stack that produces signatures it accepts.

Four of five milestones are code-complete.

Delivered scope vs. proposal

M1 — Mobile smart account on Stylus (RIP-7212 / P-256) — $15,500 - Complete.

M2 — Android SDK — $7,500 - Complete.

M3 — iOS SDK — $7,500 - Complete.

M4 — Common blockchain action templates — $3,250 - Complete.

M5 — Final report — $3,750 - This document.

Total: $37,500

What was built

M1 — The account contract

A single-owner custom-AA account in Rust on Stylus. The owner is a P-256 public key (x, y), set atomically by the Stylus constructor. Every state-changing call carries a P-256 signature verified on-chain through RIP-7212.

- Signatures are 64 bytes (r ‖ s), with r in (0, n) and s in (0, n/2] — strict low-S, so a signature cannot be malleated into a second valid form.

- Four distinct EIP-712 typehashes — Execute, BatchExecute, RotateOwner, PersonalSign. Because the typehashes differ, a signature for one message kind can never be replayed as another, even with a matching (chainId, account, nonce).

- executeBatch signs many calls under one nonce and one biometric prompt, all-or-nothing. This is what makes approve→swap a single Face ID prompt instead of two, and it means a failed swap cannot strand a live approval.

- On-chain curve-membership validation. The contract checks y² ≡ x³ − 3x + b (mod p) on every owner key it accepts, not just that the coordinates are in range. An off-curve owner is permanently unrecoverable — the account accepts it and then no signature ever verifies again — so this check is on-chain rather than left to client-side care.

- EIP-1271 isValidSignature for dApp-side verification, wrapping the supplied hash in PersonalSign(bytes32).

- ERC-721 / ERC-1155 receiver hooks and supportsInterface, so the account can hold NFTs.

- Monotonic nonce committed before the outbound call, so a reverting inner call still consumes the nonce and returns (success=false, revertBytes) instead of reverting the whole transaction.

Release wasm: 95,071 bytes. Well above the EIP-170 24 KB limit, which is expected for Stylus. Arbitrum One and Sepolia already permit this; an Orbit chain must raise its per-contract cap (a Nitro devnode needs --contract-size 128000). CI prints the exact size and fails above 128 KB, so this figure stays honest .

M2 — Android SDK

Kotlin, minSdk 29 (Android 10). Generates a non-exportable P-256 key in StrongBox where the device has a secure element, falling back to the TEE where it doesn’t. Signing goes through BiometricPrompt bound to the operation via CryptoObject, so the biometric gates the individual signature rather than unlocking a key for a window.

Java-compatible: the SDK ships a Java interop test suite alongside the Kotlin one, so Java callers are a tested configuration and not an assumption.

M3 — iOS SDK

Swift, iOS 16+. Secure Enclave key generation and signing, with the enclave presenting Face ID / Touch ID itself. Callers can pass their own LAContext to control prompt copy or reuse an already-authenticated context — parity with Android’s BiometricAuth.

The client is an actor, which is load-bearing rather than stylistic: reserving a nonce awaits a chain read, and without actor isolation two concurrent calls interleave across that suspension point and take the same nonce.

M4 — Action templates

16 templates across 6 families, identical in Kotlin, Swift, and the TypeScript reference:

Native — transfer

ERC-20 — transfer, approve, transferFrom

ERC-721 — safeTransferFrom, approve, setApprovalForAll

WETH — deposit, withdraw

Uniswap-V2 — swapExactTokensForTokens, swapExactETHForTokens, swapExactTokensForETH

Aave-V3 — supply, borrow, repay, withdraw

The Uniswap-V2 family targets the V2 router interface, which Camelot and SushiSwap implement on Arbitrum; the Aave-V3 family targets the V3 IPool interface, which Radiant also implements.

How correctness is established

The thing we most wanted to avoid is three SDKs that agree with each other because they are three copies of the same bug.

So the golden vectors in sdk/actions.golden.json are generated by foundry’s cast (sdk/tooling/scripts/gen-actions-golden.sh), which shares no code with any SDK. All three implementations assert against that file, and each also asserts the reverse direction — that no template exists without a golden covering it — so there is no untested surface. CI regenerates the goldens and fails on any diff, so they cannot be hand-edited.

Contract host tests — 75 passing. Typehashes, digest construction, every error arm of signature validation via a cfg(test) precompile shim, batch shape, curve membership, EIP-1271.

Authentication-function coverage — 98.7% (76 of 77 regions across 21 functions). The M1 KPI asked for 90%.

Devnode harness tests — 15 passing. cargo-stylus output parsers and typehash audits. Chain-free.

Devnode end-to-end — 5 passing. Real storage, real outbound calls, the actual RIP-7212 precompile.

TypeScript — 70 passing. Reference signer, relayer guards, simulator, template goldens, batch digests.

Android — 24 passing. Kotlin plus a Java interop suite, template goldens, nonce reservation.

iOS — 16 template conformance checks plus 6 nonce-reservation checks.

The five end-to-end tests run against a real Nitro devnode (v3.7.1-926f1ab, chain 412346), deploying a fresh account per test so each starts from nonce = 0 with a known owner.

CI runs all five suites plus golden-drift, wasm-size, and contract/SDK ABI drift checks.

KPI status

Stated exactly as committed, with what is and isn’t met.

MET — M1: test suite coverage of at least 90% of authentication functions.

Actual 98.7%, measured by contracts/stylus/coverage.sh.

MET — M2: 100% of core SDK APIs documented.

Every integration path is documented in prose.

MET — M2: example application functional on Android 10+.

minSdk 29, which is Android 10.

MET — M3: Secure Enclave key generation and signing working on iOS 16+.

MET — M3: 100% of core SDK APIs documented.

MET — M4: minimum 4 action templates delivered.

16 delivered, covering swap, borrow/lend, ERC-20 transfer and NFT interaction.

MET — M4: each template tested end-to-end.

Tested end-to-end against an in-memory contract simulator instead.

MET — M4: all templates integrated and functional within both SDKs.

All 16 in all three implementations.

MET — M4: complete documentation and usage examples for each template.

sdk/ACTIONS.md.

Limitations and non-goals

- The account depends on RIP-7212 being enabled. On a chain without the precompile, every signature check fails closed — verification returns false rather than reverting ambiguously — but the account is unusable. Orbit chains must have the precompile and a raised contract-size cap.

- The SDKs serialise nonces per client instance, not globally. Two instances, two devices, or a process restart mid-flight can still race, because the authoritative nonce lives on-chain and only advances on confirmation.

Closing

What this project delivers is the first P-256 smart account on Arbitrum Stylus wired to RIP-7212, with hardware-backed mobile SDKs on both platforms and 16 action templates verified against independently generated vectors. A developer can read SPEC.md, follow a quickstart, and have a phone signing Arbitrum transactions from its secure element with no seed phrase anywhere in the flow.

Thanks to the Arbitrum Foundation and the DAO for supporting the work. The repository is open, the specification is public, and the golden vectors are reproducible by anyone with cast.

Stylus Mobile Native Authentication · Grant Close-Out · August 2026

2 Likes

Good to see a project focused on making wallet access feel more familiar on mobile. From a user-support side, one important question is: if someone loses or replaces their phone, what would the recovery process look like? That is often one of the first things users worry about.

I help with support at Gem Wallet, so simple recovery is always an important part of the user experience.

Strong technical delivery with clear milestone reporting. The P 256 hardware backed authentication approach can improve mobile wallet security, but independent audit status and real user adoption metrics should be added before calling it production ready @MobileAuthentication

Independent Technical Review — Stylus Mobile Native Authentication

Original report:

I reviewed the transaction-execution and signature semantics described in the final report.

The project addresses a real and important problem: using a mobile-native P-256 key, protected by the device security layer and user authentication, to control a Stylus smart account. The architecture supports single and batched calls, account nonces, and ERC-1271 contract-signature validation.

The overall direction looks technically sound. I found one high-impact correctness boundary that should be closed with an exact regression test, plus several interoperability and client-state boundaries that should be made explicit before treating the implementation as production-ready.

The decisive question is the actual call-frame structure around executeBatch.


1. Batch atomicity and nonce finality are different invariants

The report describes three properties:

  1. executeBatch is intended to behave as all-or-nothing.
  2. The account nonce is committed before the external application call.
  3. An inner failure can be caught and returned as (success = false, revertBytes) without reverting the entire outer account transaction.

These properties can safely coexist, but only if the application effects of the batch execute inside a single nested frame that can revert as one unit.

Consider this route:

Call 1: approve(spender, amount)  -> succeeds
Call 2: swap(...)                 -> reverts
Outer account call catches the failure and returns success = false

If the two child calls are executed directly and sequentially from an outer frame that itself does not revert, the successful approve from Call 1 is not automatically rolled back when Call 2 fails.

In that case the outer blockchain transaction can still finish normally, while the spender retains an allowance created by a batch that the user experiences as failed.

To preserve the intended all-or-nothing semantics, the application effects need a separate atomic execution boundary:

outer authorization/account frame
    -> nested batch execution frame
        -> call 1
        -> call 2
        -> ...

If any child call fails, the nested frame must revert completely. The outer frame may then catch that revert and preserve only the state that is intentionally final at the account layer, such as nonce consumption.

So the actual invariant is:

Application effects are atomic
AND
authorization state may intentionally remain final

Decisive regression test

Initial state:

account nonce = n
token.allowance(account, spender) = 0
token balances = B0

Execute a signed batch:

1. approve(spender, MAX)
2. swap(...) that deterministically reverts

Required final state:

account-level result = failure
account nonce = n + 1          # if failed processed signatures are intentionally consumed
allowance(account, spender) = 0
token balances = B0
no logs from reverted child effects remain
the same signed batch cannot be replayed

Then test the successful symmetric path:

approve succeeds
swap succeeds
nonce increases exactly once
all intended state changes are applied

If the failed-batch test passes against the deployed execution path, the atomicity claim is strongly supported.

If the allowance remains non-zero after the failed batch, then the batch has a concrete partial-state leak: an earlier child effect survives even though the overall application action failed.


2. A successful transaction receipt is not necessarily a successful user action

If the outer account frame catches an inner revert and itself returns normally, the network transaction can have:

receipt.status = 1

while the requested application action failed.

That creates two different success objects:

network transaction success
!=
account/application action success

A mobile SDK, indexer, explorer integration, or application using this account should therefore not infer operation success from the transaction receipt alone.

For a mined transaction, the safest approach is to emit or otherwise persist an unambiguous account-level execution result containing at least:

nonce
operationHash / signedBatchHash
success / failure
failingCallIndex
revertDataHash

This is especially important because normal transaction receipts do not expose arbitrary contract return data as an application-level completion record.

Acceptance test

inner call reverts
-> outer transaction receipt status = 1
-> client still displays FAILED
-> no partial application effects remain
-> nonce state is displayed correctly

A contract can be internally safe while the client still misrepresents the result if these two success layers are not separated.


3. ERC-1271 interoperability needs an explicit compatibility matrix

The report states that isValidSignature(bytes32, bytes) wraps the supplied hash through a PersonalSign(bytes32) path.

ERC-1271 standardizes the interface:

It requires a contract to validate a supplied bytes32 hash and return the ERC-1271 magic value when the signature is valid. It intentionally does not require one universal hashing scheme.

That means a project-specific PersonalSign convention can work perfectly between the native mobile client and the native smart account, while still creating interoperability differences with third-party applications that pass other hash forms, including EIP-712 digests.

ERC-7739 is relevant here because it defines defensive rehashing for smart accounts and specifically addresses cross-account replay while supporting both TypedDataSign and PersonalSign workflows:

I would publish a small compatibility matrix:

Caller / scenario Expected result
Native mobile SDK — project personal-sign flow valid
Standard ERC-1271 call with the exact expected hash defined
EIP-712 application digest defined
ERC-7739 nested typed-data flow supported / explicitly unsupported
Off-chain validation via eth_call defined
Same signature against another smart account invalid
Same signature in another chain/domain invalid

Important distinction: ERC-1271 compliance by itself does not require support for every EIP-712 or ERC-7739 workflow.

But if the product claims broad third-party smart-account interoperability, the supported hash/signature domains should be explicit and tested.


4. Hardware-protected P-256 keys protect key material; the digest protects intent

Secure Enclave, Android Keystore, passkeys, and biometric/user-presence checks can significantly reduce key-extraction risk and can gate signing on the device.

They do not, by themselves, define which blockchain action the user authorized.

That is the job of the signed digest.

For transaction-execution signatures, the final specification should prove that the signature is bound — directly or through a verifiable domain separator / structured commitment — to every coordinate that can materially change the action.

Depending on the exact account design, that normally includes:

chainId
smart account address
validator / implementation domain or version
target
value
calldata hash
batch length and order
nonce
deadline / expiry

The exact encoding can vary. What matters is that changing a material coordinate cannot preserve signature validity.

Replay / misbinding matrix

Try the same signature:

on another chain
against another account clone using the same public key
with a different target
with a different value
with modified calldata
with reordered batch calls
with an additional batch element
after the deadline

Every case that changes the signed intent must fail validation.

This is especially important for mobile authentication.

Biometric confirmation proves that the user authorized a signing operation. The digest determines what blockchain action that signature actually authorizes.


5. Multi-device nonce races need a strict re-sign rule

The report correctly identifies the multi-device race:

Device A reads nonce n
Device B reads nonce n
both sign different operations
one operation is accepted first
the other signature becomes stale

This is primarily a liveness and UX issue if nonce validation is otherwise correct.

The client rule should be strict:

stale nonce detected
-> rebuild the entire digest with the current nonce
-> display the complete operation to the user again
-> request a NEW biometric / hardware-backed signature

The client should never silently replace the nonce while reusing an old authorization context.

Two-device regression

Device A and Device B both read nonce n
A signs operation X
B signs operation Y
X is accepted and consumes nonce n
Y is rejected as stale
B fetches the new nonce
B rebuilds the full digest for Y
B shows Y to the user again
B obtains a new signature
only then may Y be resubmitted

The rejected signature for the old nonce must never be transformed into a valid operation without a new user authorization.


Suggested closure package

The project can close the main questions with a relatively small public evidence package:

  1. The exact executeBatch call-frame path.
  2. Failed-batch atomicity regression:
    • approve -> reverting swap
    • allowance returns to zero
    • balances unchanged
    • nonce behavior explicit.
  3. Successful-batch symmetric regression.
  4. Account-level failure event/result test where outer receipt remains successful.
  5. ERC-1271 compatibility matrix.
  6. Cross-account and cross-chain replay tests.
  7. Two-device stale-nonce re-sign test.

If those pass against the deployed or release-candidate code, the strongest concern in this review is closed cleanly.

@MobileAuthentication Shipping a P-256 smart account architecture with hardware-backed mobile SDKs on Stylus is a massive infrastructural milestone. The foundational work here is undeniable.

However, the intersection of @MconnectDAO’s and @cxclrfx’s feedback highlights the exact friction point EVM DAOs currently face when evaluating infrastructure grants: The Chasm between Technical Delivery and Capital Efficiency.

@cxclrfx correctly enforces the rigorous technical invariants (nonce finality, execution frame boundaries) required before code can be considered production-safe.
But as @MconnectDAO points out, production-safe code does not automatically equal ecosystem adoption.

Currently, the DAO’s metric for success is “Delivery” (the SDK was built and the tests passed). But to measure true ROI on Domain Allocator funds, the DAO must measure “Impact” (Are downstream developers actually integrating this SDK, and are those integrations generating sustained on-chain activity?).

To cross this chasm, the DAO cannot rely on self-reported developer metrics. It requires Deterministic Telemetry.
To measure the true ecosystem impact of this specific grant over the next 6-12 months, the DAO needs an automated indexing architecture that:

  1. Tracks the specific on-chain signatures or factory deployments associated with this P-256 account implementation.

  2. Cross-references those deployments against 30/60/90-day active user retention to filter out testnet/ghost deployments from genuine mobile adoption.

Excellent foundational delivery by the team. Now, the burden shifts to the DAO to build the on-chain telemetry required to watch this infrastructure scale.

@JulianCross, the direction is correct, but the proposed jump from deployment signatures to 30/60/90-day retention begins one layer too late.

The real transition is not simply:

Technical Delivery
→ Ecosystem Impact

It is:

Funded obligation
→ Immutable source release
→ Reproducible build
→ Deployed artifact and initialization lineage
→ Verified production execution
→ Proven adoption path
→ Independent retained usage
→ Statistically material economic impact
→ Reconciliation against the grant objective

Every arrow is a separate proof boundary.

If those boundaries are compressed into one telemetry dashboard, the DAO can produce perfectly deterministic measurements of the wrong object.

A deployment signature proves that a contract was created. It does not prove that:

  • the deployed bytecode came from the reviewed repository release;
  • the same compiler, optimizer, dependencies, metadata settings, and environment produced it;
  • the deployment used the funded Android or iOS SDK;
  • the P-256 key originated in Secure Enclave, StrongBox, or a TEE;
  • a biometric approval occurred;
  • the deployment was external rather than team-generated;
  • the account was not a test, clone, abandoned instance, or unrelated fork;
  • repeated transactions represent independent users rather than one controller;
  • observed volume is organic rather than circular, subsidized, or temporary;
  • retained activity created measurable value attributable to the grant.

This distinction is especially important here.

RIP-7212 verifies a P-256 signature against a hash and public key. That proves cryptographic validity. It does not prove hardware origin, biometric presence, official-SDK provenance, or independent user identity.

Therefore, counting P-256 accounts, known function selectors, or matching bytecode would still not establish “hardware-backed mobile adoption.” That claim requires a separate provenance layer or a deliberately narrower metric.

The telemetry model should preserve at least five evidence states for every counted object:

ACCEPTED
EXCLUDED_WITH_REASON
DISPUTED
UNRESOLVED
SUPERSEDED

An exclusion must retain its reason, evidence, decision version, and review status. Test deployments, team-controlled deployments, unrelated forks, repeated controllers, and deployments with insufficient provenance should not simply disappear from the denominator.

The same applies to time.

“30 days” must be bound to a defined event: deployment, first successful production execution, or first independently attributable user action. The record must also preserve chain ID, block number, block hash, finality status, and observation window. Otherwise reorgs, delayed indexing, and different starting points can produce incompatible retention results under the same label.

Economic impact also needs a materiality gate. A metric can be measurable and still statistically meaningless. The DAO needs a declared denominator, minimum sample size, baseline or control, confidence range, and negative controls for ghost deployments, repeated-controller activity, wash activity, temporary incentive spikes, and other non-organic routes.

Human review remains part of the system as well. A controller classification, exclusion, or attribution decision must be reviewable and correctable. Deterministic collection does not make an interpretation infallible.

So the immediate next step should not be a broad adoption dashboard.

It should be a bounded qualification and telemetry specification for one frozen SDK release:

exact grant obligation
→ commit hash
→ build manifest
→ WASM / bytecode identity
→ deployment and initialization record
→ qualifying execution
→ provenance status
→ controller classification
→ retention window
→ material impact
→ final reconciliation

Once this chain is fixed, the indexer becomes useful because it measures a defined object. Before that, it merely produces precise numbers around an unstable definition.

The repository is already public, so this does not require another cycle of abstract discussion. One release can be frozen and qualified now. The correct approach is to close the evidence model and production boundaries once, then build telemetry on top of that stable object—rather than repeatedly changing dashboards, counters, and definitions after deployment.

I can map the bounded qualification matrix and evidence chain for this release directly from the public repository, including the acceptance states, exclusions, negative controls, and 30/60/90-day reconciliation structure.

P.S. Before the adoption layer begins, the public report still leaves several production-qualification boundaries open:

  1. The M4 commitment says every template was tested end-to-end, while the report states that the 16 templates were tested against an in-memory simulator. Five real devnode end-to-end tests do not by themselves establish a complete real-route test for every template.

  2. The SDKs serialize nonces per client instance, while the report acknowledges races across two devices, two instances, or a process restart. This needs a global pending/confirmed/reorg-aware nonce policy before multi-device production use.

  3. The account is single-owner. Owner rotation exists, but the public report does not define a recovery path when the device is lost or destroyed before the current key can authorize rotation.

  4. The EIP-1271 path wraps the supplied hash through PersonalSign(bytes32). Representative dApp interoperability should be qualified explicitly so that “EIP-1271 supported” is not assumed to mean universal compatibility.

  5. Interface-compatible action templates still need target-specific execution qualification. A Uniswap-V2-like or Aave-V3-like interface does not prove identical runtime semantics across every router, pool, token behavior, deadline, return path, or failure mode.

  6. The final production artifact should be bound to a frozen commit, deterministic build inputs, WASM hash, deployed code hash, constructor/initialization state, chain configuration, RIP-7212 availability, and contract-size limits.

Those are finite closure tasks. Once they are resolved in the correct order, the project can move from “substantial implementation delivered” to a stable production object whose adoption and economic impact can be measured without ambiguity.

@cxclrfx The distinction between a mere “deployment signature” and a “provenance-backed initialization” is the exact cryptographic rigor this ecosystem lacks. You are entirely correct: tracking an unverified bytecode blob yields deterministic measurements of a ghost.

The telemetry engine must ingest the full Funded obligation → Reproducible build → Deployed artifact lineage before the 30/60/90-day retention clock even begins.

If you are mapping the bounded qualification matrix and evidence chain for this specific SDK release from the public repo, that establishes the perfect foundational schema. I will explicitly integrate your 5-state evidence boundary (ACCEPTED / EXCLUDED_WITH_REASON / DISPUTED / UNRESOLVED / SUPERSEDED) into the ingestion layer of the broader Capital Efficiency Oracles I am architecting for the Token House.

We freeze the object’s provenance first, then we track its economic survival. Flawless framework.

1 Like