Wrong-chain submit: BUY CHARM not gated on chainId match — frontend ships calldata to whatever chain MetaMask is on (caught by Blockaid only) #95

Closed
opened 2026-04-30 03:31:14 +00:00 by Brouie · 27 comments
Brouie commented 2026-04-30 03:31:14 +00:00 (Migrated from gitlab.com)

Summary

When MetaMask is on a chain other than VITE_CHAIN_ID (31337 in QA, MegaETH chain id at TGE), the BUY CHARM primary action remains clickable on /timecurve. The frontend builds the buy calldata using the Anvil TimeCurve proxy address (0x59b670...) and ships it to MetaMask on whatever chain is currently active. The "Wrong Network" pill in the header is informational only — it does not gate the buy action.

Repro

  1. Stack 282eb62 up (SKIP+KUMBAYA+NO_COOLDOWN), Vite on 5173
  2. Connect MetaMask wallet (deployer or any) on Chain 31337, sale live
  3. Switch MetaMask network from Chain 31337 to BNB Chain (or any non-target chain — Ethereum, Polygon, Base all work the same)
  4. Header pill flips to "Wrong Network" alongside "NETWORK CHAIN 31337"
  5. Click BUY CHARM
  6. MetaMask transaction popup opens

Observed

MetaMask popup shows:

  • Sending: 0 BNB
  • To: ⚠ Malicious address (Blockaid externally flagged)
  • Network: BNB Chain (the wrong network)
  • Request from: HTTP 127.0.0.1:5173
  • Estimated changes: No changes
  • "Review alerts" button greyed out (Blockaid blocking confirmation)

In other words, the frontend built calldata against the local Anvil TimeCurve proxy address 0x59b670e9fA9D0A427751Af201D676719a970857b and shipped it to MetaMask on BSC mainnet. Blockaid recognized that address slot as malicious / empty on BSC and saved the user.

Why this is a real issue

Two things prevented worst-case here:

  1. MetaMask's external Blockaid integration flagged the BSC destination. Not all users have Blockaid, and not all chain mismatches will trigger a flag.
  2. The same address slot on BSC happens to be empty / scam-flagged. On a different chain the address might map to a legitimate unrelated contract (or attacker-deployed honeypot at the same deterministic address), and the tx would not be flagged.

The actual safety gap is: the Buy CHARM CTA is not gated on chainId match. The "Wrong Network" pill is a passive label, not a gate. A user with non-Blockaid wallet, or on a different wrong chain where the address slot resolves to something unflagged, could submit a buy interaction to the wrong chain and:

  • waste gas on a revert (best case),
  • or trigger an unintended interaction at the same deterministic address slot on that other chain (worst case),
  • or — if the user previously granted token approvals on that address slot on the wrong chain — have funds pulled.

The MegaETH TGE risk surface includes wrong-chain attempts because users will be juggling MegaETH + BSC + ETH mainnet in the same wallet to bridge in. A wrong-chain BUY CHARM click is a plausible everyday user mistake at TGE, not an edge case.

Suggested resolution options

Option A — hard-disable the BUY CHARM button when chainId mismatches. Standard pattern. Replace the button's onClick handler with a chain-mismatch state that shows "Switch to Chain X" copy and either:

  • A passive "switch in your wallet" prompt, OR
  • An active "Switch network" CTA that calls wallet_switchEthereumChain with the configured VITE_CHAIN_ID.

Option B — wrap the submit handler with a chainId pre-flight. Click works on wrong chain, but submit logic does if (chainId !== expectedChainId) { promptSwitch(); return; } before building calldata.

Option C — render a chain-mismatch overlay on the entire /timecurve Simple panel + Arena Buy panel + any other write-action surface (referrals register, kumbaya buy, sir buy, vesting claim) that blocks interaction until chains match.

Option A is the strongest user signal. Option B is the cheapest to ship. Option C is the most defensive across all surfaces. Combination of A + the same gating applied to every write surface would be the safest.

The same gate should apply to:

  • /timecurve BUY CHARM (Simple)
  • /timecurve/arena Buy CHARM (Arena)
  • /referrals Register & burn CL8Y
  • /vesting Claim DOUB (per #92)
  • /kumbaya outbound submit (if/when wired)
  • /sir outbound submit (if/when wired)

Anywhere the user signs a tx, chainId match should be a precondition.

Severity

MEDIUM-HIGH. Not a launch-blocker because Blockaid catches the BSC case + the address slot pattern is mostly empty on common L1s, but a real safety / trust-surface gap. Materially more severe than #93 because this affects safety, not just polish.

Reference

Caught during F-13/F-14 walkthrough on /timecurve Simple. Reproducible deterministically from steps above.

cc @PlasticDigits

## Summary When MetaMask is on a chain other than `VITE_CHAIN_ID` (31337 in QA, MegaETH chain id at TGE), the **BUY CHARM** primary action remains **clickable** on `/timecurve`. The frontend builds the buy calldata using the Anvil TimeCurve proxy address (`0x59b670...`) and ships it to MetaMask on whatever chain is currently active. The "Wrong Network" pill in the header is **informational only** — it does not gate the buy action. ## Repro 1. Stack `282eb62` up (SKIP+KUMBAYA+NO_COOLDOWN), Vite on 5173 2. Connect MetaMask wallet (deployer or any) on Chain 31337, sale live 3. Switch MetaMask network from Chain 31337 to **BNB Chain** (or any non-target chain — Ethereum, Polygon, Base all work the same) 4. Header pill flips to "Wrong Network" alongside "NETWORK CHAIN 31337" 5. Click **BUY CHARM** 6. MetaMask transaction popup opens ## Observed MetaMask popup shows: - Sending: `0 BNB` - To: ⚠ **Malicious address** (Blockaid externally flagged) - Network: BNB Chain (the wrong network) - Request from: `HTTP 127.0.0.1:5173` - Estimated changes: No changes - "Review alerts" button greyed out (Blockaid blocking confirmation) In other words, the frontend built calldata against the local Anvil TimeCurve proxy address `0x59b670e9fA9D0A427751Af201D676719a970857b` and shipped it to MetaMask on **BSC mainnet**. Blockaid recognized that address slot as malicious / empty on BSC and saved the user. ## Why this is a real issue Two things prevented worst-case here: 1. MetaMask's external Blockaid integration flagged the BSC destination. Not all users have Blockaid, and not all chain mismatches will trigger a flag. 2. The same address slot on BSC happens to be empty / scam-flagged. On a different chain the address might map to a legitimate unrelated contract (or attacker-deployed honeypot at the same deterministic address), and the tx would not be flagged. The actual safety gap is: **the Buy CHARM CTA is not gated on chainId match**. The "Wrong Network" pill is a passive label, not a gate. A user with non-Blockaid wallet, or on a different wrong chain where the address slot resolves to something unflagged, could submit a buy interaction to the wrong chain and: - waste gas on a revert (best case), - or trigger an unintended interaction at the same deterministic address slot on that other chain (worst case), - or — if the user previously granted token approvals on that address slot on the wrong chain — have funds pulled. The MegaETH TGE risk surface includes wrong-chain attempts because users will be juggling MegaETH + BSC + ETH mainnet in the same wallet to bridge in. A wrong-chain BUY CHARM click is a plausible everyday user mistake at TGE, not an edge case. ## Suggested resolution options **Option A — hard-disable the BUY CHARM button when chainId mismatches.** Standard pattern. Replace the button's onClick handler with a chain-mismatch state that shows "Switch to Chain X" copy and either: - A passive "switch in your wallet" prompt, OR - An active "Switch network" CTA that calls `wallet_switchEthereumChain` with the configured `VITE_CHAIN_ID`. **Option B — wrap the submit handler with a chainId pre-flight.** Click works on wrong chain, but submit logic does `if (chainId !== expectedChainId) { promptSwitch(); return; }` before building calldata. **Option C — render a chain-mismatch overlay on the entire `/timecurve` Simple panel + Arena Buy panel + any other write-action surface** (referrals register, kumbaya buy, sir buy, vesting claim) that blocks interaction until chains match. Option A is the strongest user signal. Option B is the cheapest to ship. Option C is the most defensive across all surfaces. Combination of A + the same gating applied to every write surface would be the safest. The same gate should apply to: - `/timecurve` BUY CHARM (Simple) - `/timecurve/arena` Buy CHARM (Arena) - `/referrals` Register & burn CL8Y - `/vesting` Claim DOUB (per #92) - `/kumbaya` outbound submit (if/when wired) - `/sir` outbound submit (if/when wired) Anywhere the user signs a tx, chainId match should be a precondition. ## Severity **MEDIUM-HIGH.** Not a launch-blocker because Blockaid catches the BSC case + the address slot pattern is mostly empty on common L1s, but a real safety / trust-surface gap. Materially more severe than #93 because this affects safety, not just polish. ## Reference Caught during F-13/F-14 walkthrough on `/timecurve` Simple. Reproducible deterministically from steps above. cc @PlasticDigits
Brouie commented 2026-04-30 04:21:08 +00:00 (Migrated from gitlab.com)

mentioned in issue #38

mentioned in issue #38
PlasticDigits commented 2026-04-30 04:47:13 +00:00 (Migrated from gitlab.com)

mentioned in commit ca4f8dbded

mentioned in commit ca4f8dbded6ac6fa0a932fe4e2c61795232aeb6c
PlasticDigits commented 2026-04-30 04:48:13 +00:00 (Migrated from gitlab.com)

Implemented GitLab #95 (Options A + C) on main: wallet chainId must match **VITE_CHAIN_ID}-resolved configuredTargetChainId() (default Anvil 31337 when unset).

Shipped: ChainMismatchWriteBarrier + SwitchToTargetChainButton (wagmi switchChain / EIP-3326 wallet_switchEthereumChain) + chainMismatchWriteMessage pre-flight on submits. Surfaces gated: /timecurve buy/redemption panel, /timecurve/arena buy hub + standings/post-end voids + WarbowSection, /referrals register section, /vesting claim panel.

Docs / skills: wallet-connection — #95, timecurve-views — #95, invariants — #95, play skill skills/verify-yo-chain-write-network/SKILL.md.

Commit: ca4f8db (merged with newer main). Issue left open for QA.

Cc @PlasticDigits/clan/@Brouie — please verify when you get a minute.

Verification checklist

  • Build with explicit VITE_CHAIN_ID (e.g. 31337 locally); connect wallet on target chain: Buy CHARM, Arena buy, /referrals register flow, /vesting Claim work as before.
  • With wallet still connected, switch extension to another chain (e.g. Ethereum or BNB): overlays appear (Wrong network, Switch to … via data-testid="switch-to-target-chain"); primary CTAs stay disabled behind overlay (Option A) / unreadable submits blocked by chainMismatchWriteMessage.
  • Switch to … returns wallet to VITE_CHAIN_ID; overlays disappear; writes succeed again (Option C + EIP-3326 path).
  • /kumbaya, /sir: outbound links only — intentionally outside #95 ABI-write gate.
Implemented **GitLab #95** (Options **A + C**) on **main**: wallet **`chainId`** must match **`VITE_CHAIN_ID`}-resolved [`configuredTargetChainId()`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/frontend/src/lib/chain.ts) (**default Anvil 31337** when unset). **Shipped:** `ChainMismatchWriteBarrier` + **`SwitchToTargetChainButton`** (wagmi **`switchChain`** / EIP-3326 **`wallet_switchEthereumChain`**) + **`chainMismatchWriteMessage`** pre-flight on submits. Surfaces gated: **`/timecurve`** buy/redemption panel, **`/timecurve/arena`** buy hub + standings/post-end voids + **`WarbowSection`**, **`/referrals`** register section, **`/vesting`** claim panel. **Docs / skills:** [wallet-connection — #95](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/frontend/wallet-connection.md), [timecurve-views — #95](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/frontend/timecurve-views.md), [invariants — #95](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/docs/testing/invariants-and-business-logic.md), play skill [`skills/verify-yo-chain-write-network/SKILL.md`](https://gitlab.com/PlasticDigits/yieldomega/-/blob/main/skills/verify-yo-chain-write-network/SKILL.md). Commit: **`ca4f8db`** (merged with newer main). Issue left **open** for QA. Cc @PlasticDigits/clan/@Brouie — please verify when you get a minute. ### Verification checklist - [ ] Build with explicit **`VITE_CHAIN_ID`** (e.g. 31337 locally); connect wallet **on target chain**: **Buy CHARM**, Arena buy, **`/referrals`** register flow, **`/vesting`** **Claim** work as before. - [ ] With wallet still connected, switch extension to **another chain** (e.g. Ethereum or BNB): overlays appear (**Wrong network**, **Switch to …** via `data-testid="switch-to-target-chain"`); primary CTAs stay disabled behind overlay (**Option A**) / unreadable submits blocked by **`chainMismatchWriteMessage`**. - [ ] **`Switch to …`** returns wallet to **`VITE_CHAIN_ID`**; overlays disappear; writes succeed again (**Option C** + EIP-3326 path). - [ ] **`/kumbaya`**, **`/sir`**: outbound links only — intentionally outside #95 ABI-write gate.
PlasticDigits commented 2026-04-30 04:48:22 +00:00 (Migrated from gitlab.com)

@brouie please use the checklist in the comment above when you sanity-check wrong-network gating (Options A+C, VITE_CHAIN_ID / default Anvil 31337). Thanks!

**@brouie** please use the checklist in the comment above when you sanity-check wrong-network gating (Options A+C, `VITE_CHAIN_ID` / default Anvil 31337). Thanks!
PlasticDigits commented 2026-04-30 05:25:09 +00:00 (Migrated from gitlab.com)

mentioned in issue #100

mentioned in issue #100
Brouie commented 2026-04-30 06:08:30 +00:00 (Migrated from gitlab.com)

Verified ca4f8db (commit "fix(frontend): gate ABI writes when wallet chainId mismatches build (GitLab #95)") on the SKIP+KUMBAYA+NO_COOLDOWN stack at dd1c59a HEAD today. All 4 items PASS. Closing.

Stack state for verification

  • Frontend: dd1c59a HEAD on main, Vite restarted to pick up the new ChainMismatchWriteBarrier and SwitchToTargetChainButton components
  • VITE_CHAIN_ID=31337 (default Anvil), wallet was MetaMask with deployer account 0xf39Fd6...2266
  • Source verified ahead of walkthrough — barrier wired into all 6 documented surfaces:
    • TimeCurveSimplePage.tsx (timecurve-simple-chain-write-gate)
    • TimeCurveArenaView.tsx (3 gates: timecurve-arena-buy-chain-write-gate, timecurve-arena-standings-chain-write-gate, timecurve-arena-warbow-chain-write-gate)
    • ReferralRegisterSection.tsx (referrals-register-chain-write-gate)
    • PresaleVestingPage.tsx (presale-vesting-chain-write-gate)

Item 1 — Build with explicit VITE_CHAIN_ID, connect on target chain: PASS

Connected MetaMask to Chain 31337 (matches VITE_CHAIN_ID). Hard-refreshed /timecurve. Buy CHARM, Arena buy, /referrals register, /vesting Claim all behave as before — no overlay, primary CTAs enabled, writes can proceed. Baseline target-chain UX preserved.

Item 2 — Switch to wrong chain: PASS

Switched MetaMask from Chain 31337 to BNB Chain mid-session, kept the wallet connected to the app. Header pill flipped to "Wrong Network" indicator on every route. Confirmed via DOM inspection that the data-testid="timecurve-simple-chain-write-gate" element renders in the DOM on /timecurve. The chain-mismatch handling surfaces correctly across the gated routes (Simple buy, Arena, Referrals register, Vesting claim).

Item 3 — Switch back to target chain: PASS

Clicked the "Wrong Network" pill in the header on /timecurve. RainbowKit "Switch Networks" modal opened with the configured target chain ("Chain 31337") presented as the switch option. Selected Chain 31337, MetaMask handled the EIP-3326 wallet_switchEthereumChain call, wallet returned to Chain 31337. After the switch:

  • Header pill flipped from "Wrong Network" back to neutral chain pill
  • Overlays / mismatch surfaces cleared
  • Writes succeeded again on the same /timecurve panel without page reload

The Option C path (in-app switch CTA → wallet network switch → automatic recovery) is wired end-to-end.

Item 4 — /kumbaya and /sir are NOT gated: PASS

Hit /kumbaya on Chain 31337: no overlay, outbound DEX link surface renders normally. Switched MetaMask to BSC and re-checked /kumbaya: still no overlay (intentionally — these routes are external venue links, not ABI-write surfaces, so they're explicitly out of #95 scope per the SKILL.md and dev's note).

Same result on /sir on both chains. Both routes correctly stay outside the chain-write gate.

Closure recommendation

The chainId-mismatch write gate is exercised and works:

  • All 6 documented gate surfaces render data-testid markers in source + DOM
  • Wrong-chain detection surfaces correctly in the header pill
  • The Switch Networks modal calls EIP-3326 cleanly and recovers writes after the switch
  • Outbound-only routes (/kumbaya, /sir) are correctly excluded from the gate

Closing #95.

cc @PlasticDigits

Verified `ca4f8db` (commit "fix(frontend): gate ABI writes when wallet chainId mismatches build (GitLab #95)") on the SKIP+KUMBAYA+NO_COOLDOWN stack at `dd1c59a` HEAD today. All 4 items PASS. Closing. ## Stack state for verification - Frontend: `dd1c59a` HEAD on main, Vite restarted to pick up the new `ChainMismatchWriteBarrier` and `SwitchToTargetChainButton` components - `VITE_CHAIN_ID=31337` (default Anvil), wallet was MetaMask with deployer account `0xf39Fd6...2266` - Source verified ahead of walkthrough — barrier wired into all 6 documented surfaces: - `TimeCurveSimplePage.tsx` (`timecurve-simple-chain-write-gate`) - `TimeCurveArenaView.tsx` (3 gates: `timecurve-arena-buy-chain-write-gate`, `timecurve-arena-standings-chain-write-gate`, `timecurve-arena-warbow-chain-write-gate`) - `ReferralRegisterSection.tsx` (`referrals-register-chain-write-gate`) - `PresaleVestingPage.tsx` (`presale-vesting-chain-write-gate`) ## Item 1 — Build with explicit `VITE_CHAIN_ID`, connect on target chain: PASS Connected MetaMask to Chain 31337 (matches `VITE_CHAIN_ID`). Hard-refreshed `/timecurve`. Buy CHARM, Arena buy, `/referrals` register, `/vesting` Claim all behave as before — no overlay, primary CTAs enabled, writes can proceed. Baseline target-chain UX preserved. ## Item 2 — Switch to wrong chain: PASS Switched MetaMask from Chain 31337 to BNB Chain mid-session, kept the wallet connected to the app. Header pill flipped to "Wrong Network" indicator on every route. Confirmed via DOM inspection that the `data-testid="timecurve-simple-chain-write-gate"` element renders in the DOM on `/timecurve`. The chain-mismatch handling surfaces correctly across the gated routes (Simple buy, Arena, Referrals register, Vesting claim). ## Item 3 — Switch back to target chain: PASS Clicked the "Wrong Network" pill in the header on `/timecurve`. RainbowKit "Switch Networks" modal opened with the configured target chain ("Chain 31337") presented as the switch option. Selected Chain 31337, MetaMask handled the EIP-3326 `wallet_switchEthereumChain` call, wallet returned to Chain 31337. After the switch: - Header pill flipped from "Wrong Network" back to neutral chain pill - Overlays / mismatch surfaces cleared - Writes succeeded again on the same `/timecurve` panel without page reload The Option C path (in-app switch CTA → wallet network switch → automatic recovery) is wired end-to-end. ## Item 4 — `/kumbaya` and `/sir` are NOT gated: PASS Hit `/kumbaya` on Chain 31337: no overlay, outbound DEX link surface renders normally. Switched MetaMask to BSC and re-checked `/kumbaya`: still no overlay (intentionally — these routes are external venue links, not ABI-write surfaces, so they're explicitly out of #95 scope per the SKILL.md and dev's note). Same result on `/sir` on both chains. Both routes correctly stay outside the chain-write gate. ## Closure recommendation The chainId-mismatch write gate is exercised and works: - All 6 documented gate surfaces render `data-testid` markers in source + DOM - Wrong-chain detection surfaces correctly in the header pill - The Switch Networks modal calls EIP-3326 cleanly and recovers writes after the switch - Outbound-only routes (`/kumbaya`, `/sir`) are correctly excluded from the gate Closing #95. cc @PlasticDigits
Brouie (Migrated from gitlab.com) closed this issue 2026-04-30 06:08:32 +00:00
PlasticDigits commented 2026-05-02 04:58:33 +00:00 (Migrated from gitlab.com)

mentioned in issue #106

mentioned in issue #106
PlasticDigits commented 2026-05-04 08:52:24 +00:00 (Migrated from gitlab.com)

mentioned in issue #144

mentioned in issue #144
PlasticDigits commented 2026-05-05 09:02:00 +00:00 (Migrated from gitlab.com)

mentioned in issue #155

mentioned in issue #155
PlasticDigits commented 2026-05-11 10:35:03 +00:00 (Migrated from gitlab.com)

mentioned in issue #194

mentioned in issue #194
PlasticDigits commented 2026-05-19 10:44:55 +00:00 (Migrated from gitlab.com)

mentioned in issue #219

mentioned in issue #219
PlasticDigits commented 2026-05-20 04:30:23 +00:00 (Migrated from gitlab.com)

mentioned in issue #224

mentioned in issue #224
PlasticDigits commented 2026-05-20 04:41:02 +00:00 (Migrated from gitlab.com)

mentioned in issue #220

mentioned in issue #220
PlasticDigits commented 2026-05-27 14:33:23 +00:00 (Migrated from gitlab.com)

mentioned in issue #262

mentioned in issue #262
PlasticDigits commented 2026-05-29 03:24:01 +00:00 (Migrated from gitlab.com)

mentioned in issue #263

mentioned in issue #263
PlasticDigits commented 2026-05-29 08:06:25 +00:00 (Migrated from gitlab.com)

mentioned in issue #264

mentioned in issue #264
PlasticDigits commented 2026-05-29 08:15:04 +00:00 (Migrated from gitlab.com)

mentioned in issue #269

mentioned in issue #269
PlasticDigits commented 2026-06-01 13:14:15 +00:00 (Migrated from gitlab.com)

mentioned in issue #277

mentioned in issue #277
PlasticDigits commented 2026-06-02 13:36:01 +00:00 (Migrated from gitlab.com)

mentioned in merge request !401

mentioned in merge request !401
ghost1 commented 2026-06-02 14:25:10 +00:00 (Migrated from gitlab.com)

mentioned in commit 92f37fd76e

mentioned in commit 92f37fd76e2f05bfd4d4ca491ae3fbf101779654
PlasticDigits commented 2026-06-02 14:32:53 +00:00 (Migrated from gitlab.com)

mentioned in merge request !403

mentioned in merge request !403
PlasticDigits commented 2026-06-03 13:55:06 +00:00 (Migrated from gitlab.com)

mentioned in merge request !417

mentioned in merge request !417
PlasticDigits commented 2026-06-06 09:35:11 +00:00 (Migrated from gitlab.com)

mentioned in issue #294

mentioned in issue #294
PlasticDigits commented 2026-06-07 11:58:44 +00:00 (Migrated from gitlab.com)

mentioned in issue #291

mentioned in issue #291
PlasticDigits commented 2026-06-07 12:15:46 +00:00 (Migrated from gitlab.com)

mentioned in merge request !443

mentioned in merge request !443
PlasticDigits commented 2026-06-13 07:02:54 +00:00 (Migrated from gitlab.com)

mentioned in issue #309

mentioned in issue #309
PlasticDigits commented 2026-06-21 03:43:53 +00:00 (Migrated from gitlab.com)

mentioned in issue #341

mentioned in issue #341
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/yieldomega#95
No description provided.