Skip to content

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 guardssetCustodySigner / setCustodyArbiter now require(signer != arbiter), rejecting the collapsed config at write time (mirrors Solana's init-time check). No release-time guard: the setter guard makes signer == arbiter unsettable, and releaseDisputed's existing arbiter != 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 CustodyFacet 0x8fa279e3… (diamondCut 0x9ef28975…), Arbitrum Sepolia 0x39f63b6d… (0xf14f362e…). Pre-cut reads confirmed both live configs were already DISTINCT (custodySigner = GAME_SIGNER 0x729655b3…arbiter = deployer placeholder 0x09Df7e9f…), so there was no pre-existing collapse to remediate.
  • Contract test: CustodyFacet.test.ts gains the F4 case (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.local that 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.