ERC-4361: Sign-In with Ethereum
ERC-4361 standardises the off-chain message format that lets a user "log in" to a web app using their Ethereum wallet. The wallet signs a structured plaintext message; the server verifies the signature against the claimed address. It's the de-facto authentication flow for every modern dApp — Snapshot, Etherscan, OpenSea, etc. — and one of the few ERCs whose adoption is essentially universal.
Required Message Format
example.com wants you to sign in with your Ethereum account:
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
I agree to the rules of example.com.
URI: https://example.com/login
Version: 1
Chain ID: 1
Nonce: 32891756
Issued At: 2025-01-15T13:14:15Z
Expiration Time: 2025-01-15T14:14:15Z
Not Before: 2025-01-15T13:14:15Z
Request ID: 81bb...3acf
Resources:
- ipfs://Qm…
- https://example.com/my-web2-claim.jsonThe server (not a contract) verifies the signature with ecrecover, checks the domain, nonce uniqueness, and time window, then issues a session token. ERC-4361 is a format spec for off-chain auth — no on-chain contracts are required.
Neo Equivalent: Native Witness over a Domain-Bound Message
Neo's witness model already provides the same "user signs a message, server verifies" primitive — at the protocol level, with no need for a domain-separator hack. Two viable patterns:
A. Sign-a-transaction-stub — the wallet signs an empty Neo invocation script that includes the domain, nonce, and timestamps as Notify payloads or PUSH literals. The server verifies the witness via Crypto.VerifyWithECDsa (or by simulating the script) and reads the embedded fields.
B. Sign-arbitrary-message — the wallet signs an arbitrary byte string prefixed with a Neo-Express-style magic header (010001f0 + payload + 00) using its private key. The server verifies via Crypto.VerifyWithECDsa(payload, pubKey, signature). This is what NeoLine, O3, and OneGate already implement under the brand "Neo Sign-In".
| ERC-4361 Field | Neo Equivalent |
|---|---|
domain line | Application-defined prefix in the signed message; verified server-side |
address (claim) | Neo address derived from pubKey via Helper.ToScriptHash(pubKey) |
EIP-191 prefix (\x19Ethereum Signed Message:\n) | Neo wallet message prefix (010001f0 + length + payload + 00) |
Chain ID | Neo network magic (894710606 TestNet) baked into the signed payload |
Nonce | Application-supplied random nonce; server tracks consumed nonces |
Issued At / Expiration Time / Not Before | Timestamps in the payload; server enforces window |
ecrecover (server) | Crypto.VerifyWithECDsa(payload, pubKey, sig, NamedCurveHash.secp256r1SHA256) (server) |
Resources (URIs the user is authorising) | Free-form payload; convention only |
| Wallet UX (sign typed/personal message) | NeoLine signMessage, O3 signPersonal, OneGate signMessage |
Migration Notes
The most useful "neo-side" SIWE adoption is a JS-SDK convention, not an on-chain contract. The mirror page exists because:
- dApp authors migrating from Ethereum keep asking "where's SIWE on Neo?".
- The wallet-side message format is already standard (Neo dApp API
signMessage); a thin server library that mirrorssiwe-js's API would close the migration gap for most dApps.
Recommended: publish a neo-siwe server library with the same shape as siwe — prepareMessage(), verify(), generateNonce() — wrapping the wallet's signMessage payload format. No contract required; any backend can adopt SIWE-on-Neo with one helper.
