Appearance
CustodyFacet dual-sig guard — signer ≠ arbiter
Closed the one contract-level EVM↔Solana fund-path asymmetry from the EVM↔Solana parity sweep. The CustodyFacet disputed-release dual-sig could silently collapse to single-sig if the owner set custodySigner == arbiter — whereas Solana's numero-custody enforces require_keys_neq!(signer, arbiter) at init. The sweep found all four gaps were EVM-lacking-a-Solana- hardening (the expected shape, since the earlier hardening pass only ran against the Solana cage).
What shipped
- Setter guards —
setCustodySigner/setCustodyArbiternowrequire(signer != arbiter), rejecting the collapsed config at write time (mirrors Solana's init-time check). No release-time guard: the setter guard makessigner == arbiterunsettable, andreleaseDisputed's existingarbiter != address(0)check blocks the only other equal state (both-zero at init) — so a release-time guard would be unreachable and untestable. - Cut live on both testnet diamonds via
scripts/add-custody-facet.ts(loupe-classified EIP-2535 Replace — a setter-only change, so 11 selectors Replaced / 0 Added): Base Sepolia CustodyFacet0x8fa279e3…(diamondCut0x9ef28975…), Arbitrum Sepolia0x39f63b6d…(0xf14f362e…). Pre-cut reads confirmed both live configs were already DISTINCT (custodySigner= GAME_SIGNER0x729655b3…≠arbiter= deployer placeholder0x09Df7e9f…), so there was no pre-existing collapse to remediate. - Contract test:
CustodyFacet.test.tsgains theF4case (both setters reject the equal config); the existing E-CUST-3 dual-sig release stays green as the no-regression check (12 passing).
Also
- The contracts deploy env moved into managed configuration, retiring the hand-copied
.env.localthat was being carried between worktrees.
Deferred
Two backend deposit-side asymmetries from the same sweep — a deposit credit-gate at the API boundary and a tournament seats > 0 validation — are low practical risk (durable-intent reconciler mitigation / admin-only table creation) and were deferred.