Skip to content

fix(evm): drop Base, Arbitrum and Avalanche from the 0x decoder allowlist - #470

Closed
BitHighlander wants to merge 1 commit into
release/7.14.2from
fix/0x-allowlist-drop-truncating-chains
Closed

fix(evm): drop Base, Arbitrum and Avalanche from the 0x decoder allowlist#470
BitHighlander wants to merge 1 commit into
release/7.14.2from
fix/0x-allowlist-drop-truncating-chains

Conversation

@BitHighlander

Copy link
Copy Markdown
Owner

Follow-up to #468, as agreed: keep the 0x decoder where it works, gate it where it doesn't.

The problem

TokenType.chain_id and tokenByChainAddress() are uint8_t (include/keepkey/firmware/ethereum_tokens.h:42,54). Three of the six chains in zx_isExchangeProxyChain() don't fit:

chain id as uint8_t
Ethereum 1 1
BNB Chain 56 56
Polygon 137 137
Base 8453 5
Arbitrum 42161 177
Avalanche 43114 106

The truncated values match no table entry, so the lookup returns UnknownToken and ethereumFormatAmount() short-circuits to the literal "Unknown token value" (lib/firmware/ethereum.c:347) — for both operands. zx_confirmZxTransERC20() renders:

Transform ERC20
Input Unknown token value
Output Unknown token value

…while the transformations[] body executes.

Why that is worse than it looks

It removes the only justification for clear-signing transformERC20 at all. The exemption exists on the argument that the input amount and minimum output amount shown on screen bound the outcome. On these three chains no amount is shown, so there is no bound and no disclosure — a blind signature wearing a decoder's title.

0x does deploy the same Exchange Proxy on all six, so this is not an address-confusion problem like Optimism (which uses a different proxy and was already excluded). The decoder correctly identifies the contract and then cannot say anything true about the trade.

What this does

Denies those three, so their swaps take the generic raw-calldata path: AdvancedMode-gated, bytes on screen. The owner can still sign — the device just stops claiming it understood. Ethereum, BNB Chain and Polygon keep decoded signing, where the amounts render.

The test now pins both directions, including that the truncated values (177, 106) are not themselves a way back in.

Scope

This is the near-term half. The underlying defect is the narrow chain id — #455. Once the token table carries a full-width chain id, these three can be restored with their amounts actually rendering; the test comment says so, so the restoration is a deliberate act rather than an accident.

Verified

ARM cross-compile in the pinned builder image; full firmware-unit + board-unit suites green (make xunit exit 0); cppcheck clean under CI's exact invocation (0 findings); clang-format clean.

Branched from release/7.14.2 at 2d2637747 (the #468 merge). Note #469: PRs targeting release/** get no pull_request CI event, so this PR's checks come from the head-branch push run and do not test the merge result.

…list

0x deploys the same Exchange Proxy on these three chains, so the decoder finds
the contract -- but it cannot describe the trade there, and a screen that
asserts understanding it does not have is worse than no screen.

TokenType.chain_id and tokenByChainAddress() are uint8_t
(ethereum_tokens.h:42,54), so the chain ids truncate: Base 8453 -> 5, Arbitrum
42161 -> 177, Avalanche 43114 -> 106. None matches a table entry, the lookup
returns UnknownToken, and ethereumFormatAmount() short-circuits to the literal
"Unknown token value" (ethereum.c:347) for BOTH operands. zx_confirmZxTransERC20
then renders

    Transform ERC20
    Input Unknown token value
    Output Unknown token value

while the transformations[] body executes.

That matters because it removes the only justification for clear-signing
transformERC20 at all. The exemption exists on the argument that the input
amount and the minimum output amount shown on screen bound the outcome. On
these three chains no amount is shown, so there is no bound and no disclosure:
a blind signature wearing a decoder's title.

Denying them routes those swaps to the generic raw-calldata path -- AdvancedMode
gated, bytes on screen. The owner can still sign; the device simply stops
claiming it understood. Ethereum, BNB Chain and Polygon keep decoded signing,
where the amounts actually render.

This is the near-term half. The underlying defect is the narrow chain id
(#455); once the token table carries a full-width one these three can be
restored with their amounts rendering, and the test says so.

Verified: ARM cross-compile in the pinned builder image; full firmware-unit +
board-unit suites green; cppcheck clean under CI's exact invocation (0
findings); clang-format clean.

Refs #455
@BitHighlander

Copy link
Copy Markdown
Owner Author

Blast radius, checked and intentional: this gates two decoders, not one.

zx_isExchangeProxyChain() has two callers:

  • lib/firmware/ethereum_contracts/zxtransERC20.c:46 — 0x transformERC20
  • lib/firmware/ethereum_contracts/zxswap.c:46 — 0x zxSwap (Uniswap/Sushiswap routing)

Both share the defect, so both should be gated. zxswap.c:116-117 calls tokenByChainAddress(msg->chain_id, …) for the from/to tokens and formats them through ethereumFormatAmount() at :127,129 — the identical path that short-circuits to "Unknown token value" when the truncated chain id misses the table. On Base, Arbitrum and Avalanche its screen degrades the same way:

Uniswap
Sell Unknown token value
Min buy Unknown token value

So the change routes both 0x decoders to the raw-calldata path on those three chains, which is the intended outcome — the remedy tracks the defect, not one symptom of it.

zxliquid.c does not call the allowlist and is unaffected.

@BitHighlander

Copy link
Copy Markdown
Owner Author

No existing test breaks — and that is the point.

Every 0x test in the pinned suite (deps/python-keepkey/tests/test_msg_ethereum_erc20_0x_signtx.py) uses chain_id=1: lines 44, 77, 111, 181. Nothing anywhere in tests/ uses 8453, 42161 or 43114. So this change breaks nothing.

That absence is also the reason the defect shipped. The decoder was extended to six chains, and the test suite only ever exercised the one where the token table happens to work. On the other five the screen was never looked at — and on three of them it renders "Unknown token value" for both amounts.

Worth adding as part of #455: when the chain id is widened and these chains are restored, the restoring change should carry a test that asserts the amounts actually render on each allowlisted chain, not merely that the chain is accepted. zx_isExchangeProxyChain(8453) == true would have passed the whole time while the screen said nothing.

CI note for this PR: the first run's lint-format was cancelled inside its apt.llvm.org install step at the 3-minute job timeout, which silently SKIPPED every build and test job (#471). Re-run is green through all four gate jobs and Stage-2 is running.

@BitHighlander

Copy link
Copy Markdown
Owner Author

Superseded by #472, which fixes the same problem on the correct axis.

Measuring the generated token tables showed the chain allowlist was treating a symptom:

chain token entries
Ethereum (1) 1924
BSC (56) 3
Polygon (137) 3
Base (8453) 0
Arbitrum (42161) 0
Avalanche (43114) 0

Denying three chains would have left the identical hole on the three it kept — BSC and Polygon carry three tokens each, so any 0x swap there involving a fourth renders "Unknown token value" exactly as Base would — and it would have needed a manual edit to undo once tables were added.

#472 gates on whether both token lookups actually resolve, which is chain-agnostic, data-agnostic, costs no flash, and self-corrects as tables change.

Closing unmerged. Nothing here is lost: the allowlist stays as it was (all six chains), and the Base/Arbitrum/Avalanche behaviour this PR wanted is now enforced by the resolution gate instead, with tests covering it.

@BitHighlander
BitHighlander deleted the fix/0x-allowlist-drop-truncating-chains branch August 17, 2026 21:59
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