bug(web): EVM T&C sign requires injected window.ethereum — fails in Binance Web3 and MetaMask iOS #15

Closed
opened 2026-08-30 14:33:21 +00:00 by PlasticDigits · 6 comments
PlasticDigits commented 2026-08-30 14:33:21 +00:00 (Migrated from gitlab.com)

Summary

Community report (2026-08-30): a mobile user on production https://terms.cl8y.com/sign/evm completed the terms scroll + consent checkbox, then Connect & sign showed:

Error: No EVM wallet found (install MetaMask or similar)

The same error reproduced when the reporter tried Binance Web3 Wallet and again when trying MetaMask on iOS. Screenshot of the hosted portal matches web/src/pages/evm.ts: consent is checked, CTA is enabled, and String(new Error(…)) is rendered in the status region.

This is the EVM analogue of #9 (Terra Classic dead-ended on Keplr extension not found in mobile Chrome). Terra now has injected multi-wallet + Open in Keplr + in-page WalletConnect (#11). EVM still has none of that.

Safari / Chrome / Firefox on a phone do not inject an EIP-1193 provider. MetaMask iOS and Binance Web3 only inject inside their in-app dApp browsers (or via WalletConnect). The portal currently treats a missing window.ethereum as a hard failure and tells the user to install a desktop extension.

Workaround that may work today: paste the portal URL into MetaMask or Binance Web3’s in-app browser. Retail users opening sign_urls.evm from an integrator in the system browser never get that instruction — they only get the error.


Current codebase

Portal EVM sign (web/src/pages/evm.ts):

  • Idle copy: “Connect your wallet to sign.”
  • onSign starts with if (!window.ethereum) throw new Error("No EVM wallet found (install MetaMask or similar)");
  • Then createWalletClient({ chain: mainnet, transport: custom(window.ethereum) }), requestAddresses(), signMessage (EIP-191 personal_sign via viem).
  • Submit goes to POST /api/v1/signatures/wallet with network: "EVM". Server verifies EIP-191 recovery in api/src/verify/evm.rs.
  • renderSignShell is used; extraControls is omitted. Comments in web/src/signShell.ts and skills/portal-sign-disclosure/SKILL.md currently say EVM must omit extraControls (Terra-only Keplr / WC).

There is no:

  • EIP-6963 multi-provider discovery (eip6963:announceProvider)
  • Fallback to window.ethereum.providers[], window.BinanceChain, or delayed ethereum#initialized
  • Open-in-MetaMask / Open-in-Binance-Web3 / Copy-link CTA
  • In-page WalletConnect personal_sign for EVM
  • Binding of query account=0x… (getClaimedAccount exists but only Terra uses it)

Playwright EVM coverage (web/e2e/helpers/evm-wallet.ts, web/e2e/evm-sign.spec.ts) injects window.ethereum up front. web/e2e/sign-pages.spec.ts asserts EVM has no Open in Keplr — it does not assert a missing-provider CTA. The missing-wallet path is untested.

Integrators (@plasticdigits/cl8y-clickwrap TermsGate) only redirect to sign_urls.evm. They do not implement EVM connect. Every CL8Y property that sends EVM users to this portal inherits the dead-end.

Gap analysis already lists this: gaps/GAP_1786322222.md items 10–11 (WalletConnect / EIP-6963 / mobile deep links) — “other networks still extension-only.”


Why this is needed

  • EVM retail users are on mobile. iOS Safari cannot install MetaMask as an extension. Android Chrome cannot use a desktop extension either.
  • Reported wallets (Binance Web3, MetaMask iOS) are in-app-browser or WalletConnect wallets. The portal never offers those paths.
  • Ecosystem clickwrap is property-scoped. Every property that uses sign_urls.evm strands phone users after they already read and agreed to the terms.
  • Terra #9 / #11 already established the product rule: missing injected provider is not a dead-end. EVM still violates it.
  • Even inside a wallet WebView, window.ethereum can be late, EIP-6963-only, or exposed as window.BinanceChain / ethereum.providers. A single synchronous window.ethereum check fails those environments too.

Constraints / guardrails

  1. Do not weaken EIP-191 verify in api/src/verify/evm.rs. WalletConnect / in-app browsers must still produce a 65-byte personal_sign the existing verifier accepts (recovered address = claimed account_id). No eth_sign of a raw hash. No silent switch to EIP-712 / SIWE without a versioned API change and golden tests.
  2. Do not change the canonical legal message without coordinated Rust + SDK golden tests (api/src/message.rs, packages/cl8y-clickwrap/src/message.ts). Messages must keep Content-SHA256 (#6).
  3. Network remains EVM. Do not retarget a specific L2 chain id for the legal message; personal_sign is chain-agnostic. createWalletClient({ chain: mainnet }) is only a viem default — do not require the wallet to be on Ethereum mainnet if that blocks Binance / other EVM wallets from signing an off-chain message.
  4. Property isolation unchanged: a signature for property A must not satisfy property B.
  5. No admin tokens / WalletConnect Cloud secrets in git. Reuse Legal-owned VITE_WC_PROJECT_ID (already required for Galaxy Station). Do not copy DEX / ustr-cmm Cloud ids.
  6. Address binding: recovered signer must match account_id (lowercase 0x + 40 hex). If the integrator passes account=0x… on the query string (getClaimedAccount / SDK buildSignUrl({ account })), a different connected address is a hard fail (Terra #11 continuity).
  7. Redirect allowlists stay authoritative (web/src/redirect.ts, #3). Deep-link / WC return URLs must encode this portal sign URL (origin + path + search), never query-supplied redirect_uri. Unknown schemes fail closed.
  8. Deep-link allowlist (same spirit as isAllowedWalletConnectDeepLink for Terra): only documented MetaMask / Binance Web3 / wc: / https: portal URLs. No javascript:, no arbitrary https:// from wallet payloads.
  9. User-gesture navigation only for app opens (no async location.href after an await). Copy-link copies the portal URL (or raw wc: URI for pairing), not a third-party wrapper that can drop the query string.
  10. Consent gate unchanged (#2 / skills/portal-sign-disclosure/SKILL.md): full terms on-page, scroll-to-bottom, checkbox, then wallet. Wallet APIs must not run before consent. signShell must still reset busy on throw and early return (#10).
  11. Safe render: terms and status stay text nodes. Do not innerHTML wallet names, pairing URIs, or query values.
  12. Fail closed: unknown / missing signer ≠ signed_latest. Client must not skip POST /signatures/wallet.
  13. Do not ask phones to install a desktop browser extension. Idle / error copy must be retail-short.
  14. Keep Terra / Solana / Telegram sign pages unchanged except sharing tiny helpers (copy-link, origin-safe URL builder) if that avoids duplication. Do not mount Keplr CTAs on EVM.
  15. Update the “EVM must omit extraControls” invariant in signShell.ts comments and skills/portal-sign-disclosure/SKILL.md when EVM gains CTAs — that line is now wrong, not a reason to skip the fix.
  16. No RainbowKit / wagmi app-wide modal unless it is a thin, audited adapter that still hits the same personal_sign + existing submit path. Prefer the Terra pattern: small portal-owned modules, mockable in Playwright.

Relevant files

Area Path
Portal EVM page web/src/pages/evm.ts
Sign shell / UI web/src/signShell.ts, web/src/ui.ts
Query (account, redirect_uri) web/src/query.ts
Redirect allowlist web/src/redirect.ts
Terra pattern to mirror (not copy blindly) web/src/keplrMobile.ts, web/src/keplrMobileUi.ts, web/src/terra/walletConnect*.ts, web/src/terra/injected.ts
API EVM verify api/src/verify/evm.rs
Wallet submit api/src/routes/signatures.rs
Message builders api/src/message.rs, packages/cl8y-clickwrap/src/message.ts
SDK / TermsGate (redirect only) packages/cl8y-clickwrap
Disclosure skill (update EVM extraControls note) skills/portal-sign-disclosure/SKILL.md
Testing skill skills/testing-coverage/SKILL.md
E2E web/e2e/evm-sign.spec.ts, web/e2e/sign-pages.spec.ts, web/e2e/helpers/evm-wallet.ts
Gap list gaps/GAP_1786322222.md items 10–11

Stage them on one /sign/evm change (do not split MetaMask vs Binance — same page, same error):

  1. Provider discovery (fixes in-app browsers that already inject): resolve an EIP-1193 provider from, in order, EIP-6963 announcements, window.ethereum.providers, window.ethereum, window.BinanceChain. Wait briefly / listen for eip6963:announceProvider and ethereum#initialized so late MetaMask iOS / Binance WebView injection is not a false miss. If several providers announce, let the user pick (rdns / name); do not silently sign with a hidden injected wallet.
  2. Missing-provider UX (fixes system Safari/Chrome): do not throw No EVM wallet found (install MetaMask or similar) as the only UI. Show Open in MetaMask, Open in Binance Web3, and Copy link, using documented universal / dapp-browser links whose url is the current portal sign page (query string intact). Idle copy should say the wallet app browser can sign; Chrome/Safari cannot use a desktop extension. If a documented Binance dapp-browser URL cannot be confirmed, still ship Copy-link + MetaMask link rather than inventing a scheme.
  3. In-page WalletConnect (Chrome/Safari without switching browsers): personal_sign of the canonical legal UTF-8 message via Legal VITE_WC_PROJECT_ID. Mobile is not QR-only: Open MetaMask / Open Binance Web3 / Copy pairing link (wc: URI), user-gesture <a href> only, scheme allowlist. Hide WC when the project id is unset (same as Galaxy Station). Pairing hrefs must not encode query-supplied redirect_uri.
  4. Desktop extension path stays: current Playwright mock window.ethereum + MetaMask Chrome extension must keep working.

Do not tell Android/iOS users to install the MetaMask Chrome extension.


Acceptance criteria

  • Mobile Safari / Chrome: opening /sign/evm?property=… never dead-ends on No EVM wallet found (install MetaMask or similar) without a working next step (Open in MetaMask and/or Open in Binance Web3 and/or in-page WalletConnect + Copy link).
  • Happy path A: user produces signed_latest: true for (property, EVM, 0x…) from MetaMask iOS in-app browser (injected or EIP-6963).
  • Happy path B: user produces signed_latest: true from Binance Web3 in-app browser (injected ethereum / BinanceChain / EIP-6963).
  • Happy path C (required Chrome/Safari-viable path): user stays in the system browser, approves in MetaMask or Binance Web3 via WalletConnect or Open-in-app, status signed_latest: true.
  • Desktop injected MetaMask / mock window.ethereum path still works (existing e2e).
  • Consent gate still blocks wallet APIs until terms are scrolled and the checkbox is checked.
  • API still rejects wrong recovered address, wrong property/version/content hash, tampered message, bad timestamp, oversized body.
  • If account=0x… is on the query string, a different connected address fails closed (no signed_latest for the claimed account).
  • Terra / Solana / Telegram sign flows unchanged (EVM still must not show Open in Keplr).
  • Copy is retail-short (no “EIP-191” / “EIP-6963” in idle status).
  • skills/portal-sign-disclosure/SKILL.md and signShell.ts comments no longer say EVM must omit all extraControls if EVM CTAs ship.

Test plan (functional paths)

  1. Desktop Chromium + injected window.ethereum: existing evm-sign.spec.ts happy path (regression) + redirect.spec.ts allowlist.
  2. No provider: page shows Open-in-MetaMask / Open-in-Binance / Copy-link (and/or WC pairing); Connect & sign does not leave a raw Error: No EVM wallet found… as the only UI; button re-enables (#10).
  3. EIP-6963 mock: provider announced via eip6963:announceProvider with no window.ethereum → Connect & sign still requests accounts and personal_signs the canonical message.
  4. Late inject: window.ethereum appears after ethereum#initialized (or after a short delay) → no false miss if the user waits / retries.
  5. window.BinanceChain only: treated as EIP-1193-compatible when request exists.
  6. Multiple providers: picker / explicit choice; must not sign with a non-selected provider.
  7. Open-in-app links: href is origin-safe portal URL (property / redirect_uri / app_name / account preserved); javascript: and foreign origins rejected; query redirect_uri is not the deeplink target.
  8. Copy link: copies portal https://…/sign/evm?…, not a wallet host.
  9. WalletConnect mock (if WC ships): pairing sheet Open {wallet} + Copy wc:; personal_sign of legal message → submit → signed_latest: true. Unset VITE_WC_PROJECT_ID hides Galaxy-style WC (fail closed, not a thrown Cloud error).
  10. Already signed: getStatus short-circuit still shows success without a second signature.
  11. Property isolation: signature for property A does not satisfy property B.
  12. Claimed account mismatch: account=0xaaa… with connected 0xbbb… fails closed.
  13. Consent: wallet request / WC connect must not run while checkbox is unchecked.
  14. Playwright: keep Chromium-only CI (#4); mock providers — no mandatory real MetaMask/Binance app job.
  15. Unit: provider resolver, deeplink builder (allowlist, origin match), claimed-account compare (checksum vs lowercase).

Test plan (attack / abuse / hack vectors)

Vector Expectation
Replay sig for another property / version_label / content_sha256 / account Reject (message bind, existing API tests)
Submit EIP-191 sig for message A claiming message B Reject
Signature recovers address A, account_id address B Reject
Query account=0x… ≠ connected wallet Client hard-fail; no submit for the claimed account
Deep-link / WC return URL uses redirect_uri or a non-portal origin No open redirect; builder returns null / non-navigable
javascript:, data:, unknown schemes in Open-in-app / pairing href Rejected by allowlist
WalletConnect payload supplies https://evil.example/… as pairing URL Not opened (isAllowed… equivalent)
Fake window.ethereum / EIP-6963 announce in page (XSS / malicious extension) CSP + server-side EIP-191 verify still required; client must not skip submit/verify
WC session for a different account than account_id Address mismatch → reject
eth_sign of prehash / EIP-712 typed data instead of personal_sign of legal UTF-8 API reject (recovery / encoding)
Extremely large signature / WC JSON body Reject or 413 (MAX_REQUEST_BODY_BYTES)
Timestamp skew outside window Reject
Consent bypass (enable CTA via DOM, skip checkbox) Wallet may be invoked client-side; acceptance still requires valid server verify — do not treat checkbox as auth
Phishing copy that sends users to a lookalike wallet download Use official documented store / universal links only; no third-party APK URLs
WC project id leaked in client bundle Expected for Reown; still must not commit other secrets; rotate via env rebuild

Verification criteria

  • Manual: iOS Safari (no inject) against staging or prod portal for a test property: does not dead-end; Open in MetaMask and/or WalletConnect completes signed_latest: true.
  • Manual: MetaMask iOS in-app browser: Connect & sign succeeds (injected / EIP-6963).
  • Manual: Binance Web3 in-app browser: Connect & sign succeeds.
  • Manual: desktop MetaMask extension still signs.
  • cd api && cargo test EVM verify + wallet integration tests remain green.
  • cd web && npm test + Playwright evm-sign / sign-pages / new missing-provider specs green (5 workers).
  • Integrator sites that redirect to sign_urls.evm no longer strand mobile system-browser users on this error.
  • Disclosure skill + signShell comments updated so the next agent does not strip EVM CTAs as “out of scope.”
## Summary Community report (2026-08-30): a mobile user on production `https://terms.cl8y.com/sign/evm` completed the terms scroll + consent checkbox, then **Connect & sign** showed: `Error: No EVM wallet found (install MetaMask or similar)` The same error reproduced when the reporter tried **Binance Web3 Wallet** and again when trying **MetaMask on iOS**. Screenshot of the hosted portal matches `web/src/pages/evm.ts`: consent is checked, CTA is enabled, and `String(new Error(…))` is rendered in the status region. This is the **EVM analogue of [#9](https://gitlab.com/plasticdigits/cl8y-ecosystem-legal/-/issues/9)** (Terra Classic dead-ended on `Keplr extension not found` in mobile Chrome). Terra now has injected multi-wallet + Open in Keplr + in-page WalletConnect ([#11](https://gitlab.com/plasticdigits/cl8y-ecosystem-legal/-/issues/11)). **EVM still has none of that.** Safari / Chrome / Firefox on a phone do not inject an EIP-1193 provider. MetaMask iOS and Binance Web3 only inject inside their **in-app dApp browsers** (or via WalletConnect). The portal currently treats a missing `window.ethereum` as a hard failure and tells the user to install a **desktop** extension. Workaround that may work today: paste the portal URL into MetaMask or Binance Web3’s in-app browser. Retail users opening `sign_urls.evm` from an integrator in the system browser never get that instruction — they only get the error. --- ## Current codebase Portal EVM sign (`web/src/pages/evm.ts`): - Idle copy: “Connect your wallet to sign.” - `onSign` starts with `if (!window.ethereum) throw new Error("No EVM wallet found (install MetaMask or similar)");` - Then `createWalletClient({ chain: mainnet, transport: custom(window.ethereum) })`, `requestAddresses()`, `signMessage` (EIP-191 `personal_sign` via viem). - Submit goes to `POST /api/v1/signatures/wallet` with `network: "EVM"`. Server verifies EIP-191 recovery in `api/src/verify/evm.rs`. - `renderSignShell` is used; **`extraControls` is omitted**. Comments in `web/src/signShell.ts` and `skills/portal-sign-disclosure/SKILL.md` currently say **EVM must omit extraControls** (Terra-only Keplr / WC). There is **no**: - EIP-6963 multi-provider discovery (`eip6963:announceProvider`) - Fallback to `window.ethereum.providers[]`, `window.BinanceChain`, or delayed `ethereum#initialized` - Open-in-MetaMask / Open-in-Binance-Web3 / Copy-link CTA - In-page WalletConnect `personal_sign` for EVM - Binding of query `account=0x…` (`getClaimedAccount` exists but only Terra uses it) Playwright EVM coverage (`web/e2e/helpers/evm-wallet.ts`, `web/e2e/evm-sign.spec.ts`) **injects `window.ethereum` up front**. `web/e2e/sign-pages.spec.ts` asserts EVM has **no** Open in Keplr — it does **not** assert a missing-provider CTA. The missing-wallet path is untested. Integrators (`@plasticdigits/cl8y-clickwrap` `TermsGate`) only redirect to `sign_urls.evm`. They do not implement EVM connect. Every CL8Y property that sends EVM users to this portal inherits the dead-end. Gap analysis already lists this: `gaps/GAP_1786322222.md` items 10–11 (WalletConnect / EIP-6963 / mobile deep links) — “other networks still extension-only.” --- ## Why this is needed - EVM retail users are on **mobile**. iOS Safari cannot install MetaMask as an extension. Android Chrome cannot use a desktop extension either. - Reported wallets (**Binance Web3**, **MetaMask iOS**) are in-app-browser or WalletConnect wallets. The portal never offers those paths. - Ecosystem clickwrap is property-scoped. Every property that uses `sign_urls.evm` strands phone users after they already read and agreed to the terms. - Terra [#9](https://gitlab.com/plasticdigits/cl8y-ecosystem-legal/-/issues/9) / [#11](https://gitlab.com/plasticdigits/cl8y-ecosystem-legal/-/issues/11) already established the product rule: **missing injected provider is not a dead-end**. EVM still violates it. - Even **inside** a wallet WebView, `window.ethereum` can be late, EIP-6963-only, or exposed as `window.BinanceChain` / `ethereum.providers`. A single synchronous `window.ethereum` check fails those environments too. --- ## Constraints / guardrails 1. **Do not weaken EIP-191 verify** in `api/src/verify/evm.rs`. WalletConnect / in-app browsers must still produce a 65-byte `personal_sign` the existing verifier accepts (recovered address = claimed `account_id`). No `eth_sign` of a raw hash. No silent switch to EIP-712 / SIWE without a versioned API change and golden tests. 2. **Do not change the canonical legal message** without coordinated Rust + SDK golden tests (`api/src/message.rs`, `packages/cl8y-clickwrap/src/message.ts`). Messages must keep `Content-SHA256` ([#6](https://gitlab.com/plasticdigits/cl8y-ecosystem-legal/-/issues/6)). 3. **Network remains `EVM`.** Do not retarget a specific L2 chain id for the legal message; `personal_sign` is chain-agnostic. `createWalletClient({ chain: mainnet })` is only a viem default — do not require the wallet to be on Ethereum mainnet if that blocks Binance / other EVM wallets from signing an off-chain message. 4. **Property isolation** unchanged: a signature for property A must not satisfy property B. 5. **No admin tokens / WalletConnect Cloud secrets in git.** Reuse Legal-owned `VITE_WC_PROJECT_ID` (already required for Galaxy Station). Do not copy DEX / ustr-cmm Cloud ids. 6. **Address binding:** recovered signer must match `account_id` (lowercase `0x` + 40 hex). If the integrator passes `account=0x…` on the query string (`getClaimedAccount` / SDK `buildSignUrl({ account })`), a different connected address is a **hard fail** (Terra #11 continuity). 7. **Redirect allowlists** stay authoritative (`web/src/redirect.ts`, [#3](https://gitlab.com/plasticdigits/cl8y-ecosystem-legal/-/issues/3)). Deep-link / WC return URLs must encode **this** portal sign URL (`origin + path + search`), never query-supplied `redirect_uri`. Unknown schemes fail closed. 8. **Deep-link allowlist** (same spirit as `isAllowedWalletConnectDeepLink` for Terra): only documented MetaMask / Binance Web3 / `wc:` / `https:` portal URLs. No `javascript:`, no arbitrary `https://` from wallet payloads. 9. **User-gesture navigation only** for app opens (no async `location.href` after an await). Copy-link copies the **portal** URL (or raw `wc:` URI for pairing), not a third-party wrapper that can drop the query string. 10. **Consent gate unchanged** ([#2](https://gitlab.com/plasticdigits/cl8y-ecosystem-legal/-/issues/2) / `skills/portal-sign-disclosure/SKILL.md`): full terms on-page, scroll-to-bottom, checkbox, then wallet. Wallet APIs must not run before consent. `signShell` must still reset `busy` on throw **and** early return ([#10](https://gitlab.com/plasticdigits/cl8y-ecosystem-legal/-/issues/10)). 11. **Safe render:** terms and status stay text nodes. Do not `innerHTML` wallet names, pairing URIs, or query values. 12. **Fail closed:** unknown / missing signer ≠ `signed_latest`. Client must not skip `POST /signatures/wallet`. 13. **Do not ask phones to install a desktop browser extension.** Idle / error copy must be retail-short. 14. **Keep Terra / Solana / Telegram sign pages unchanged** except sharing tiny helpers (copy-link, origin-safe URL builder) if that avoids duplication. Do not mount Keplr CTAs on EVM. 15. **Update the “EVM must omit extraControls” invariant** in `signShell.ts` comments and `skills/portal-sign-disclosure/SKILL.md` when EVM gains CTAs — that line is now wrong, not a reason to skip the fix. 16. **No RainbowKit / wagmi app-wide modal** unless it is a thin, audited adapter that still hits the same `personal_sign` + existing submit path. Prefer the Terra pattern: small portal-owned modules, mockable in Playwright. --- ## Relevant files | Area | Path | |------|------| | Portal EVM page | `web/src/pages/evm.ts` | | Sign shell / UI | `web/src/signShell.ts`, `web/src/ui.ts` | | Query (`account`, `redirect_uri`) | `web/src/query.ts` | | Redirect allowlist | `web/src/redirect.ts` | | Terra pattern to mirror (not copy blindly) | `web/src/keplrMobile.ts`, `web/src/keplrMobileUi.ts`, `web/src/terra/walletConnect*.ts`, `web/src/terra/injected.ts` | | API EVM verify | `api/src/verify/evm.rs` | | Wallet submit | `api/src/routes/signatures.rs` | | Message builders | `api/src/message.rs`, `packages/cl8y-clickwrap/src/message.ts` | | SDK / TermsGate (redirect only) | `packages/cl8y-clickwrap` | | Disclosure skill (update EVM extraControls note) | `skills/portal-sign-disclosure/SKILL.md` | | Testing skill | `skills/testing-coverage/SKILL.md` | | E2E | `web/e2e/evm-sign.spec.ts`, `web/e2e/sign-pages.spec.ts`, `web/e2e/helpers/evm-wallet.ts` | | Gap list | `gaps/GAP_1786322222.md` items 10–11 | --- ## Recommended direction Stage them on **one** `/sign/evm` change (do not split MetaMask vs Binance — same page, same error): 1. **Provider discovery (fixes in-app browsers that already inject):** resolve an EIP-1193 provider from, in order, EIP-6963 announcements, `window.ethereum.providers`, `window.ethereum`, `window.BinanceChain`. Wait briefly / listen for `eip6963:announceProvider` and `ethereum#initialized` so late MetaMask iOS / Binance WebView injection is not a false miss. If several providers announce, let the user pick (rdns / name); do not silently sign with a hidden injected wallet. 2. **Missing-provider UX (fixes system Safari/Chrome):** do **not** throw `No EVM wallet found (install MetaMask or similar)` as the only UI. Show **Open in MetaMask**, **Open in Binance Web3**, and **Copy link**, using documented universal / dapp-browser links whose `url` is the current portal sign page (query string intact). Idle copy should say the wallet **app browser** can sign; Chrome/Safari cannot use a desktop extension. If a documented Binance dapp-browser URL cannot be confirmed, still ship Copy-link + MetaMask link rather than inventing a scheme. 3. **In-page WalletConnect (Chrome/Safari without switching browsers):** `personal_sign` of the canonical legal UTF-8 message via Legal `VITE_WC_PROJECT_ID`. Mobile is **not QR-only**: **Open MetaMask** / **Open Binance Web3** / **Copy pairing link** (`wc:` URI), user-gesture `<a href>` only, scheme allowlist. Hide WC when the project id is unset (same as Galaxy Station). Pairing hrefs must not encode query-supplied `redirect_uri`. 4. **Desktop extension path stays:** current Playwright mock `window.ethereum` + MetaMask Chrome extension must keep working. Do **not** tell Android/iOS users to install the MetaMask Chrome extension. --- ## Acceptance criteria - [ ] Mobile Safari / Chrome: opening `/sign/evm?property=…` never dead-ends on `No EVM wallet found (install MetaMask or similar)` without a working next step (Open in MetaMask and/or Open in Binance Web3 and/or in-page WalletConnect + Copy link). - [ ] Happy path A: user produces `signed_latest: true` for `(property, EVM, 0x…)` from **MetaMask iOS in-app browser** (injected or EIP-6963). - [ ] Happy path B: user produces `signed_latest: true` from **Binance Web3** in-app browser (injected `ethereum` / `BinanceChain` / EIP-6963). - [ ] Happy path C (required Chrome/Safari-viable path): user stays in the system browser, approves in MetaMask **or** Binance Web3 via WalletConnect or Open-in-app, status `signed_latest: true`. - [ ] Desktop injected MetaMask / mock `window.ethereum` path still works (existing e2e). - [ ] Consent gate still blocks wallet APIs until terms are scrolled and the checkbox is checked. - [ ] API still rejects wrong recovered address, wrong property/version/content hash, tampered message, bad timestamp, oversized body. - [ ] If `account=0x…` is on the query string, a different connected address fails closed (no `signed_latest` for the claimed account). - [ ] Terra / Solana / Telegram sign flows unchanged (EVM still must not show Open in Keplr). - [ ] Copy is retail-short (no “EIP-191” / “EIP-6963” in idle status). - [ ] `skills/portal-sign-disclosure/SKILL.md` and `signShell.ts` comments no longer say EVM must omit all extraControls if EVM CTAs ship. --- ## Test plan (functional paths) 1. **Desktop Chromium + injected `window.ethereum`:** existing `evm-sign.spec.ts` happy path (regression) + `redirect.spec.ts` allowlist. 2. **No provider:** page shows Open-in-MetaMask / Open-in-Binance / Copy-link (and/or WC pairing); Connect & sign does **not** leave a raw `Error: No EVM wallet found…` as the only UI; button re-enables ([#10](https://gitlab.com/plasticdigits/cl8y-ecosystem-legal/-/issues/10)). 3. **EIP-6963 mock:** provider announced via `eip6963:announceProvider` with **no** `window.ethereum` → Connect & sign still requests accounts and `personal_sign`s the canonical message. 4. **Late inject:** `window.ethereum` appears after `ethereum#initialized` (or after a short delay) → no false miss if the user waits / retries. 5. **`window.BinanceChain` only:** treated as EIP-1193-compatible when `request` exists. 6. **Multiple providers:** picker / explicit choice; must not sign with a non-selected provider. 7. **Open-in-app links:** href is origin-safe portal URL (property / redirect_uri / app_name / account preserved); `javascript:` and foreign origins rejected; query `redirect_uri` is **not** the deeplink target. 8. **Copy link:** copies portal `https://…/sign/evm?…`, not a wallet host. 9. **WalletConnect mock (if WC ships):** pairing sheet Open {wallet} + Copy `wc:`; `personal_sign` of legal message → submit → `signed_latest: true`. Unset `VITE_WC_PROJECT_ID` hides Galaxy-style WC (fail closed, not a thrown Cloud error). 10. **Already signed:** `getStatus` short-circuit still shows success without a second signature. 11. **Property isolation:** signature for property A does not satisfy property B. 12. **Claimed account mismatch:** `account=0xaaa…` with connected `0xbbb…` fails closed. 13. **Consent:** wallet `request` / WC connect must not run while checkbox is unchecked. 14. **Playwright:** keep Chromium-only CI ([#4](https://gitlab.com/plasticdigits/cl8y-ecosystem-legal/-/issues/4)); mock providers — no mandatory real MetaMask/Binance app job. 15. **Unit:** provider resolver, deeplink builder (allowlist, origin match), claimed-account compare (checksum vs lowercase). --- ## Test plan (attack / abuse / hack vectors) | Vector | Expectation | |--------|-------------| | Replay sig for another `property` / `version_label` / `content_sha256` / account | Reject (message bind, existing API tests) | | Submit EIP-191 sig for message A claiming message B | Reject | | Signature recovers address A, `account_id` address B | Reject | | Query `account=0x…` ≠ connected wallet | Client hard-fail; no submit for the claimed account | | Deep-link / WC return URL uses `redirect_uri` or a non-portal origin | No open redirect; builder returns null / non-navigable | | `javascript:`, `data:`, unknown schemes in Open-in-app / pairing href | Rejected by allowlist | | WalletConnect payload supplies `https://evil.example/…` as pairing URL | Not opened (`isAllowed…` equivalent) | | Fake `window.ethereum` / EIP-6963 announce in page (XSS / malicious extension) | CSP + **server-side** EIP-191 verify still required; client must not skip submit/verify | | WC session for a different account than `account_id` | Address mismatch → reject | | `eth_sign` of prehash / EIP-712 typed data instead of `personal_sign` of legal UTF-8 | API reject (recovery / encoding) | | Extremely large signature / WC JSON body | Reject or 413 (`MAX_REQUEST_BODY_BYTES`) | | Timestamp skew outside window | Reject | | Consent bypass (enable CTA via DOM, skip checkbox) | Wallet may be invoked client-side; **acceptance still requires valid server verify** — do not treat checkbox as auth | | Phishing copy that sends users to a lookalike wallet download | Use official documented store / universal links only; no third-party APK URLs | | WC project id leaked in client bundle | Expected for Reown; still must not commit other secrets; rotate via env rebuild | --- ## Verification criteria - Manual: iOS Safari (no inject) against staging or prod portal for a **test property**: does not dead-end; Open in MetaMask and/or WalletConnect completes `signed_latest: true`. - Manual: MetaMask iOS in-app browser: Connect & sign succeeds (injected / EIP-6963). - Manual: Binance Web3 in-app browser: Connect & sign succeeds. - Manual: desktop MetaMask extension still signs. - `cd api && cargo test` EVM verify + wallet integration tests remain green. - `cd web && npm test` + Playwright `evm-sign` / `sign-pages` / new missing-provider specs green (5 workers). - Integrator sites that redirect to `sign_urls.evm` no longer strand mobile system-browser users on this error. - Disclosure skill + signShell comments updated so the next agent does not strip EVM CTAs as “out of scope.”
PlasticDigits commented 2026-08-30 16:01:41 +00:00 (Migrated from gitlab.com)

mentioned in commit e455f1a9d8

mentioned in commit e455f1a9d85d8ffc7b4fc8a75ed88fcc496f5475
PlasticDigits commented 2026-08-30 16:01:44 +00:00 (Migrated from gitlab.com)

mentioned in merge request !32

mentioned in merge request !32
PlasticDigits commented 2026-08-30 17:41:15 +00:00 (Migrated from gitlab.com)

mentioned in commit 72c45a73d5

mentioned in commit 72c45a73d559efadb0969224e8b420a20e43e55b
PlasticDigits (Migrated from gitlab.com) closed this issue 2026-08-30 17:41:16 +00:00
PlasticDigits commented 2026-08-30 17:56:40 +00:00 (Migrated from gitlab.com)

mentioned in merge request voting!28

mentioned in merge request voting!28
PlasticDigits commented 2026-08-31 04:12:25 +00:00 (Migrated from gitlab.com)

marked as related to #16

marked as related to #16
PlasticDigits commented 2026-08-31 04:41:31 +00:00 (Migrated from gitlab.com)

mentioned in merge request !33

mentioned in merge request !33
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
code/cl8y-ecosystem-legal#15
No description provided.