Skip to content

feat(chat): session-wallet security — SIWE domain binding, withdraw, contract-wallet guard#762

Open
bussyjd wants to merge 3 commits into
feat/agent-chat-widgetfrom
feat/chat-siwe-session
Open

feat(chat): session-wallet security — SIWE domain binding, withdraw, contract-wallet guard#762
bussyjd wants to merge 3 commits into
feat/agent-chat-widgetfrom
feat/chat-siwe-session

Conversation

@bussyjd

@bussyjd bussyjd commented Jul 16, 2026

Copy link
Copy Markdown
Contributor

Stacked on #752 (base is its branch, so the diff shows only these 2 commits; retarget to main when #752 merges).

Hardens the chat widget's session-wallet mechanism against the two ways users could actually lose funds, and adds the recovery path:

  1. EIP-4361 sign-in message (wallet-enforced domain binding). The session key is keccak256(personal_sign(message)); personal_sign has no origin binding, so with a prose message a phishing site could request the byte-identical text and derive the victim's session wallet. The message is now strict SIWE grammar: wallets detect it, compare the message domain to the requesting origin, and interpose their deceptive-site warning on mismatch. Every field is deliberately static (constant nonce and Issued At): the signature is key material, never a server-replayable proof — replay protection is meaningless here, while determinism is what lets users recover a session by re-signing. Address is EIP-55-checksummed via an in-page keccak loop verified against the spec vectors; Chain ID comes from the 402 challenge, so sessions are per-chain like the funds themselves.
  2. Withdraw. The session wallet signs a direct EIP-3009 transferWithAuthorization for its full balance back to the connected wallet, which submits it (session wallets hold no ETH). This also converts an offer hostname change from a fund-stranding event into drain-and-refund — relevant because domain-bound keys re-derive differently on a new hostname, by design. First use was a real on-chain sweep prior to the message change landing.
  3. Contract-wallet guard. ERC-1271 smart wallets sign non-deterministically, so the derived session wallet could never be recovered — connect() now refuses them with an explanation instead of stranding funds through normal use. EIP-7702-delegated EOAs (code 0xef0100…) still sign with the EOA key and are allowed.

Message v1 is retired (documented in-code); its only funded session was swept via the new withdraw flow before the cutover, so no user funds were affected.

Tests green; commits signed; live-verified (withdraw exercised with a real settled sweep on Base mainnet).

https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk

bussyjd added 3 commits July 16, 2026 10:19
withdraw: the session wallet signs a direct EIP-3009
transferWithAuthorization for its full balance back to the connected
wallet, which submits it (and pays the dust of gas) — the session wallet
never needs ETH. This is the recovery path removing reveal-key promised,
and it makes offer hostname changes a drain-and-refund instead of a
fund-stranding event.

connect() now getCode-guards the account: ERC-1271 smart-contract
wallets sign non-deterministically, so the session wallet this page
derives could never be recovered — refuse with an explanation instead of
stranding funds through normal use. EIP-7702 delegated EOAs (code
0xef0100…) still sign with the EOA key and are allowed.

Implemented by grok-4.5 lane from spec; diff reviewed, tests re-run.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
The session-key message was prose: it named the domain, but nothing
enforced it — personal_sign has no origin binding, so a phishing site
could request the byte-identical message and derive the victim's
session wallet from the signature. Formatted as strict EIP-4361, wallets
(MetaMask, Rabby) detect the SIWE grammar, compare the message domain to
the requesting origin, and interpose a deceptive-site warning on
mismatch — the domain check moves from user attention to wallet
enforcement.

Every field is static by design: the signature is key material
(keccak256 of it derives the session wallet), never a server-replayable
proof, so a rotating nonce protects nothing here while determinism is
what lets users recover their session by re-signing. Address is EIP-55
checksummed via an in-page keccak loop (verified against the EIP-55
spec vectors); Chain ID comes from the 402 challenge, so sessions are
per-chain, matching where the funds actually live.

Message v1 was retired after its only funded session was swept via the
new withdraw flow.

Implemented by grok-4.5 lane from spec; diff reviewed, checksum
executed against reference vectors, tests re-run.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
fund / withdraw / explorer become a slim inline row of 22px icon
buttons (Feather-style inline SVGs, no icon dependency — the CSP is
same-origin only), mirroring the 402 page's canonical .icon-btn shape
and hover; the session address gains a click-to-copy button reusing
that page's copy glyph verbatim. Tooltips + aria-labels carry the
labels the icons dropped.

Implemented by grok-4.5 lane from spec; diff reviewed, tests re-run.

Claude-Session: https://claude.ai/code/session_014YjPMViNrZ7zBVgUQzwEKk
bussyjd added a commit that referenced this pull request Jul 16, 2026
Built-in browser chat widget for agent-type offers (base of SIWE stack #762).
bussyjd added a commit that referenced this pull request Jul 16, 2026
Session-wallet security: EIP-4361 SIWE domain binding, withdraw path,
contract-wallet guard, session-strip icon row.
@bussyjd bussyjd mentioned this pull request Jul 16, 2026
6 tasks
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