follow-up(!15): EVM account= continuity — matching path, WC mismatch, deeplink proof #16
Labels
No labels
agent:implement
agent:ready
api
bot
bug
ci
enhancement
ready
security
terra-classic
testing
ux
web
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
code/cl8y-ecosystem-legal#16
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Integrator report after
@plasticdigits/cl8y-clickwrap0.1.1: voting Accept now putsaccount=0x…on/sign/evm. Terra Classic already bound that query (assertAccountContinuity). The report assumed EVM still signed whatever wallet connected.That bind is already on
mainand in the currentterms.cl8y.combundle (GitLab #15 / !33):getClaimedAccount()→assertEvmAccountContinuityon injected and WalletConnect, mismatch copy This page is for a different wallet,Sign as 0x…as a text node. Do not reimplement the helper.What is still missing is proof of the path voting actually uses (matching
account=+ same wallet → accept) and WC / deeplink / hostile-query coverage. Mismatch e2e exists; the happy path does not. A WalletConnect session for a different address is only covered on the injected mock.Current codebase
web/src/query.tsgetClaimedAccount()readsaccount, trim, empty → null. Never a redirect target.web/src/pages/evm.tsshowsSign as ${claimedAccount}viael()text nodes; passesclaimedAccountintosignEvmMessage.web/src/evm/account.tscanonicalizeEvmAddress= lowercase0x+ 40 hex (lockstep APInormalize_accountEVM). Mismatch or invalid claimed →EVM_ACCOUNT_MISMATCH. No claim → pass through connected address.web/src/evm/sign.tsassert beforeprepare/personal_sign.web/src/evm/walletConnect.tsassert on session address (and e2e hook) beforepersonal_sign.web/src/evm/deeplink.tsencodes current portalorigin+path+search(soaccount=stays). Never encode queryredirect_urias the Open-in-app target.api/src/verify/evm.rs+api/src/account.rs: recovered signer must match submittedaccount_id. Queryaccount=is portal UX; the API does not see it.TermsGateAccept +buildSignUrl({ account })in@plasticdigits/cl8y-clickwrap0.1.1 (GitLab npm).web/src/terra/chain.tsassertAccountContinuity; e2e mismatch inweb/e2e/terra-sign.spec.ts.Tests that exist
web/src/evm/account.test.ts, injected mismatch inweb/src/evm/sign.test.ts, deeplink preservesaccount=inweb/src/evm/deeplink.test.ts.web/e2e/evm-sign.spec.tsmismatch only (account=0xaaa…vs mocktestEvmAccount). Missing-provider Open in MetaMask case does not putaccount=on the page, so href preservation is unproven in Playwright.Tests that do not exist
account=equals connected wallet → Accepted +signed_latestfor that address (the voting Accept path).account=when it is on the page.connectAndSignEvmWalletConnectmismatch (web/src/evm/walletConnect.test.tsonly gates project id / e2e hook).account=(javascript:,terra1…, too short) → fail closed, not used ashref.Skills already mention invariant 13 in
skills/portal-sign-disclosure/SKILL.md. Keep them in lockstep.Why this follow-up is needed
Voting (and other integrators) now send
account=on Accept, not only on Open-in-MetaMask hints. If the matching path regresses, users get a false Accepted for a different0x…than the dApp polls, then return still unsigned — or, worse, a future change skips the assert and records the wrong account’s terms while the UI still says Sign as <claimed>.Mismatch-only tests do not prove the 0.1.1 integrator contract. WC is a second signer path that can skip the injected assert if someone wires
preparewithoutassertEvmAccountContinuity.Constraints / guardrails
assertEvmAccountContinuity/ a second checksum scheme. Extend tests and any thin wiring holes only.verify/evm.rs) remains source of truth. Do not trust queryaccountinPOST /signatures/wallet.accountis not a URL. Never pass it tolocation,<a href>, WalletConnect pairing, or Open-in-app targets except as an existing query key on the portal sign URL.Sign as …(el()/textContent). NoinnerHTMLof query values.0x+40 hex only. EIP-55 vs lower must succeed; mixed/invalid claimed must fail closed (same message is OK).account_id).wallet_switchEthereumChainto Ethereum mainnet as part of this work (personal_signis chain-agnostic).redirect_uriinto MetaMask/Binance deeplinks./sign/solanastill ignoresaccount=(postponed with #2 / #4). Telegram uses Telegram ids, not EVM0x. Do not expand this issue to those pages.Relevant files
web/src/pages/evm.tsweb/src/evm/account.ts+account.test.tsweb/src/evm/sign.ts+sign.test.tsweb/src/evm/walletConnect.ts+walletConnect.test.tsweb/src/evm/deeplink.ts+deeplinkUi.tsweb/src/query.tsweb/e2e/evm-sign.spec.ts+web/e2e/helpers/evm-wallet.tsapi/src/verify/evm.rs,api/src/account.rspackages/cl8y-clickwrap/src/react/TermsGate.tsx,packages/cl8y-clickwrap/src/urls.tsskills/portal-sign-disclosure/SKILL.md,skills/testing-coverage/SKILL.mdRecommended direction
goto /sign/evm?property=cl8y.com&account=<mockChecksumOrLower>, consent, accept → heading Accepted,GET .../status?...&account=<lower>signed_latest: true. Assert Sign as is visible. Use the same address the mock will connect (EIP-55 on the query is a plus).account=other0x→ different wallet, no Accepted, claimed address remains unsigned.account=0x…; Open in MetaMask href contains that account; Open in Binanceurl=contains it; Copy link is the portal URL (notredirect_uri, notjavascript:).EVM_ACCOUNT_MISMATCHand does not callpersonal_sign. Invalid claimed (javascript:alert(1),terra1…,0x123) throws;Sign asstill text-only if rendered.skills/testing-coverage/SKILL.mdrow forevm-sign.spec.tsupdated (matching + WC mismatch + deeplinkaccount=)./sign/evm?property=…&account=0x…still shows Sign as and refuses a different injected account (bundle already contains the mismatch string).Acceptance criteria
account=+ same injected mock → accept records that account, not a sibling provider.account=from the current portal search; they never useaccountas an href target.accountquery is not executed / not used as navigation; bind still fail-closed.accountabsent) still signs the connected wallet (existing tests).accounton POST.cd web && npx vitest run src/evm src/query.test.tsandnpm run test:e2e -- evm-sign(Playwright workers=5) green.Test plan (functional paths)
account=mock lowercase → consent → Connect & sign → Accepted → status true for that account.0xaaa…vs mock) → different wallet, claimed unsigned, mock account also not newly signed for the claimed key.account→ fail closed; CTA re-enabled (signShellfinallybusy reset).personal_sign(same as unsigned-already-signed today).account: existing happy paths (injected, EIP-6963, BinanceChain, late inject, WC) unchanged.account+redirect_urion the page, MetaMask/Binance/Copy keepaccountand do not navigate toredirect_uri.Test plan (attack / abuse)
account=javascript:alert(1)/data:/https://evil: must not become<a href>orlocation. Bind fail-closed. Sign as is a text node if shown.account=as open redirect: Open-in-app and success redirect still use portal URL / allowlistedredirect_urionly (web/src/redirect.ts).accountmust not be fed tosafeRedirectUri.account=→ can sign a different wallet and see Accepted; integrator status poll for the original address stays unsigned (fail-closed for the dApp). Do not “fix” this by authenticating the query param.account=with another0xthey control → they may accept for that address; dApp still polls the connected store address. Document; no server-side “claimed query” ACL.personal_signa non-selected provider when claimed matches the hidden one.personal_sign/ no POST.accountsChangedmid-sign: if the wallet signs a different key thanaccount_idin the message, API recover must reject. Do not add a portal-only bypass.Sign as: payload inaccountappears as text, not DOM.account=terra1…on/sign/evm→ invalid EVM address → mismatch, no submit.Verification criteria
account=).https://terms.cl8y.com/sign/evm?property=<registered>&account=0x…shows Sign as 0x…; connecting a different injected account does not Accepted.account=and Sign as matches the connected voting address.Out of scope
account=bind (#2 / #4).marked as related to #15
mentioned in issue #17
marked as related to #17
mentioned in merge request !33
mentioned in commit
45f4ea5b61Merge review (!33)
Merged !33 into
mainat45f4ea5(glab mr merge 15 --yes --remove-source-branch, no automerge, did not wait for CI). No merge conflicts (has_conflicts: false, 0 diverged commits vsmain).Acceptance criteria vs local verify
All #16 AC items are covered by the MR (unit + Playwright). Production code was not a reimplementation of
assertEvmAccountContinuity— comments/docs plusPLAYWRIGHT_WEB_PORTfor e2e; bind stays the existing helper. API EIP-191 verify was unchanged.Local (worktree
test/16-evm-account-continuity):127.0.0.1:5173was already owned by an unrelatedCL8Y-webvite preview, so e2e used port 5174 (supported by the MR config).Problems (not merge blockers)
#69/ jobs failed withci_quota_exceeded(same quota issue noted on #10 / #13). Not a code defect. Did not wait for CI per merge instructions.https://terms.cl8y.com/sign/evm?property=<registered>&account=0x…(Sign as + refuse a different injected account). The bind was already onmainbefore this test MR; a deploy is not required for the new tests to be true in prod, but the checkbox was never done./sign/solanastill ignoresaccount=— #17.Post-merge leftovers (manual QA / CI quota note) are tracked in a follow-up issue.
mentioned in issue #18
marked as related to #18
Post-merge leftovers are #18 (production / integrator smoke). Solana bind remains #17.