Add Stark key registration tool for USER_UNREGISTERED withdrawals - #22
Merged
Merged
Conversation
Users whose Stark key is not the decimal form of their Ethereum address were registered off-chain by Immutable X, so the bridge has no ethKey on record and withdraw() reverts with USER_UNREGISTERED. Registering needs a Stark-curve signature from the Stark private key, which Immutable X derived from a wallet signature, so users cannot do it through Etherscan. tools/stark-key-registration is a local web page that: - asks the wallet to sign the Immutable X key message and re-derives the Stark key in the browser; - signs the registration payload bound to the connected address and submits registerSender, so funds can only go to the signing wallet; - finalises each pending withdrawal once the key is registered to the connected wallet. The derivation reimplements generateLegacyStarkPrivateKey from @imtbl/core-sdk 3.6.1. For about 1 in 30 wallets the SDK disambiguated between three grinding variants by querying the Immutable X API, which has been retired; the tool computes every candidate and uses the one holding funds on the bridge. Safeguards: CSP with connect-src 'none', refusal to run off localhost, chain and implementation checks, eth_call simulation before every transaction, and pinned dependencies with a four-library runtime bundle. Tests: unit equivalence with the SDK (including nock-driven fallback branches), signature checks against the SDK's elliptic curve, mainnet fork tests against the deployed bridge, and a Playwright run of the built page on a fork. docs/finalise-pending-withdrawals.md gains a getEthKey check step, a USER_UNREGISTERED section pointing to the tool, and a troubleshooting table. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
All alerts resolved. Learn more about Socket for GitHub. This PR previously contained dependency changes with security issues that have been resolved, removed, or ignored. |
Socket flagged ten axios advisories and one ws advisory reached through @imtbl/core-sdk@3.6.1 (axios 0.26.1, ws 8.18.0), and obfuscated-code warnings for nock and @mswjs/interceptors. All were development dependencies used only as test references and absent from the page bundle, but the SDK pins axios 0.26.1 and cannot be upgraded. - Record the SDK's outputs once in test/vectors/sdk-3.6.1.json. generate-sdk-vectors.ts drives generateLegacyStarkPrivateKey with nock answering each API-resolved branch, and runs with both packages installed via `npm i --no-save`, so neither enters the lockfile. The derivation test now compares against these vectors: single-candidate wallets, the exact candidate set for ambiguous wallets (including a three-candidate wallet), and the SDK signer's public keys. - Replace elliptic and hash.js in the registration test with a BigInt port of StarkCurveECDSA.verify, which checks signatures by the on-chain rule and clears elliptic's GHSA-848j-6mx2-7j84 advisory. - Store test wallets in the vectors as indices rather than private keys. npm audit reports no vulnerabilities for the full tree. Unit (12), fork (9) and end-to-end (3) suites pass, and mutating the grinding indices or the registration prefix still fails them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ion tool Manual runs with MetaMask surfaced several problems the stub-wallet tests did not catch. - The network switch sent chainId 0x01; MetaMask requires unpadded hex and rejected it, and the page swallowed the error. Send 0x1, report failures, and reconnect after a successful switch. - Unlocking MetaMask and approving the site emits accountsChanged, and the page reloaded on any wallet event, discarding the acknowledgements. MetaMask also emits chainChanged after resolving the switch request, which reloaded the page once reconnected. Ignore wallet events before a connection exists, and afterwards reload only when the account changes or the wallet leaves mainnet. Guard against concurrent connection attempts. - Show the switch button only for a wrong network, with a note that MetaMask can keep a separate network per site. - A failing wallet RPC surfaced as ethers' "missing revert data". Explain that the bridge could not be read, show the endpoint's error, and offer a retry. Read balances five at a time, since wallet endpoints throttle bursts of parallel eth_calls. - Label the page "Ethereum Mainnet only" and list the tokens checked for pending withdrawals, since getWithdrawalBalance needs an asset ID and tokens outside config/operate/mainnet/imx_tokens.json are not found. - Add ?allow-registration-without-funds, which offers registration for unregistered keys with no pending withdrawals so registration can be tested on mainnet. The page shows a testing-mode banner when it is set. - Stop emitting source maps (DevTools fetches are blocked by the page's connect-src 'none') and add an inline favicon. The stub wallet in the end-to-end tests now follows MetaMask's event ordering and chain ID validation, and covers each of the above. Docs: registration uses about 8 million gas because the bridge verifies the Stark signature in Solidity. State this, with costs at several gas prices, in the guide and the tool README, and explain the out-of-gas trace a simulator shows when the gas limit or balance is too low. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
ermyas
marked this pull request as ready for review
September 24, 2026 02:00
allan-almeida-imtbl
approved these changes
Sep 24, 2026
lfportal
approved these changes
Sep 24, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Users whose Stark key is not the decimal form of their Ethereum address were registered off-chain by Immutable X. The bridge has no
ethKeyon record for them, sowithdraw()reverts withUSER_UNREGISTEREDeven thoughgetWithdrawalBalanceshows funds. Registration (registerSender/registerEthAddress, live since StarkExchangeMigrationV2) needs a Stark-curve signature from the Stark private key. Immutable X derived that key from a wallet signature, so users cannot produce it through Etherscan.This PR adds
tools/stark-key-registration, a local web page for Ethereum Mainnet that:config/operate/mainnet/imx_tokens.json(getWithdrawalBalanceneeds an asset ID, so each token is queried);registerSender;It also updates
docs/finalise-pending-withdrawals.mdwith:getEthKeycheck step;USER_UNREGISTEREDsection pointing to the tool;Key derivation
src/lib/derivation.tsreimplementsgenerateLegacyStarkPrivateKeyfrom@imtbl/core-sdk3.6.1. For about 1 in 30 wallets the first grinding round is rejected, and the SDK's three grinding variants then disagree. The SDK chose between them by querying the Immutable X API (/v1/users), which now returns 404, so the SDK fails for those users. The tool computes every candidate and uses the one holding funds on the bridge. Every candidate derives from the wallet's own signature, so a derivation error yields a key with no funds, never another user's key.Safeguards
registerSenderusesmsg.senderas the owner, and the page withdraws only for keys registered to the connected wallet.deriveAccounts. The CSP setsconnect-src 'none', and chain access goes through the wallet extension.127.0.0.1/localhost. It checks for chain 1 and implementation0x273b65a7231321D4ee47a4c47408Ef43517455Ec.eth_callbefore it is sent.npm ci --omit=devinstallsviteplus four runtime libraries (ethers,@scure/bip32,@scure/starknet,@noble/hashes), andnpm auditis clean for the full tree.@imtbl/core-sdkis not a dependency: its outputs are recorded as test vectors bytest/vectors/generate-sdk-vectors.ts, which installs it withnpm i --no-save. Socket flagged the SDK'saxios@0.26.1andws@8.18.0; those alerts are resolved by this change.Registration gas
registerSenderuses about 7.9M gas, becauseStarkCurveECDSA.verifydoes elliptic-curve arithmetic in Solidity. That is about 0.0006 ETH at 0.07 gwei and 0.04 ETH at 5 gwei. It fits within the 60M block limit and the 16.7M per-transaction cap. With too little balance or gas limit, wallets and simulators report the transaction as likely to fail, and the trace stops at the modexp precompile. The docs describe this.Testing override
?allow-registration-without-fundsoffers registration for unregistered keys with no pending withdrawals, so registration can be tested end to end on mainnet with a test wallet. It is not linked from the page, shows a testing-mode banner when set, and is documented in the tool README's Development section.Test plan
npm run typechecknpm audit: 0 vulnerabilities.npm test: 12 unit tests.@imtbl/core-sdk3.6.1 recorded intest/vectors/sdk-3.6.1.json. This covers typical wallets, and the exact candidate set for ambiguous wallets across every API-resolved branch, including a three-candidate wallet. Public keys match the SDK's Stark signer.StarkCurveECDSA.verify, including itsr/s⁻¹bounds, and fail for another address.ETH_RPC_URL=… npm run test:fork: 9 tests against the deployed bridge on an anvil fork.ETH_RPC_URL=… npm run test:e2e: 7 Playwright tests of the built page on a fork, using a stub wallet that follows MetaMask's event ordering and chain ID validation.registerSenderprompt. The issues found in this run are fixed in 9bfe72a.registerSendercalldata MetaMask produced, run witheth_callon real mainnet from the signing address, succeeds. The same calldata from another address revertsINVALID_STARK_SIGNATURE.🤖 Generated with Claude Code