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
- Compare addresses case-insensitively inside
enforceOperatorsNotProxyAdmin.
- Normalize keys for uniqueness
Sets (minters, validatorRegisterers, controllers).
- Optionally lowercase inside
schemaAddress so every downstream consumer sees one canonical form.
- 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.
Summary
enforceOperatorsNotProxyAdmincompares operator addresses toproxy.adminwith strict===, and several genesis uniqueness checks (Setof addresses) are also case-sensitive. Ethereum addresses are case-insensitive;schemaAddressaccepts 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 toproxy.admin. A config that uses EIP-55 checksum forproxy.adminand lowercase forowner/pauser/rescuer/ a minter / a controller is the same address on-chain, but today it passesschemaNativeFiatToken/schemaProtocolConfig/schemaValidatorManager/schemaDenylistand 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):The same pattern applies to minter uniqueness: two minter entries that differ only by case are both accepted.
Root cause
scripts/genesis/types.ts:schemaAddressisz.string().regex(/^0x[0-9a-fA-F]{40}$/)with no case normalization.enforceOperatorsNotProxyAdminusesvalue === proxyAdmin.Uniqueness
Sets inNativeFiatToken(minters) andValidatorManager(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 inschemaAddress, or use viemisAddressEqual).Proposed fix
enforceOperatorsNotProxyAdmin.Sets (minters, validatorRegisterers, controllers).schemaAddressso every downstream consumer sees one canonical form.I have the change and tests ready and am requesting assignment before opening a PR, per #405 / CONTRIBUTING.md.