Skip to content

bug(genesis): enforceOperatorsNotProxyAdmin and role uniqueness are case-sensitive #411

Description

@kutluhaneth46

Summary

enforceOperatorsNotProxyAdmin compares operator addresses to proxy.admin with strict ===, and several genesis uniqueness checks (Set of addresses) are also case-sensitive. Ethereum addresses are case-insensitive; schemaAddress accepts any hex case and does not normalize. The same address written with different casing therefore bypasses both the proxy-admin collision check and role-uniqueness checks.

Why this matters

FiatTokenProxy (and the other admin proxies) reject fallback calls from the proxy admin. Genesis validation exists specifically so operator roles cannot be set to proxy.admin. A config that uses EIP-55 checksum for proxy.admin and lowercase for owner / pauser / rescuer / a minter / a controller is the same address on-chain, but today it passes schemaNativeFiatToken / schemaProtocolConfig / schemaValidatorManager / schemaDenylist and produces a genesis where those roles cannot call through the proxy.

Committed network configs appear to use consistent casing today, so this is a validation gap rather than a live mainnet misconfiguration. It is still a footgun for any future config author or tooling that mixes checksummed and lowercase addresses.

Reproduction

Against current main (2a3e8ab):

import { schemaNativeFiatToken } from './scripts/genesis/NativeFiatToken'

const adminChecksum = '0xAaaaAaAaAaaAaaAaaAaaAaaAaaAaaAaaAaaAaaAa'
const adminLower = '0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa'

const base = {
  proxy: { admin: adminChecksum },
  owner: '0x2222222222222222222222222222222222222222',
  pauser: '0x3333333333333333333333333333333333333333',
  blacklister: '0x4444444444444444444444444444444444444444',
  masterMinter: '0x5555555555555555555555555555555555555555',
  rescuer: '0x6666666666666666666666666666666666666666',
  minters: [{ address: '0x7777777777777777777777777777777777777777', allowance: 1n }],
}

schemaNativeFiatToken.safeParse({ ...base, owner: adminChecksum }).success
// false — correctly rejected

schemaNativeFiatToken.safeParse({ ...base, owner: adminLower }).success
// true — BUG: same address, different case, accepted

The same pattern applies to minter uniqueness: two minter entries that differ only by case are both accepted.

Root cause

scripts/genesis/types.ts:

  • schemaAddress is z.string().regex(/^0x[0-9a-fA-F]{40}$/) with no case normalization.
  • enforceOperatorsNotProxyAdmin uses value === proxyAdmin.

Uniqueness Sets in NativeFiatToken (minters) and ValidatorManager (registerers / controllers) store the raw string, so mixed-case duplicates slip through as well.

Note: addressToBigInt / storage writes are case-insensitive (hex → bigint), so the chain would store one address while validation thought they differed.

Expected behavior

Address equality and uniqueness in genesis validation should be case-insensitive (e.g. compare toLowerCase(), or normalize in schemaAddress, or use viem isAddressEqual).

Proposed fix

  1. Compare addresses case-insensitively inside enforceOperatorsNotProxyAdmin.
  2. Normalize keys for uniqueness Sets (minters, validatorRegisterers, controllers).
  3. Optionally lowercase inside schemaAddress so every downstream consumer sees one canonical form.
  4. Unit tests covering proxy-admin collision and uniqueness with mixed-case inputs.

I have the change and tests ready and am requesting assignment before opening a PR, per #405 / CONTRIBUTING.md.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions