Final Testnetexplorer K_J · Final Testnet · 48359
en

Contract

0x7f03b4517acac0d00cafdbdf547124bfd3259d24

Address
0x7f03b4517acac0d00cafdbdf547124bfd3259d24
Kind
verified contract FinalChainProxy
Balance
0 vETH
Nonce
1
Code
1,908 bytes codehash 0x420ce8b0c6dd8cf01518ef1ce8785a1273fff3dd76448139c4d935bb120a0c48

account tree

Tree
1 · accounts
Present
no leaf
Key
0x9044da3c9f7657607696a7b1985295892b8a3affc03e18af4d4160b652768017
Live root
0x6d72b53fa4ff33a006dbb1e62210da0466cea22c427258ba677493791d7c7734
This address holds no leaf in the account tree. Every Final Wallet — service identities included — has one, so an absent leaf means an ordinary account rather than a wallet.
transactionseventstoken transferscontract

source verified

Contract
FinalChainProxy exact match · immutables masked
Compiler
v0.8.33+commit.64118f21
Optimizer
enabled · 200 runs
EVM version
prague
Verified
2026-09-13T13:30:46.027Z
Provenance
preverify-final-chain (forge artifact, bytecode compared against live code)

contracts/IFinalAccessManager.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
//
// Change Date: 2029-01-01
// Change License: GPL-2.0-or-later
//
// Additional Use Grant:
// 1. Any person or entity may use this interface to integrate with the Final
//    DeFi Protocol's access plane.
// 2. Integrators, dApps, relayers, and end users may consume role and ownership
//    state through this interface as part of their integration with the Final
//    DeFi Protocol.
// 3. For the avoidance of doubt, this Grant does NOT permit the commercial
//    deployment of a Fork of this interface or a competing wallet/relayer
//    stack derived from it without permission prior to the Change Date.
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;

/**
 * @title Final Access Manager Interface
 * @notice The access plane every chain-global contract in this system consults, and the delayed-grant lifecycle
 *         that governs how membership in it changes.
 * @dev The plane recognizes one contract-level `owner` plus a fixed set of named roles (`FINAL_MANAGER`,
 * `FINAL_WALLET_MANAGER`, `FINAL_UPGRADE`, `FINAL_TREASURY`). Role identifiers are `keccak256` of the role name,
 * so they are stable across chains and upgrades and can be computed off chain without reading any contract.
 *
 * **The owner bypasses every role check.** `hasRole` answers true for the owner on any known role, so a role is
 * never a way to hold authority the owner does not also hold. Any power that must be separable from the owner
 * has to be kept in its own storage on the contract that needs it, not modelled as a role here.
 *
 * Roles carry an optional grant DELAY. When a role's delay is zero it is granted immediately; when it is
 * non-zero the grant must be queued, left to mature, and then applied, with cancellation available throughout.
 * Changing a delay does not retroactively re-time grants already queued: an entry keeps the readiness stamp it
 * was given, so an operator cannot shorten a maturing grant by lowering the delay underneath it.
 */
interface IFinalAccessManager {
    /// @notice Emitted when the grant delay for a role is updated.
    event RoleGrantDelaySet(bytes32 indexed role, uint64 delay);
    /// @notice Emitted when a delayed role grant is queued for `account`.
    event RoleGrantQueued(bytes32 indexed role, address indexed account, uint64 readyAt);
    /// @notice Emitted when a queued role grant is cancelled before apply.
    event RoleGrantCancelled(bytes32 indexed role, address indexed account);

    /// @notice The queued grant's `readyAt` has not been reached yet.
    error RoleGrantDelayNotMet();
    /// @notice No queued grant exists for the supplied `(role, account)` pair.
    error NoPendingGrant();
    /// @notice A queued grant already exists; cancel before re-queueing.
    error PendingGrantAlreadyQueued();
    /// @notice The role has a non-zero grant delay; use the queue/apply path.
    error DelayedRoleRequiresQueue();

    /// @notice Returns the contract-level owner with full authority.
    /// @return ownerAddress Current owner address.
    function owner() external view returns (address ownerAddress);

    /// @notice Returns the proposed next owner awaiting acceptance, or zero if none.
    /// @dev Set during a two-step ownership transfer; the address must itself call
    /// `acceptOwnership` within the timelock window to finalize.
    /// @return pendingOwnerAddress Address that must accept to become the new owner.
    function pendingOwner() external view returns (address pendingOwnerAddress);

    /// @notice Returns true if `account` holds `role`.
    /// @param role Role identifier to check.
    /// @param account Account to validate.
    /// @return hasRoleFlag Whether the role is present.
    function hasRole(bytes32 role, address account) external view returns (bool hasRoleFlag);

    /// @notice Returns true if `account` holds either `primaryRole` or `secondaryRole`.
    /// @param primaryRole First role identifier to check.
    /// @param secondaryRole Second role identifier to check.
    /// @param account Account to validate.
    /// @return hasAnyRoleFlag Whether any role is present.
    function hasAnyRole(
        bytes32 primaryRole,
        bytes32 secondaryRole,
        address account
    ) external view returns (bool hasAnyRoleFlag);

    /// @notice Returns the configured grant delay (seconds) for `role`.
    /// @param role Role identifier to inspect.
    /// @return delay Grant delay in seconds; `0` means immediate.
    function roleGrantDelay(bytes32 role) external view returns (uint64 delay);

    /// @notice Returns the queued grant's readiness timestamp for the pair.
    /// @param role Role identifier.
    /// @param account Account whose queue slot is being inspected.
    /// @return readyAt Unix timestamp at which `applyGrantRole` is allowed; `0` if no entry is queued.
    function queuedRoleReadyAt(bytes32 role, address account) external view returns (uint64 readyAt);

    /// @notice Sets the grant delay (seconds) for `role`. Zero disables the queue.
    /// @param role Role identifier to configure.
    /// @param delay Grant delay in seconds.
    function setRoleGrantDelay(bytes32 role, uint64 delay) external;

    /// @notice Queues a delayed grant of `role` to `account`.
    /// @param role Role identifier.
    /// @param account Account to grant once the delay elapses.
    function queueGrantRole(bytes32 role, address account) external;

    /// @notice Applies a previously queued grant of `role` to `account`.
    /// @param role Role identifier.
    /// @param account Account whose queued grant is being applied.
    function applyGrantRole(bytes32 role, address account) external;

    /// @notice Cancels a pending queued grant of `role` to `account`.
    /// @param role Role identifier.
    /// @param account Account whose queued grant is being cancelled.
    function cancelGrantRole(bytes32 role, address account) external;
}

contracts/finalchain/FinalCertificate.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
//
// Change Date: 2029-01-01
// Change License: GPL-2.0-or-later
//
// Additional Use Grant:
// 1. Any person or entity may link against and call this certificate reader,
//    and may encode certificates that it accepts, as part of the Final DeFi
//    Protocol.
// 2. Operators, integrators, and end users may have their certificates parsed,
//    self-checked, and verified through any Final DeFi surface that links it.
// 3. For the avoidance of doubt, this Grant does NOT permit the commercial
//    deployment of a Fork of this certificate reader or a competing identity
//    certificate format derived from it without permission prior to the
//    Change Date.
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;

import {FinalChainPrecompiles} from "./FinalChainPrecompiles.sol";
import {FinalChainTime} from "./FinalChainTime.sol";

/**
 * @title Final Certificate
 * @notice Reads a Final Certificate on chain and self-checks it, so a certificate's keys can never be
 *         anything other than the keys it declares.
 * @dev Deployed only as part of this project's own reth-based state plane, and only on the reth-based chains
 *      that carry the precompiles it calls: SHA3-256 at `0x0202`, ML-DSA-87 at `0x0204` and
 *      SLH-DSA-SHAKE-256s at `0x0205`, each address being that primitive's FIPS number. The contracts it is
 *      linked into probe those precompiles at construction and refuse to exist where they are absent, so
 *      this library never runs somewhere its verdicts would be meaningless. It takes part in no CREATE2
 *      derivation, and nothing outside this directory imports it.
 *
 *      The SHA3 precompile is not a convenience: the certificate format hashes with FIPS-202 SHA3 and the
 *      EVM's `keccak256` is a DIFFERENT function, so a digest computed with the wrong one matches no
 *      certificate any issuer ever wrote.
 *
 *      ## Why the chain parses this at all
 *
 *      The alternative is taking the TBS bytes and the public keys as separate arguments and deriving
 *      `certHash` from the bytes. That looks like verification and is not: nothing compares the keys to the
 *      certificate, so a registrar could bind any certificate to any keypair, the registry would hold a key
 *      the certificate does not contain, and every signature that key produced would verify against a
 *      certificate that never authorised it.
 *
 *      So the keys are read OUT of the certificate. There is one input, and no pair of arguments that can
 *      disagree.
 *
 *      Gas is deliberately not a design constraint on the chain this runs on and must not be optimised for.
 *      Parsing and re-hashing on chain costs more than trusting a parse done elsewhere and buys a verdict
 *      that is re-derivable from public state, which is the trade this whole plane is built on.
 *
 *      ## The key-identifier check
 *
 *      A certificate declares `SubjectKeyId` as the SHA3-256 digest of its `PublicKeyBlock`. Having parsed
 *      that block, {parse} recomputes the digest and compares. The field sits inside the TBS, so it is
 *      covered by the issuer's signatures — which makes the check a statement about what the issuer
 *      attested, not merely about internal consistency of bytes the caller supplied.
 *
 *      ## Deploy-linked, not inlined
 *
 *      {parseLive}, {parseRecovery}, {parseCa} and {verifyIssuerSignatures} are `external`, so the identity
 *      registry calls them across a link boundary rather than carrying them in its own bytecode, which it
 *      has no room for. The link target is fixed at deployment: a linked library is code, not a pointer
 *      anyone can move afterwards.
 *
 *      ## What this library deliberately does not do
 *
 *      It does not verify an issuer's signatures over the TBS as part of parsing, and it does not walk a
 *      certificate chain to the root. On the registration path there is nothing to walk — a chain-attested
 *      certificate is admitted by this chain against pinned issuer constants and the holder's own proof of
 *      possession, so an issuer signature is not what makes it valid. {verifyIssuerSignatures} is here for
 *      callers verifying an off-chain issuance, and it verifies exactly what it is handed.
 *
 *      It also does not check an encapsulation key's length or structure. Those are checked where they are
 *      REGISTERED, by the precompiles that own the answer, because two checks of one thing in two shapes is
 *      how one of them ends up weaker and nobody notices which.
 */
library FinalCertificate {
    /// @notice The four magic bytes every certificate opens with, `"PQCF"`.
    uint32 internal constant MAGIC = 0x50514346;
    /// @notice The current wire generation, which encoders write.
    /// @dev A generation this parser does not know fails to parse rather than being reinterpreted: the
    ///      folded key commitment, and therefore every wallet address, derives from this exact layout, so a
    ///      layout read under the wrong generation would produce a self-consistent digest that matches
    ///      nothing.
    uint32 internal constant VERSION = 2;
    /// @notice The previous wire generation, still accepted on parse.
    /// @dev Reading an older artifact is not the same as admitting it. Whether such a certificate may be
    ///      REGISTERED is settled at admission, by the holder's proof of possession and the chain-issuer
    ///      pins, rather than by refusing to decode it.
    uint32 internal constant VERSION_V4 = 1;

    /// @notice The institution identity extension, which carries an issuer's legal name, registration
    ///         number and jurisdiction.
    uint16 internal constant EXT_INSTITUTION = 0x0102;

    /// @notice ML-KEM-1024 (FIPS 203), the lattice half of the encapsulation pair.
    /// @dev Algorithm identifiers ARE the FIPS numbers, in one space shared by signatures and encapsulation
    ///      — the same identifiers the quorum wire format uses, and the numbers the precompile addresses end
    ///      in. One space rather than two means an identifier can never be read against the wrong table.
    uint16 internal constant ALG_ML_KEM_1024 = 0x0003;
    /// @notice ML-DSA-87 (FIPS 204). Transaction class.
    uint16 internal constant ALG_ML_DSA_87 = 0x0004;
    /// @notice SLH-DSA-SHAKE-256s (FIPS 205). Access class, and the seal.
    uint16 internal constant ALG_SLH_DSA_SHAKE_256S = 0x0005;
    /// @notice FN-DSA (FIPS 206). Reserved: there is no implementation behind it and it is never accepted in
    ///         a slot.
    uint16 internal constant ALG_FN_DSA = 0x0006;
    /// @notice HQC-5 (FIPS 207), the code-based half of the encapsulation pair.
    uint16 internal constant ALG_HQC_5 = 0x0007;

    /// @notice Certificate signing, for both of an issuer's keys.
    /// @dev Says which key to verify WITH; it grants nothing on its own — capability to issue comes from the
    ///      depth pair.
    uint16 internal constant PURPOSE_CERT_SIGNING = 0x0004;

    /// @notice The live stage's transaction-class slot, ML-DSA-87.
    /// @dev A wallet holds four slots in two stages of two, and a certificate carries ONE stage, never all
    ///      four. The stage is what is issued, rotated and revoked as a unit, and a holder presenting a live
    ///      certificate presents both of that stage's keys or neither — splitting them per slot would let
    ///      half a stage be presented as if it were whole.
    /// @dev This applies to services exactly as it applies to a user's wallet. A co-signer is a Final
    ///      Wallet: same four slots, same split, same algorithms. There is no second kind of identity in
    ///      this system.
    uint16 internal constant PURPOSE_ACTIVE_TX = 0x0010;
    /// @notice The live stage's access-class slot, SLH-DSA-SHAKE-256s.
    uint16 internal constant PURPOSE_ACTIVE_ACCESS = 0x0011;
    /// @notice The recovery stage's transaction-class slot, ML-DSA-87.
    uint16 internal constant PURPOSE_RECOVERY_TX = 0x0012;
    /// @notice The recovery stage's access-class slot, SLH-DSA-SHAKE-256s.
    uint16 internal constant PURPOSE_RECOVERY_ACCESS = 0x0013;
    /// @notice The live stage's encapsulation slot.
    /// @dev Each stage's encapsulation pair is resolved alongside its signing pair, and the identity
    ///      registry stores both halves, so a sender can encapsulate to a registered party without a second
    ///      lookup somewhere less authoritative. Both halves sit under ONE purpose and are told apart by
    ///      algorithm, which is why the key loop matches on the `(purpose, algorithm)` pair.
    uint16 internal constant PURPOSE_ACTIVE_KEM = 0x0014;
    /// @notice The recovery stage's encapsulation slot, carrying the same two algorithms.
    uint16 internal constant PURPOSE_RECOVERY_KEM = 0x0015;
    /// @notice The seal purpose: a second SLH-DSA-SHAKE-256s key that co-signs membership-class quorum
    ///         decisions (the registrar quorum); operational quorum actions take the ML-DSA-87 vote alone.
    /// @dev Distinct from the access key, and carried by SERVICE certificates only — a user's wallet never
    ///      seals. Optional in the format, so a certificate without it parses unchanged.
    /// @dev Outside the folded key commitment: a seal is operational, rotated by issuing a new live
    ///      certificate, and it must not move a wallet address it plays no part in deriving.
    uint16 internal constant PURPOSE_ACTIVE_SEAL = 0x0016;

    /// @notice A sentinel purpose no certificate can carry.
    /// @dev Lets {parse} be told "this stage has no encapsulation slot" without a second boolean argument.
    ///      `0xffff` is outside the purpose registry and is reserved by being used here.
    uint16 internal constant NO_KEM_PURPOSE = 0xffff;

    /// @notice Nanoseconds per millisecond, the conversion from a certificate's validity fields to this
    ///         chain's clock.
    /// @dev A certificate stamps validity in NANOseconds and this chain's clock is MILLIseconds, so the
    ///      parser divides by 1e6 on the way in and nothing downstream ever compares across units. Getting
    ///      the divisor wrong does not fail loudly: it shifts every window by three orders of magnitude, so
    ///      every certificate reads as already valid, including one issued for the future.
    uint64 internal constant NS_PER_MILLISECOND = FinalChainTime.NS_PER_MILLISECOND;

    /**
     * @title Parsed
     * @notice What the chain keeps out of one certificate.
     * @dev Every field is read OUT of the TBS. Nothing here can be supplied alongside the bytes, which is
     *      what makes it impossible for a caller to bind a certificate to material the certificate does not
     *      contain.
     */
    struct Parsed {
        /// `SHA3-256` of the TBS bytes: the certificate's own identity, and the handle revocation is keyed
        /// on.
        bytes32 certHash;
        /// The certificate's 32-byte serial. A serial is per certificate SET, so the two stages of one
        /// wallet share it and two stages that disagree are two different wallets.
        bytes32 serial;
        /// keccak256 of the issuer-name bytes, for the chain-issuer pin: a chain-attested certificate
        /// carries the chain's own constant issuer name, and the registry compares one hash rather than two
        /// strings.
        bytes32 issuerDnHash;
        /// The subject-name bytes verbatim. Kept whole rather than hashed because the jurisdiction rule
        /// reads its country component at issuer registration.
        bytes subjectDn;
        /// The institution extension's VALUE, when present; empty otherwise. Issuer registration parses
        /// the declared jurisdiction out of it and requires it to match the subject name's country.
        bytes institutionExt;
        /// SHA3-256 of the ISSUER's public key block. Zero-length — and so
        /// `bytes32(0)` here — for exactly one certificate in the hierarchy,
        /// which is what terminates chain validation.
        bytes32 authorityKeyId;
        /// SHA3-256 of this certificate's own public key block. The child's
        /// `authorityKeyId` must equal it, which is what links the two.
        bytes32 subjectKeyId;
        /// Position on the delegation axis; 0 is the chain's own root.
        uint8 depth;
        /// Deepest level this key may issue to. `== depth` means it signs no certificates at all, which is
        /// every end entity. The pair is immutable per certificate, which is why consumers discriminate
        /// record kinds by it rather than by a role bit.
        uint8 maxDelegationDepth;
        /// MILLISECONDS, converted from the schema's nanoseconds — this chain's clock.
        uint64 notBefore;
        /// Milliseconds. Zero means never expires, which the schema allows.
        uint64 notAfter;
        /// The stage's transaction-class key. ML-DSA-87 — spending, and every
        /// high-cadence protocol action.
        bytes transactionKey;
        /// The stage's access-class key. SLH-DSA-SHAKE-256s — identity,
        /// rotation, recovery-pair promotion. A different hardness assumption,
        /// so a lattice break leaves the key that governs identity standing.
        bytes accessKey;
        /// The stage's ML-KEM-1024 encapsulation key. Empty on a CA, which has
        /// no encapsulation stage, and on any v4 certificate issued without
        /// one — see `parse` for why that is tolerated rather than refused.
        bytes kemMlKem;
        /// The stage's HQC-5 encapsulation key. Carried under the SAME purpose
        /// as the lattice half and distinguished only by algorithm, which is
        /// why the parser matches on the `(purpose, algorithm)` pair.
        bytes kemHqc;
        /// The service's seal key (`PURPOSE_ACTIVE_SEAL`, SLH-DSA-SHAKE-256s).
        /// Empty on every certificate that does not carry one — a user wallet,
        /// a recovery stage, a CA.
        bytes sealKey;
        /// Where the TBS ends, so a caller holding the whole certificate can
        /// find the `SignatureBlock` without parsing forward again.
        uint256 tbsLength;
    }

    /// @notice The bytes do not open with the certificate magic, so they are not a certificate at all.
    /// @param got The four bytes that were present.
    error BadMagic(uint32 got);
    /// @notice The wire generation is one this parser does not read.
    /// @param got The generation the certificate declares.
    error BadVersion(uint32 got);
    /// @notice The TBS ends before a field the parser was about to read.
    /// @param needed The offset the read required.
    /// @param got The length actually supplied.
    error Truncated(uint256 needed, uint256 got);
    /// @notice The recomputed key-block digest does not equal the one the certificate declares, so the keys
    ///         present are not the keys the issuer attested.
    /// @param derived The digest recomputed from the key block.
    /// @param declared The digest the certificate carries.
    error SubjectKeyIdMismatch(bytes32 derived, bytes32 declared);
    /// @notice A stage is missing a key it must carry, or carries half of a pair that is issued whole.
    /// @param purpose The purpose whose slot is unfilled.
    error MissingSlot(uint16 purpose);
    /// @notice A slot carries a key of the wrong scheme. It would verify cryptographically and mean
    ///         something else entirely, which is exactly what splitting the classes exists to prevent.
    /// @param purpose The slot's purpose.
    /// @param algorithm The algorithm identifier that was present.
    error WrongAlgorithmForSlot(uint16 purpose, uint16 algorithm);
    /// @notice Two key entries share one `(purpose, algorithm)` pair, so one would silently shadow the
    ///         other.
    /// @param purpose The repeated purpose.
    /// @param algorithm The repeated algorithm identifier.
    error DuplicateKey(uint16 purpose, uint16 algorithm);
    /// @notice The key entries are not in ascending `(purpose, algorithm)` order. The schema requires that
    ///         order so `certHash` is reproducible across implementations.
    error KeysNotSorted();
    /// @notice A signing key whose length is not the one its algorithm defines.
    /// @param algorithm The algorithm identifier the entry declares.
    /// @param length The key length that was present.
    error BadKeyLength(uint16 algorithm, uint256 length);
    /// @notice A delegation bound shallower than the certificate's own depth, which admits nothing.
    /// @param depth The certificate's position on the delegation axis.
    /// @param maxDelegationDepth The deepest level it claims to issue to.
    error InvalidDepth(uint8 depth, uint8 maxDelegationDepth);
    /// @notice A certificate that expires no later than it begins.
    /// @param notBefore The declared start, in the schema's nanoseconds.
    /// @param notAfter The declared end, in the schema's nanoseconds.
    error ValidityInverted(uint64 notBefore, uint64 notAfter);

    /**
     * @notice Parse and self-check a `TBSCertificate`.
     * @dev Checking for a CAPABILITY rather than a type is the certificate schema's own rule, and the reason
     *      there is no type field to check instead. Passing the LIVE purposes to a recovery certificate
     *      finds neither key and reverts — which is what stops a recovery certificate being registered as a
     *      live one and handing the recovery pair everyday authority.
     *
     *      Self-check means the declared `SubjectKeyId` is recomputed from the key block that follows it and
     *      compared. That field is inside the TBS and therefore covered by the issuer's signatures, so the
     *      comparison turns "these bytes decode" into "the issuer attested these exact keys". Doing it on
     *      chain costs one precompile call and buys a verdict any reader can recompute; gas is not a design
     *      constraint on the chain this runs on, and must not be traded for a check that would then have to
     *      be taken on trust from whichever process ran it.
     *
     *      A stage is issued as a unit, so both of a stage's signing keys must be present, and its
     *      encapsulation pair must be present in full or absent in full.
     * @param tbs the TBS bytes, verbatim. Not the whole certificate.
     * @param txPurpose the transaction-class purpose this stage should carry.
     * @param accessPurpose the access-class purpose for the same stage.
     * @param kemPurpose the encapsulation purpose for the same stage, or {NO_KEM_PURPOSE} for a stage that
     *        has none.
     * @return out The parsed certificate: digest, serial, names, key identifiers, depth pair, validity
     *         window, and every key slot the stage carries.
     */
    function parse(bytes calldata tbs, uint16 txPurpose, uint16 accessPurpose, uint16 kemPurpose)
        internal
        view
        returns (Parsed memory out)
    {
        _need(tbs, 58);
        if (uint32(bytes4(tbs[0:4])) != MAGIC) revert BadMagic(uint32(bytes4(tbs[0:4])));
        // Both live wire generations parse. An artifact issued under the older one is read rather than
        // refused; whether it may be ADMITTED is a separate question, settled at registration by the
        // holder's proof of possession and the chain-issuer pins.
        uint32 wireVersion = uint32(bytes4(tbs[4:8]));
        if (wireVersion != VERSION && wireVersion != VERSION_V4) revert BadVersion(wireVersion);

        out.certHash = FinalChainPrecompiles.sha3_256(tbs);
        out.serial = bytes32(tbs[8:40]);
        out.depth = uint8(tbs[40]);
        out.maxDelegationDepth = uint8(tbs[41]);

        uint64 notBeforeNs = uint64(bytes8(tbs[42:50]));
        uint64 notAfterNs = uint64(bytes8(tbs[50:58]));
        if (out.maxDelegationDepth < out.depth) {
            revert InvalidDepth(out.depth, out.maxDelegationDepth);
        }
        if (notAfterNs != 0 && notAfterNs <= notBeforeNs) {
            revert ValidityInverted(notBeforeNs, notAfterNs);
        }
        out.notBefore = notBeforeNs / NS_PER_MILLISECOND;
        out.notAfter = notAfterNs == 0 ? 0 : notAfterNs / NS_PER_MILLISECOND;

        // Four length-prefixed fields: IssuerDN, SubjectDN, AuthorityKeyId,
        // SubjectKeyId. Every field before them is fixed width, which is the
        // whole reason the schema orders them this way.
        uint256 p = 58;
        uint256 issuerDnLen;
        (p, issuerDnLen) = _skipLengthPrefixed(tbs, p);
        out.issuerDnHash = keccak256(tbs[p - issuerDnLen:p]);
        uint256 subjectDnLen;
        (p, subjectDnLen) = _skipLengthPrefixed(tbs, p);
        out.subjectDn = tbs[p - subjectDnLen:p];
        uint256 akidLen;
        (p, akidLen) = _skipLengthPrefixed(tbs, p);
        out.authorityKeyId = _bytes32At(tbs, p - akidLen, akidLen);
        uint256 skidLen;
        (p, skidLen) = _skipLengthPrefixed(tbs, p);
        uint256 skidStart = p - skidLen;

        _need(tbs, p + 2);
        uint16 keyCount = uint16(bytes2(tbs[p:p + 2]));
        p += 2;
        // AFTER the count word. `SubjectKeyId` is SHA3-256 of the KeyEntry
        // array alone — `encodeTbs` writes `PublicKeyCount` as its own field and
        // `encodePublicKeyBlock` returns only the entries. Hashing the count in
        // produces a digest that is self-consistent and matches no certificate
        // any issuer ever wrote.
        uint256 blockStart = p;

        uint32 previousSort = 0;
        for (uint256 i = 0; i < keyCount; i++) {
            _need(tbs, p + 8);
            uint16 alg = uint16(bytes2(tbs[p:p + 2]));
            uint16 purpose = uint16(bytes2(tbs[p + 2:p + 4]));
            uint32 keyLen = uint32(bytes4(tbs[p + 4:p + 8]));
            p += 8;
            _need(tbs, p + keyLen);

            // Ascending by (purpose, algorithm), duplicates invalid. The schema
            // requires the order so `certHash` is reproducible across
            // implementations; enforcing it here also means a second entry for
            // one slot cannot quietly shadow the first.
            uint32 sortKey = (uint32(purpose) << 16) | uint32(alg);
            if (i > 0) {
                if (sortKey == previousSort) revert DuplicateKey(purpose, alg);
                if (sortKey < previousSort) revert KeysNotSorted();
            }
            previousSort = sortKey;

            // The algorithm is pinned per CLASS, not merely recorded. A
            // transaction slot carrying an access-class key would verify
            // cryptographically and mean something entirely different — an
            // identity key must never authorize a transaction, or splitting the
            // classes buys nothing.
            // Matched on the PAIR, not on the purpose alone. A CA carries two
            // keys under one purpose (`0x0004`) distinguished only by
            // algorithm, so matching on purpose first would find the first of
            // them twice and the second never.
            if (purpose == txPurpose && alg == ALG_ML_DSA_87) {
                if (keyLen != FinalChainPrecompiles.ML_DSA_87_PUBLIC_KEY_LEN) {
                    revert BadKeyLength(alg, keyLen);
                }
                out.transactionKey = tbs[p:p + keyLen];
            } else if (purpose == accessPurpose && alg == ALG_SLH_DSA_SHAKE_256S) {
                if (keyLen != FinalChainPrecompiles.SLH_DSA_SHAKE_256S_PUBLIC_KEY_LEN) {
                    revert BadKeyLength(alg, keyLen);
                }
                out.accessKey = tbs[p:p + keyLen];
            } else if (purpose == kemPurpose && alg == ALG_ML_KEM_1024) {
                out.kemMlKem = tbs[p:p + keyLen];
            } else if (purpose == kemPurpose && alg == ALG_HQC_5) {
                out.kemHqc = tbs[p:p + keyLen];
            } else if (purpose == PURPOSE_ACTIVE_SEAL && alg == ALG_SLH_DSA_SHAKE_256S) {
                if (keyLen != FinalChainPrecompiles.SLH_DSA_SHAKE_256S_PUBLIC_KEY_LEN) {
                    revert BadKeyLength(alg, keyLen);
                }
                out.sealKey = tbs[p:p + keyLen];
            } else if (purpose == PURPOSE_ACTIVE_SEAL) {
                // The seal is hash-based by definition — it exists to stand on
                // the OTHER assumption from the transaction key it co-signs
                // with. A lattice seal would be two signatures on one bet.
                revert WrongAlgorithmForSlot(purpose, alg);
            } else if (purpose == txPurpose || purpose == accessPurpose) {
                // A slot the caller asked for, carrying the wrong scheme. It
                // would verify cryptographically and mean something else
                // entirely — an identity key must never authorize a
                // transaction, or splitting the classes buys nothing.
                revert WrongAlgorithmForSlot(purpose, alg);
            } else if (purpose == kemPurpose) {
                // Same rule for the encapsulation slot. A third KEM appearing
                // under this purpose is a hybrid whose second family nobody
                // agreed on, and admitting it silently is how a pair becomes a
                // trio that one reader honours and another ignores.
                revert WrongAlgorithmForSlot(purpose, alg);
            }

            // NO length check on the KEM keys here, and that is deliberate.
            // The signing slots are checked against a constant because the
            // parser's own callers depend on the length; an encapsulation key
            // is checked by `0x0203` / `0x0207` at the moment it is REGISTERED,
            // where the answer is a well-formedness verdict rather than a
            // parse failure. Two checks of the same thing in two shapes is how
            // one of them ends up weaker and nobody notices which.
            p += keyLen;
        }

        // `SubjectKeyId` is SHA3-256 of the KeyEntry array, count word
        // EXCLUDED — `blockStart` is taken after the count is consumed, for the
        // reason given where it is set. Recomputing it is what turns "these
        // bytes decode" into "the CA signed these exact keys"; the field is
        // inside the TBS, so it is covered by the signatures.
        out.subjectKeyId = FinalChainPrecompiles.sha3_256(tbs[blockStart:p]);
        bytes32 declared = _bytes32At(tbs, skidStart, skidLen);
        if (out.subjectKeyId != declared) revert SubjectKeyIdMismatch(out.subjectKeyId, declared);

        // Both or neither. A stage is issued as a unit, so a certificate
        // carrying one of its two keys is not a partial certificate — it is a
        // certificate for a stage that does not exist.
        if (out.transactionKey.length == 0) revert MissingSlot(txPurpose);
        if (out.accessKey.length == 0) revert MissingSlot(accessPurpose);

        // The encapsulation pair is both-or-neither for the same reason, and
        // the reason is louder here: a hybrid quietly reduced to one family is
        // identical on the wire, so a certificate carrying only the lattice
        // half would seal successfully and silently drop the code-based hedge.
        // Neither is the CA case and the pre-v4 case, both legitimate.
        if ((out.kemMlKem.length == 0) != (out.kemHqc.length == 0)) {
            revert MissingSlot(kemPurpose);
        }

        _need(tbs, p + 2);
        uint16 extCount = uint16(bytes2(tbs[p:p + 2]));
        p += 2;
        for (uint256 i = 0; i < extCount; i++) {
            _need(tbs, p + 7);
            uint16 extType = uint16(bytes2(tbs[p:p + 2]));
            uint32 valueLen = uint32(bytes4(tbs[p + 3:p + 7]));
            p += 7;
            _need(tbs, p + valueLen);
            // The Institution extension's VALUE, kept for the issuer
            // profile's jurisdiction rule. Everything else is skipped as
            // before — extensions are structural to certHash, semantic to
            // whichever consumer knows them.
            if (extType == EXT_INSTITUTION) out.institutionExt = tbs[p:p + valueLen];
            p += valueLen;
        }
        out.tbsLength = p;
    }

    /// @notice Parse a LIVE-stage certificate: the live transaction and access keys.
    /// @dev `external`, like the other three entry points below. The identity registry sits against the
    ///      deployed-code ceiling and this parser is its single largest inlined dependency, so the four doors
    ///      it calls are DEPLOY-LINKED: the library is one more contract in the state plane's fixed deploy
    ///      order, and its address is baked immutably into the registry's bytecode. A linked library is code,
    ///      not a key — nothing can repoint it after deployment, so the split costs a call boundary and no
    ///      trust.
    /// @param tbs The TBS bytes, verbatim.
    /// @return The parsed and self-checked certificate.
    function parseLive(bytes calldata tbs) external view returns (Parsed memory) {
        return parse(tbs, PURPOSE_ACTIVE_TX, PURPOSE_ACTIVE_ACCESS, PURPOSE_ACTIVE_KEM);
    }

    /// @notice Parse a RECOVERY-stage certificate.
    /// @dev The recovery pair authorizes rotating the wallet's own credentials and NOTHING else. Acting as a
    ///      guardian is an ordinary action for that account and uses the live access key, so keeping the two
    ///      stages in separate certificates is what makes that boundary something a verifier can see.
    /// @param tbs The TBS bytes, verbatim.
    /// @return The parsed and self-checked certificate.
    function parseRecovery(bytes calldata tbs) external view returns (Parsed memory) {
        return parse(tbs, PURPOSE_RECOVERY_TX, PURPOSE_RECOVERY_ACCESS, PURPOSE_RECOVERY_KEM);
    }

    /// @notice Parse a certificate authority's certificate, whose two keys are both cert-signing.
    /// @dev Both classes resolve to the same purpose, which is why {parse} matches on the
    ///      `(purpose, algorithm)` PAIR: an authority carries two keys under one purpose and matching on the
    ///      purpose alone would find the first of them twice and the second never.
    /// @dev No encapsulation purpose. An authority signs and is never sealed to, so {NO_KEM_PURPOSE} is
    ///      passed as a value the key loop can never match. An authority certificate carrying encapsulation
    ///      keys would parse them into slots the registry then discards, which is a shape worth refusing to
    ///      have at all.
    /// @param tbs The TBS bytes, verbatim.
    /// @return The parsed and self-checked certificate.
    function parseCa(bytes calldata tbs) external view returns (Parsed memory) {
        return parse(tbs, PURPOSE_CERT_SIGNING, PURPOSE_CERT_SIGNING, NO_KEM_PURPOSE);
    }

    /**
     * @notice Verify an issuer's dual signature over a TBS.
     * @dev Both must verify, not either. Two signatures under two different hardness assumptions is the
     *      entire reason a certificate carries two, and accepting one would collapse that to whichever
     *      family breaks first.
     *
     *      Provided for callers that verify an off-chain issuance against keys they already trust. The
     *      caller supplies the issuer's keys, so it is the caller's job to have taken them from a registered
     *      record rather than from its own calldata — a key handed in with the signature proves nothing.
     * @param tbs The signed TBS bytes.
     * @param issuerMlDsaKey The issuer's registered ML-DSA-87 cert-signing key.
     * @param issuerSlhDsaKey The issuer's registered SLH-DSA-SHAKE-256s cert-signing key.
     * @param mlDsaSignature The lattice signature over `tbs`.
     * @param slhDsaSignature The hash-based signature over `tbs`.
     * @return Whether both signatures verify.
     */
    function verifyIssuerSignatures(
        bytes memory tbs,
        bytes memory issuerMlDsaKey,
        bytes memory issuerSlhDsaKey,
        bytes memory mlDsaSignature,
        bytes memory slhDsaSignature
    ) external view returns (bool) {
        return FinalChainPrecompiles.verifyMlDsa87(issuerMlDsaKey, tbs, mlDsaSignature)
            && FinalChainPrecompiles.verifySlhDsa(issuerSlhDsaKey, tbs, slhDsaSignature);
    }

    /// @notice Refuse a TBS that is shorter than the parser is about to read.
    /// @dev Called before every read rather than once at the top, because the layout is variable-length: a
    ///      certificate can be well-formed up to its key block and truncated inside it, and a parser that
    ///      only checked the fixed header would read whatever calldata followed.
    /// @param tbs The TBS bytes.
    /// @param upto The offset the next read needs to be valid.
    function _need(bytes calldata tbs, uint256 upto) private pure {
        if (tbs.length < upto) revert Truncated(upto, tbs.length);
    }

    /// @notice Step over one four-byte-length-prefixed field and report where it was.
    /// @dev Bounds-checks the prefix before reading it and the value before returning, so a truncated
    ///      certificate cannot make the cursor run past the end of calldata. The caller recovers the value's
    ///      slice as `tbs[next - length:next]`.
    /// @param tbs The TBS bytes.
    /// @param p Offset of the length prefix.
    /// @return next Offset just past the field's value.
    /// @return length The field's declared length.
    function _skipLengthPrefixed(bytes calldata tbs, uint256 p)
        private
        pure
        returns (uint256 next, uint256 length)
    {
        _need(tbs, p + 4);
        length = uint32(bytes4(tbs[p:p + 4]));
        next = p + 4 + length;
        _need(tbs, next);
    }

    /// @notice Read a key identifier out of the TBS as one word.
    /// @dev Answers `bytes32(0)` for any length other than 32 rather than reverting. A key identifier that
    ///      is not 32 bytes is not a SHA3-256 digest, so it cannot match the value it is compared against,
    ///      and the comparison at the call site produces the correct refusal with no separate error to
    ///      define. The one legitimate short case is a zero-length authority key identifier, which the
    ///      caller must reject on its own terms.
    /// @param tbs The TBS bytes.
    /// @param start Offset of the field's value.
    /// @param length The field's declared length.
    /// @return The 32-byte value, or zero when the field is not 32 bytes long.
    function _bytes32At(bytes calldata tbs, uint256 start, uint256 length)
        private
        pure
        returns (bytes32)
    {
        // A SubjectKeyId that is not 32 bytes is not a SHA3-256 digest, so it
        // cannot match and the comparison will fail — which is the correct
        // outcome and needs no separate error.
        if (length != 32) return bytes32(0);
        return bytes32(tbs[start:start + 32]);
    }
}

contracts/finalchain/FinalChainInitializable.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
pragma solidity ^0.8.20;

import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol";

/**
 * @title Final Chain Initializable
 * @notice The once-only initializer of a Final Chain state-plane contract that stands behind `FinalChainProxy`
 *         (ruled 2026-09-12: every plane contract does).
 *
 * @dev The proxy never re-runs an implementation's constructor, so a constructor that writes STORAGE — the
 *      trees' zero-hash ladder and live roots, a bootstrap admin, the supply's 100M — would leave the proxy's
 *      storage empty: the writes land in the implementation, which nothing reads through. Such a contract
 *      moves those writes into one internal `_setUp(...)` guarded by {initializer} and calls it from BOTH
 *      places: its constructor (a direct deploy — every Foundry fixture, every test — behaves exactly as
 *      before, and the bare implementation marks its OWN storage initialized, so nobody can initialize it
 *      later) and an external `initialize(...)`, which `FinalChainProxy`'s constructor runs by `delegatecall`
 *      in the proxy's storage. Constructor immutables (`registry`, `trees`, …) need none of this: they live in
 *      the implementation's code and read as constants through the proxy.
 *
 *      The flag lives in a namespaced slot, not in Solidity storage: inheriting this contract shifts no
 *      layout, and an implementation upgraded in place can never collide with it. An upgrade that appends
 *      storage seeds it through a new guarded function of its own — `initialize` runs once per proxy, ever.
 *
 *      A proxy deployed WITHOUT its init data is a live hole: `initialize` is external and the first caller
 *      would be the admin. The deploy tool refuses to place a proxy whose implementation declares
 *      `initialize` without running it, and reads {initialized} back before it continues.
 */
abstract contract FinalChainInitializable {
    /// @dev `bytes32(uint256(keccak256("final.chain.initialized")) - 1)`.
    bytes32 private constant INITIALIZED_SLOT = 0x1bf7ff51edde3507ea8edc0d02272dc3e66fd14d0a75a234f844ee7b236829d2;

    /// @notice The contract's storage was set up — by its constructor (a direct deploy) or by `initialize`
    ///         through its proxy.
    event Initialized();

    /// @notice `initialize` ran already in this storage — the constructor's, or a proxy's, once.
    error AlreadyInitialized();

    /// @dev Guards the one function that replays the constructor's storage writes. Sets the flag BEFORE the
    ///      body so a re-entrant call from inside the body cannot run it twice.
    modifier initializer() {
        StorageSlot.BooleanSlot storage flag = StorageSlot.getBooleanSlot(INITIALIZED_SLOT);
        if (flag.value) revert AlreadyInitialized();
        flag.value = true;
        _;
        emit Initialized();
    }

    /// @notice Whether this storage was set up. False on a proxy whose init data was not run — the state the
    ///         deploy tool refuses.
    function initialized() external view returns (bool) {
        return StorageSlot.getBooleanSlot(INITIALIZED_SLOT).value;
    }
}

contracts/finalchain/FinalChainPrecompiles.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
//
// Change Date: 2029-01-01
// Change License: GPL-2.0-or-later
//
// Additional Use Grant:
// 1. Any person or entity may link this library into contracts deployed on a
//    Final DeFi Protocol chain in order to reach that chain's hash and
//    post-quantum signature-verification precompiles.
// 2. Integrators, node operators, and auditors may use it to reproduce and
//    independently re-verify any verdict those precompiles produced, as part of
//    their integration with the Final DeFi Protocol.
// 3. For the avoidance of doubt, this Grant does NOT permit the commercial
//    deployment of a Fork of this library or a competing state plane derived
//    from it without permission prior to the Change Date.
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;

/**
 * @title Final Chain Precompiles
 * @notice The three primitives Final Chain adds to the EVM, and the only
 *         supported way to reach them.
 *
 * @dev **These exist ONLY on Final Chain (chain id 48359).** They are provided
 * by this chain's own node binary, and
 * nothing at these addresses on Ethereum, Optimism or any other chain will
 * answer. A contract that calls them must be one that only ever runs here;
 * `assertAvailable` below is the cheap way to fail loudly rather than treat an
 * empty return as a verified signature.
 *
 * The addresses are the FIPS numbers, which is the whole allocation rule —
 * there is no local registry to consult and no way for two implementations to
 * disagree about where a primitive lives:
 *
 * | address | primitive | FIPS |
 * |---|---|---|
 * | `0x…0202` | SHA3-256 | 202 |
 * | `0x…0203` | ML-KEM-1024 key validation | 203 |
 * | `0x…0204` | ML-DSA-87 verify | 204 |
 * | `0x…0205` | SLH-DSA-SHAKE-256s verify | 205 |
 * | `0x…0207` | HQC-5 key validation | 207 |
 *
 * The two KEM addresses VALIDATE keys and do nothing else, for one reason:
 * encapsulation is a SENDER operation and decapsulation needs the secret key,
 * so neither belongs on a chain at all. Checking that a registered public key
 * is well-formed is hardening rather than a dependency, and nothing in this
 * system waits on it.
 *
 * HQC's number is 207. It had none when the KEM pair was chosen, which was the
 * one thing separating it from ML-KEM here — a primitive with no standard
 * number has no address under this rule, and inventing one would have been a
 * local convention masquerading as the global one.
 *
 * **No AEAD precompile, at any number.** The chain must never be able to
 * decrypt an intent, and checking a revealed body against its commitment is a
 * hash compare that `0x0202` already serves.
 *
 * ## Why this library refuses to take a public key from its caller
 *
 * It does take one — the primitives are pure functions and cannot do otherwise.
 * The rule lives one level up, in `FinalPqQuorum`: a key passed as an argument
 * proves nothing, because anyone holding a keypair can produce a valid
 * signature under it. Only a key read from `FinalIdentityRegistry` is evidence
 * about WHO signed. Every call site here must be able to answer "where did this
 * key come from" with "storage", never "calldata".
 *
 * ## `success` is not the answer
 *
 * A `staticcall` to a verifier returns two things and both matter. `success`
 * false means the call was malformed — usually a length bug in the caller — and
 * `success` true with a zero word means the signature did not verify. The
 * helpers below collapse both to `false` for the caller's convenience, which is
 * safe in that direction and only in that direction: treating a failed call as
 * a valid signature would be the whole security of the system.
 */
library FinalChainPrecompiles {
    /// @notice SHA3-256 (FIPS 202). NOT `keccak256`, which is the
    /// pre-standardisation padding and produces a different digest.
    address internal constant SHA3_256 = address(0x0202);
    /// @notice ML-DSA-87 verification (FIPS 204). Transaction-class keys.
    address internal constant ML_DSA_87 = address(0x0204);
    /// @notice SLH-DSA-SHAKE-256s verification (FIPS 205). Access-class keys.
    address internal constant SLH_DSA_SHAKE_256S = address(0x0205);

    /// @notice ML-KEM-1024 encapsulation-key validation (FIPS 203).
    /// @dev VALIDATES; it does not encapsulate. Runs FIPS 203 §7.2's own
    /// encapsulation-key check — the type check and the modulus check — and
    /// nothing else. Encapsulation is a sender operation and decapsulation
    /// needs the secret key, so neither belongs on a chain.
    address internal constant ML_KEM_1024 = address(0x0203);

    /// @notice HQC-5 public-key validation (FIPS 207).
    /// @dev Structural only: the length, and the three padding bits the
    /// encoding leaves beyond `n = 57637`. HQC has no cheap key-validity
    /// predicate and this does not pretend to one.
    address internal constant HQC_5 = address(0x0207);

    /// @notice ML-DSA-87 public key length. Round-3 Dilithium5 shares it.
    uint256 internal constant ML_DSA_87_PUBLIC_KEY_LEN = 2592;
    /// @notice ML-DSA-87 signature length. Round-3 Dilithium5 is 4595.
    uint256 internal constant ML_DSA_87_SIGNATURE_LEN = 4627;
    /// @notice SLH-DSA-SHAKE-256s public key length (`PK.seed ‖ PK.root`).
    uint256 internal constant SLH_DSA_SHAKE_256S_PUBLIC_KEY_LEN = 64;
    /// @notice SLH-DSA-SHAKE-256s signature length. The `f` set is 49,856.
    uint256 internal constant SLH_DSA_SHAKE_256S_SIGNATURE_LEN = 29792;

    /// @notice Thrown when a precompile is absent, i.e. this is not Final Chain
    /// or the node is stock reth rather than `final-reth`.
    error PrecompileUnavailable(address precompile);

    /**
     * @notice Reverts unless all five precompiles answer.
     * @dev Call this from a constructor. A contract whose security rests on PQ
     * verification must not deploy onto a chain that cannot perform it — the
     * failure mode otherwise is a quorum that reaches threshold with zero valid
     * signatures, discovered at the worst possible moment.
     *
     * The probe is SHA3-256 of the empty string, whose value is a published
     * FIPS 202 constant. It cannot be produced by an address with no code
     * (which returns empty) nor by `keccak256` (which gives a different digest
     * for the same input), so it distinguishes "the right precompile" from both
     * "nothing here" and "the wrong hash function".
     */
    function assertAvailable() internal view {
        bytes32 expected = 0xa7ffc6f8bf1ed76651c14756a061d662f580ff4de43b49fa82d80a4b80f8434a;
        (bool ok, bytes memory out) = SHA3_256.staticcall("");
        if (!ok || out.length != 32 || bytes32(out) != expected) {
            revert PrecompileUnavailable(SHA3_256);
        }
        // The two signature verifiers are probed by shape rather than by a
        // known-answer vector: a KAT here would put a 29,792-byte signature in
        // this contract's bytecode. A deliberately short input is a
        // *precompile error* by contract, so a FAILED call is the pass and a
        // silent success would mean something else is answering at the address.
        _probeRejectsShortInput(ML_DSA_87);
        _probeRejectsShortInput(SLH_DSA_SHAKE_256S);
        // The two KEM validators are probed the other way round, because they
        // are total by contract: a wrong length is a malformed KEY, which is
        // the question being asked, so they ANSWER rather than error. A
        // one-byte input must therefore come back as a well-formed `false`, and
        // a failed call means nothing is there.
        _probeAnswersFalse(ML_KEM_1024);
        _probeAnswersFalse(HQC_5);
    }

    /**
     * @dev A short input must make the precompile ERROR. The gas budget is the
     * whole subtlety.
     *
     * A reverting CONTRACT refunds the gas it did not use. A precompile that
     * returns an error consumes **everything forwarded to it** — and Solidity
     * forwards 63/64 of what is left by default. Two such probes in a
     * constructor therefore burn all but 1/4096 of the deployment's gas, and
     * the deploy fails with no revert data at all.
     *
     * That is not hypothetical: it is what happened the first time this ran
     * against a real `final-reth`, and no Foundry test could have caught it.
     * A mocked precompile is a contract, and a contract's `require` hands the
     * gas back.
     *
     * 5,000 is generous for a call that fails on a length check before any
     * cryptography runs, and small enough that both probes together are noise
     * against a deployment.
     */
    function _probeRejectsShortInput(address precompile) private view {
        bool ok;
        assembly ("memory-safe") {
            let ptr := mload(0x40)
            mstore8(ptr, 0x00)
            ok := staticcall(5000, precompile, ptr, 0x01, 0x00, 0x00)
        }
        if (ok) revert PrecompileUnavailable(precompile);
    }

    /**
     * @dev A one-byte input must come back as a well-formed zero word.
     *
     * The inverse of `_probeRejectsShortInput`, and the inversion is the point:
     * these two precompiles are TOTAL. Every byte string has an answer to "is
     * this a well-formed key", and for one byte the answer is no. A precompile
     * that errored here would be one that treats a malformed key as a caller
     * bug, which is the opposite of what a registry wants.
     *
     * Gas is bounded for the same reason as the other probe — an erroring
     * precompile consumes everything forwarded — even though the pass case
     * returns normally and refunds.
     */
    function _probeAnswersFalse(address precompile) private view {
        bool ok;
        bytes32 answer;
        assembly ("memory-safe") {
            let ptr := mload(0x40)
            mstore8(ptr, 0x00)
            ok := staticcall(5000, precompile, ptr, 0x01, ptr, 0x20)
            answer := mload(ptr)
        }
        if (!ok || answer != bytes32(0)) revert PrecompileUnavailable(precompile);
    }

    /**
     * @notice Is `encapsulationKey` a well-formed ML-KEM-1024 key?
     *
     * @dev The check a registry owes a sender. A malformed encapsulation key
     * stored on chain is an account whose intents cannot be sealed, and the
     * discovery happens at the first attempt to seal one — on the hybrid path,
     * as a pair silently reduced to one family, which is the failure with no
     * error attached.
     *
     * False rather than reverting on any shape, including the wrong length,
     * because the caller is asking a question and every input has an answer.
     */
    function isWellFormedMlKem1024(bytes memory encapsulationKey) internal view returns (bool) {
        return _validatesKey(ML_KEM_1024, encapsulationKey);
    }

    /// @notice Is `publicKey` a well-formed HQC-5 key?
    /// @dev Structural, and honestly partial — see the precompile. It catches a
    /// truncated key, a key from the wrong parameter set, and a tail carrying
    /// smuggled bytes, which are the three ways this goes wrong in practice.
    function isWellFormedHqc5(bytes memory publicKey) internal view returns (bool) {
        return _validatesKey(HQC_5, publicKey);
    }

    /// @dev A failed CALL is not a false answer. It means nothing is at the
    /// address — this is not Final Chain, or the node is stock reth — and
    /// reading it as "the key is malformed" would silently disable the check on
    /// exactly the deployment where it cannot run.
    function _validatesKey(address precompile, bytes memory key) private view returns (bool) {
        (bool ok, bytes memory out) = precompile.staticcall(key);
        if (!ok || out.length != 32) revert PrecompileUnavailable(precompile);
        return bytes32(out) != bytes32(0);
    }

    /// @notice FIPS 202 SHA3-256 over `data`.
    /// @dev The certificate schema hashes `TBSCertificate`, `SubjectKeyId` and
    /// `AuthorityKeyId` with this, so it is the only function that can check a
    /// `certHash` against the bytes it claims to summarise.
    function sha3_256(bytes memory data) internal view returns (bytes32 digest) {
        (bool ok, bytes memory out) = SHA3_256.staticcall(data);
        if (!ok || out.length != 32) revert PrecompileUnavailable(SHA3_256);
        digest = bytes32(out);
    }

    /// @notice Verify an ML-DSA-87 signature. False on any failure, including
    /// a malformed call.
    function verifyMlDsa87(bytes memory publicKey, bytes memory message, bytes memory signature)
        internal
        view
        returns (bool)
    {
        if (
            publicKey.length != ML_DSA_87_PUBLIC_KEY_LEN
                || signature.length != ML_DSA_87_SIGNATURE_LEN
        ) return false;
        return _verify(ML_DSA_87, publicKey, signature, message);
    }

    /// @notice Verify an SLH-DSA-SHAKE-256s signature. False on any failure.
    function verifySlhDsa(bytes memory publicKey, bytes memory message, bytes memory signature)
        internal
        view
        returns (bool)
    {
        if (
            publicKey.length != SLH_DSA_SHAKE_256S_PUBLIC_KEY_LEN
                || signature.length != SLH_DSA_SHAKE_256S_SIGNATURE_LEN
        ) return false;
        return _verify(SLH_DSA_SHAKE_256S, publicKey, signature, message);
    }

    /// @dev `publicKey ‖ signature ‖ message`, in that order. Both fixed-length
    /// fields come first so the message is unambiguously the remainder — the
    /// same reason the precompile takes no length prefix.
    function _verify(
        address precompile,
        bytes memory publicKey,
        bytes memory signature,
        bytes memory message
    ) private view returns (bool) {
        (bool ok, bytes memory out) =
            precompile.staticcall(abi.encodePacked(publicKey, signature, message));
        return ok && out.length == 32 && bytes32(out) != bytes32(0);
    }
}

contracts/finalchain/FinalChainProxy.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
pragma solidity ^0.8.20;

import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol";
import {FinalProxyBase} from "../proxy/FinalProxy.sol";
import {FinalIdentityRegistry} from "./FinalIdentityRegistry.sol";
import {FinalPqQuorum} from "./FinalPqQuorum.sol";

/**
 * @title Final Chain Proxy
 * @notice The upgradeable proxy in front of a Final Chain state-plane contract — the intent log and the
 *         settlement log first (ruled 2026-09-09). Delegation is the shared `FinalProxyBase`; the upgrade
 *         authority is the identity registry's sealed registrar quorum, the same gate every state-plane
 *         contract already answers to for its configuration.
 *
 * @dev Why not `FinalProxy`. The chain-global proxy of the execution chains binds two things this chain does
 *      not have: its initializer admits only the canonical CREATE2 deployer (the Final Chain has no ECDSA
 *      deployer and no CREATE2 derivation — the plane is deployed by a post-quantum identity), and its upgrade
 *      and control planes authorize through a `FinalAccessManager` that exists on no Final Chain. This contract
 *      keeps the delegation core and the ERC-1967 implementation slot and replaces the authority planes with
 *      the one authority the plane has.
 *
 *      What an upgrade is. `upgradeTo` burns one gate nonce on the registry and requires a sealed registrar
 *      quorum over the new implementation — ML-DSA-87 vote plus SLH-DSA seal, exactly-K approvals in ascending
 *      signer order, the anchor window, all enforced by `FinalPqQuorum.require_`. The verifying contract is
 *      THIS proxy (`msg.sender` inside the registry's gate), so an approval collected for one proxy can never
 *      be spent on another, and the per-caller counter is the replay protection. While the registry's
 *      bootstrap window is open the bootstrap admin upgrades alone, so the plane deploy can place and wire
 *      the proxies before the seal; after the seal only the quorum can.
 *
 *      What this proxy deliberately does not do. It holds no admin address, applies no timelock and knows no
 *      second role: delay and quorum belong to the roster the registry governs, not here, so the upgrade path
 *      stays small enough to audit whole. It also never re-runs an implementation's constructor — a Final Chain
 *      implementation keeps its plane-global pointers (`registry`, `trees`) as constructor immutables in ITS
 *      code, read through `delegatecall` as constants, which is exactly the property an upgrade must not move.
 *      An implementation's `address(this)` under delegation is this proxy, the address every co-signer and
 *      the fleet pin and every quorum digest binds.
 *
 *      Storage. This contract declares no Solidity storage; the implementation address lives in the ERC-1967
 *      slot, so an implementation's layout can never collide with it. The registry is an immutable of the
 *      proxy's own code: an upgrade cannot move the authority that authorizes upgrades.
 */
contract FinalChainProxy is FinalProxyBase {
    /// @notice The registry whose sealed registrar quorum authorizes upgrades. Set once, at deployment.
    FinalIdentityRegistry public immutable registry;

    /// @notice Action domain of an upgrade in the registrar quorum's digest — distinct from every state-plane
    ///         `configure` and `seed` domain, so an approval to configure a log can never be replayed to
    ///         replace its code.
    bytes32 public constant DOMAIN_PROXY_UPGRADE = keccak256("FINAL_CHAIN_PROXY_UPGRADE_v01");

    /// @dev ERC-1967 implementation slot: `bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1)`,
    ///      the same slot `FinalProxy` uses, so explorers and the deploy tool read one location for both.
    bytes32 private constant IMPLEMENTATION_SLOT = 0x360894A13BA1A3210667C828492DB98DCA3E2076CC3735A920A3CA505D382BBC;

    /// @notice The implementation changed.
    /// @param previous The implementation that served until this call.
    /// @param current The implementation that serves from this call.
    event Upgraded(address indexed previous, address indexed current);

    /// @notice A zero registry or a zero implementation cannot stand behind a proxy.
    error InvalidProxyArg();
    /// @notice The proposed implementation carries no code; delegating to it would succeed silently.
    error ImplementationHasNoCode(address implementation);
    /// @notice The proposed implementation is the one already installed; nothing to do and no nonce to burn.
    error ImplementationUnchanged(address implementation);

    /**
     * @notice Place the proxy in front of `implementation` and optionally run one initializing call on it.
     * @dev The plane deployer deploys this like every other plane contract — no canonical deployer, no
     *      CREATE2. `initData` is executed by `delegatecall` against the implementation in this proxy's
     *      storage context, which is how an implementation that needs post-deploy wiring receives it; an
     *      implementation whose state is entirely its constructor immutables passes empty data.
     * @param registry_ The identity registry whose registrar quorum authorizes upgrades.
     * @param implementation_ The first implementation. Must carry code.
     * @param initData Optional initializing calldata, executed against `implementation`.
     */
    constructor(FinalIdentityRegistry registry_, address implementation_, bytes memory initData) {
        if (address(registry_) == address(0) || implementation_ == address(0)) revert InvalidProxyArg();
        if (implementation_.code.length == 0) revert ImplementationHasNoCode(implementation_);
        registry = registry_;
        StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = implementation_;
        emit Upgraded(address(0), implementation_);
        if (initData.length != 0) _delegateCall(implementation_, initData);
    }

    /**
     * @notice Replace the implementation under the sealed registrar quorum — or, while the registry's
     *         bootstrap window is open, by the bootstrap admin alone.
     * @dev The same window and the same quorum every state-plane `configure` uses. The payload folded into
     *      the digest is the new implementation alone; the domain, the chain, this proxy's address, the
     *      anchor block and the burned nonce are added by the registry (`FinalPqQuorum.digest`), so the
     *      approvals are bound to exactly one upgrade of exactly one proxy.
     *
     *      Order: authority first, then the code check, then the write. A refused quorum burns the nonce
     *      (the registry's rule — an approval set is spent whether or not it suffices) and changes nothing
     *      here. `initData` runs against the NEW implementation after the pointer moves, so an implementation
     *      that appends storage can seed it in the same transaction; it is the caller's contract that the
     *      data targets an initializer the new implementation guards.
     * @param newImplementation The implementation to install. Must carry code and differ from the current one.
     * @param initData Optional calldata executed against `newImplementation` after the switch.
     * @param anchorBlock The block the registrars read the roster at.
     * @param approvals The sealed registrar quorum. Empty while bootstrap is open and the caller is the admin.
     */
    function upgradeTo(
        address newImplementation,
        bytes calldata initData,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        if (registry.bootstrapSealed() || msg.sender != registry.bootstrapAdmin()) {
            registry.requireRegistrarQuorum(
                DOMAIN_PROXY_UPGRADE, keccak256(abi.encode(newImplementation, keccak256(initData))), anchorBlock, approvals
            );
        }
        if (newImplementation == address(0)) revert InvalidProxyArg();
        if (newImplementation.code.length == 0) revert ImplementationHasNoCode(newImplementation);
        address previous = StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value;
        if (newImplementation == previous) revert ImplementationUnchanged(newImplementation);
        StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = newImplementation;
        emit Upgraded(previous, newImplementation);
        if (initData.length != 0) _delegateCall(newImplementation, initData);
    }

    /// @notice The implementation currently serving this proxy.
    /// @dev Read by the deploy tool's drift check and the explorer's verifier; the same ERC-1967 slot every
    ///      generic tool reads.
    function implementation() external view returns (address) {
        return StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value;
    }

    /// @inheritdoc FinalProxyBase
    function _proxyImplementation() internal view override returns (address) {
        return StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value;
    }
}

contracts/finalchain/FinalChainTime.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
//
// Change Date: 2029-01-01
// Change License: GPL-2.0-or-later
//
// Additional Use Grant:
// 1. Any person or entity may link this time library into contracts deployed on
//    a Final DeFi Protocol chain, and may read its constants to interpret the
//    timestamps and durations that chain publishes.
// 2. Integrators, indexers, and operators may use it to convert between this
//    chain's clock and the units their own systems keep, as part of their
//    integration with the Final DeFi Protocol.
// 3. For the avoidance of doubt, this Grant does NOT permit the commercial
//    deployment of a Fork of this library or a competing state plane derived
//    from it without permission prior to the Change Date.
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;

/**
 * @title Final Chain Time
 * @notice **On this chain, `block.timestamp` is MILLISECONDS, not seconds.**
 * @dev Every other EVM chain stamps seconds. This one cannot. It mints a block every 100 ms, and the protocol
 * requires block timestamps to strictly increase, so a second-denominated clock would exhaust its distinct
 * values ten times over per second. Milliseconds is the deliberate consequence, and it is a property of the
 * CHAIN itself rather than of any contract here — nothing in this library can change it, and nothing deployed
 * beside this library may assume otherwise.
 *
 * Every duration and every instant on this chain is therefore in milliseconds. This library exists so that fact
 * is stated in one place and converted in one place, instead of being assumed independently everywhere a
 * deadline or a delay is written.
 *
 * ## The naming rule, which is a safety rule
 *
 * A field or constant carrying a duration or an instant on this chain ends in `Ms`. This is not decoration. A
 * delay field named for seconds while holding milliseconds elapses a thousand times too fast: a one-day
 * recovery delay would mature in about eighty-six seconds, and a two-year dormancy threshold in under a day.
 * Those delays are the whole of what stands between a stolen credential and an account, so a name that states
 * the wrong unit is not a cosmetic defect — it is the defect, wearing a disguise. `Seconds`-suffixed names do
 * not appear in this directory and must not be introduced.
 *
 * A test harness is not a check on this. Standard EVM tooling stamps `block.timestamp` in seconds, so a suite
 * can agree with the contracts under test and both be wrong about the chain they deploy to. The unit has to be
 * carried by the names.
 *
 * Solidity's `hours` and `days` suffixes remain the clearest way to write a duration, so durations are written
 * as `24 hours * MS_PER_SECOND` rather than as a bare literal: the intent stays readable and the unit stays
 * explicit at the point of use.
 */
library FinalChainTime {
    /// @notice Milliseconds per second — the whole conversion between this chain's clock and ordinary time,
    ///         named once.
    /// @dev Multiply a `seconds`-denominated Solidity duration literal by this to express it in this chain's
    ///      units. It is deliberately the only place the factor appears.
    uint64 internal constant MS_PER_SECOND = 1_000;

    /// @notice Nanoseconds per millisecond — the divisor for values that arrive stamped in nanoseconds.
    /// @dev The certificate schema stamps validity windows in nanoseconds, so a certificate converts DOWN to
    ///      this chain's clock. Dividing rather than multiplying is the direction that cannot overflow, and it
    ///      truncates toward the past, which for a validity window is the conservative rounding.
    uint64 internal constant NS_PER_MILLISECOND = 1_000_000;

    /// @notice This chain's current time, in milliseconds.
    /// @dev A function rather than a bare `block.timestamp` read so the unit is visible at every call site.
    ///      It performs no arithmetic and exists purely so that reading the clock is self-describing, where
    ///      `block.timestamp` on this chain is silently a thousand times what a reader would assume.
    /// @return nowInMs The current block's timestamp, in milliseconds.
    function nowMs() internal view returns (uint64) {
        return uint64(block.timestamp);
    }
}

contracts/finalchain/FinalIdentityRegistry.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
//
// Change Date: 2029-01-01
// Change License: GPL-2.0-or-later
//
// Additional Use Grant:
// 1. Any person or entity may deploy this identity registry as part of a Final
//    DeFi Protocol state plane, and may register, rotate, and revoke identity
//    records in it under the authority this contract enforces.
// 2. Operators, integrators, and end users may read the certificates, public
//    keys, role bits, and signer bindings it holds, and may call its views to
//    resolve an identity, a sender, or a quorum roster.
// 3. For the avoidance of doubt, this Grant does NOT permit the commercial
//    deployment of a Fork of this identity registry or a competing certificate
//    authority derived from it without permission prior to the Change Date.
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;

import {FinalCertificate} from "./FinalCertificate.sol";
import {FinalChainTime} from "./FinalChainTime.sol";
import {FinalChainPrecompiles} from "./FinalChainPrecompiles.sol";
import {FinalPqQuorum} from "./FinalPqQuorum.sol";
import {FinalSweep} from "../utils/FinalSweep.sol";
import {FinalChainInitializable} from "./FinalChainInitializable.sol";

/// @dev Commitment space for one stage's encapsulation pair.
///      Byte-equal to `FinalWalletFactory.DOMAIN_KEM_BUNDLE` and to the certificate issuer's own preimage
/// constant. Three independent derivations of one word: a mismatch in any of them is a certificate that
/// verifies nowhere, so the value is pinned by test against the other two rather than imported.
bytes32 constant DOMAIN_KEM_BUNDLE = keccak256("FINAL_KEM_BUNDLE_v01");

/// @dev Commitment space for the identity tree's wallet leaf.
///      Byte-equal to `IdentityRootModule.DOMAIN_IDENTITY_LEAF` on every execution chain. Restated rather
/// than imported because that module lives on other chains and no import would make the two one value; a
/// cross-contract parity test pins the pair. The spelling is FROZEN: the premined certificates were mined
/// against this exact constant, and the leaf it derives is the `certHash` inside a wallet's address
/// derivation, so changing a byte here moves addresses that already exist.
bytes32 constant DOMAIN_IDENTITY_LEAF = keccak256("FINAL_IDENTITY_LEAF_PQ_v01");

/// @dev Commitment space for the identity tree's ISSUER leaf.
///      An issuer projects under its own domain — `DOMAIN_ISSUER_LEAF ‖ certHash ‖ version ‖
/// issuerTreeRoot` — so an issuer record is stapleable for offline licence verification while the distinct
/// domain keeps it out of wallet admission: an execution chain's gateway folds with the wallet domain, so an
/// issuer leaf can never satisfy an identity-certificate check there. `issuerTreeRoot` is a RESERVED word,
/// zero until an issuer's own certificate-tree anchor is wired — the only clean path to offline licence
/// revocation, since fixed-depth insertion-ordered state trees cannot prove non-inclusion.
bytes32 constant DOMAIN_ISSUER_LEAF = keccak256("FINAL_ISSUER_LEAF_v01");

/// @dev The issuer name every chain-attested certificate carries, as a keccak digest.
///      The chain is the issuer but holds no keypair, so a chain-attested certificate carries this named
/// value in its issuer field: required by the wire format, verifying nothing on its own, and covered by
/// `certHash`. The name is deliberately environment-agnostic and jurisdiction-silent — the issuer is the
/// worldwide network rather than a legal entity, and an environment-specific name would fork `certHash` per
/// environment. Compared as a hash rather than as a string, so the check costs one word.
bytes32 constant CHAIN_ISSUER_DN_HASH = keccak256("CN=Final Chain,O=Final DeFi");

/// @dev The authority key identifier every chain-attested certificate names.
///      `SHA3-256(utf8("FINAL_CHAIN_AUTHORITY_v01"))` — a DOMAIN constant rather than the digest of a key,
/// because the chain issues certificates and holds no public key block to hash. Precomputed rather than
/// derived at construction: the harness the unit tests run under does not implement the real SHA3 function,
/// and the literal is pinned by test against a reference implementation. A zero-length authority key
/// identifier is reserved and is admitted nowhere.
bytes32 constant CHAIN_AUTHORITY_KEY_ID =
    0x9a6a5d8139ad2d28957698330aaa691017dba7dc80eb7cbec585239fb680bbab;

/**
 * @title Identity Leaf Sink
 * @notice The identity tree's projection door on the state-trees contract.
 * @dev A narrow interface rather than an import, because the trees contract imports THIS file — the
 *      dependency runs that way, and this is the one call that runs the other. Declaring the single method
 *      here keeps the cycle away from the compiler without duplicating either contract's surface.
 */
interface IIdentityLeafSink {
    /// @notice Recompute and store the identity-tree leaf for each named account.
    /// @dev Called inside the same transaction as every identity mutation, so an execution chain's admission
    ///      set sees a registration, rotation or revocation the moment this chain does. The leaf VALUE is
    ///      derived by the trees contract from the registry's post-mutation state, so the caller supplies
    ///      accounts and never a leaf.
    /// @param accounts The accounts whose leaves are stale.
    function syncIdentityLeaves(address[] calldata accounts) external;
}

/**
 * @title Revocation Recorder
 * @notice The revocation log's recording door.
 * @dev Same narrow-interface reasoning as the leaf sink above. `recorded` is read first, so a fingerprint
 *      somebody already recorded through the log's permissionless door cannot revert the registry mutation
 *      that feeds it.
 */
interface IRevocationRecorder {
    /// @notice Fold a permanently retired signer fingerprint into the revocation log.
    /// @dev The log applies its own permanence gate, reading this registry back; the call states nothing the
    ///      registry has not already decided.
    /// @param signerId The fingerprint that has lost standing for good.
    function record(bytes32 signerId) external;
    /// @notice Whether the log already holds `signerId`.
    /// @param signerId The fingerprint to look up.
    /// @return Whether a leaf for it exists.
    function recorded(bytes32 signerId) external view returns (bool);
}

/**
 * @title Final Identity Registry
 * @notice Who every party in the system is, on chain: one record per party, carrying its certificate and its
 *         actual public keys.
 * @dev Every service, every co-signer, every certificate authority and every operator has one record here.
 *      The record holds the party's public keys in full rather than commitments to them, and this contract is
 *      the certificate authority as well as the roster.
 *
 *      ## Where this runs
 *
 *      Only on this project's own reth-based chains. Verification happens inside precompiles that exist
 *      nowhere else: SHA3-256 at `0x0202`, ML-DSA-87 at `0x0204` and SLH-DSA-SHAKE-256s at `0x0205`, each
 *      address being that primitive's FIPS number. The constructor probes them and refuses to deploy where
 *      they are absent, so a registry of keys the chain cannot check never comes into existence. This
 *      contract takes part in no CREATE2 derivation — its address is per chain, and nothing derives an
 *      address from it — and nothing outside this directory imports it.
 *
 *      Gas is deliberately NOT a design constraint on that chain and must not be optimised for. Where a
 *      choice below trades gas for a verdict that is re-derivable from public state, the verdict wins: a
 *      signature checked in a precompile is a fact anyone can recompute, where the same check run in a
 *      library by whichever process happened to hold the keys is only a claim.
 *
 *      ## Keys are read from STORAGE, never from calldata
 *
 *      A commitment would be a quarter of the storage and would be enough to CHECK a key someone hands you.
 *      It is not enough to VERIFY A SIGNATURE, because verification needs the key itself — and a key that
 *      arrives in calldata proves nothing, since anyone holding a keypair can produce a valid signature under
 *      it. A quorum built on caller-supplied keys is a quorum of one: whoever built the calldata.
 *
 *      So the keys live here in full. `FinalPqQuorum` resolves a member through this registry and reads that
 *      member's key from this registry's storage, and "which key is co-signer three" has exactly one answer,
 *      in exactly one place. That is the load-bearing rule of every quorum on the chain, not an optimisation.
 *
 *      ## The certificate is the record, not a pointer to one
 *
 *      `certHash` is `SHA3-256(TBSCertificate)`: the certificate's own identity, and the handle revocation is
 *      keyed on. {registerWallet} and {registerIssuer} take the certificate's TBS bytes and read everything
 *      out of them — the digest, the serial, the key identifiers, the depth pair, the validity window and
 *      every public key. Neither takes a key argument, so no two arguments can disagree and no registrar can
 *      bind a certificate to a keypair that certificate does not contain.
 *
 *      ## The root is the first record here, not a self-signed file
 *
 *      This chain is the only root certificate authority, and the root is pinned as an entry in this registry
 *      rather than distributed as a self-signed certificate somebody has to install. Chain validation
 *      terminates here BY IDENTITY. Everything registered after the root is verified on chain, inside the
 *      precompiles, against what this registry already holds: the holder's own two signatures over the
 *      admission digest, the pinned chain-issuer constants, and — for a nested issuer — lineage to a
 *      registered parent whose depth admits it. There is no path by which a key enters this registry
 *      unattested; a registrar cannot register anything else.
 *
 *      ## Roles are a bitmask
 *
 *      One party is legitimately several things: a co-signer that also publishes, an operator that is also a
 *      guardian. A single enum would force either duplicate records for one key, which is two sources of
 *      truth about one party, or a role hierarchy nobody agrees on. A mask has neither problem, and a quorum
 *      asks whether an account CARRIES a capability rather than whether it IS a type.
 *
 *      ## Membership is hybrid-gated
 *
 *      Who is in this registry, and with which roles, is the root of every quorum on the chain, so it is the
 *      one thing no single key may decide. Once bootstrap is sealed, every membership mutation — register,
 *      roles, revoke, a hash-based signing key, the registrar threshold itself — and every state-plane
 *      configuration change routed through {requireRegistrarQuorum} takes a `ROLE_REGISTRAR` quorum whose
 *      approvals carry BOTH families: the ML-DSA-87 vote and the SLH-DSA seal. A lattice break cannot then
 *      rewrite the roster, and neither can a hash-function break; only both at once.
 *
 *      The bootstrap window is the only exception. While it is open the bootstrap admin writes alone, because
 *      every roster has to be installed by someone before it can install itself. {sealBootstrap} closes it
 *      irreversibly, and refuses to close it onto a registrar quorum that cannot be met.
 *
 *      ## The sender is not the account
 *
 *      Transactions on this chain are signed by ML-DSA-87, and the node derives `msg.sender` from the key as
 *      `keccak256(0x04 ‖ publicKey)[12:]`. That address pays gas and holds no authority. {accountOfSender}
 *      binds it to the identity whose live transaction key it derives from, so a `msg.sender` gate anywhere
 *      on this chain asks {senderHasRole} and resolves to the identity — and a key rotation moves the binding
 *      instead of the roster.
 *
 *      ## What this contract deliberately does not do
 *
 *      It never un-revokes: a revoked certificate is finished, and reversing that would reopen every past
 *      verification. It never enumerates a mapping inside a mutation — the registrars supply the chain list a
 *      revocation touches, and a fingerprint an incomplete list missed stays permanently recordable through
 *      the revocation log's own permissionless door. It holds no funds, exposes no payable entrypoint, and
 *      reserves nothing against a sweep. And it grants no capability by parsing one: a certificate says which
 *      keys a party holds, `roles` says what the party may do, and the two arrive as different arguments on
 *      purpose.
 */
contract FinalIdentityRegistry is FinalSweep, FinalChainInitializable {
    // ---------------------------------------------------------------- roles

    /// @notice May co-sign account-state rounds (tree 1).
    uint256 public constant ROLE_ACCOUNT_COSIGNER = 1 << 0;
    /// @notice May co-sign MMR / bundle-log advances.
    uint256 public constant ROLE_MMR_COSIGNER = 1 << 1;
    /// @notice May publish PHI ledger state (tree 2).
    uint256 public constant ROLE_PHI_PUBLISHER = 1 << 2;
    /// @notice May publish vAsset state (tree 3).
    uint256 public constant ROLE_VASSET_PUBLISHER = 1 << 3;
    /// @notice May publish oracle data (tree 4).
    uint256 public constant ROLE_ORACLE_PUBLISHER = 1 << 4;
    /// @notice May publish settlement / asset registry roots (trees 5 and 6).
    uint256 public constant ROLE_REGISTRY_PUBLISHER = 1 << 5;
    /// @notice May act as a wallet guardian.
    uint256 public constant ROLE_GUARDIAN = 1 << 6;
    /// @notice May submit transactions on behalf of the protocol.
    uint256 public constant ROLE_RELAYER = 1 << 7;
    /// @notice May register and revoke identities once bootstrap is sealed.
    uint256 public constant ROLE_REGISTRAR = 1 << 8;
    /// @notice A certificate authority — the root, or an intermediate under it.
    uint256 public constant ROLE_CERTIFICATE_AUTHORITY = 1 << 9;
    /// @notice May co-sign `FinalSettlementLog` appends — the cross-chain
    /// settlement quorum, the same members whose LMS keys satisfy the
    /// execution chains' settlement set. A role of its own rather than a
    /// second use of `ROLE_REGISTRY_PUBLISHER`: the registries (trees 5/6)
    /// change on listing cadence and settlement leaves release custody, and
    /// one role for both would put the value plane behind the listing roster.
    uint256 public constant ROLE_SETTLEMENT_COSIGNER = 1 << 10;

    // ----------------------------------------------------- action domains

    /// @notice Action domain for registering or rotating a wallet identity.
    /// @dev One domain per membership mutation, so an approval to grant a role can never be replayed as one
    ///      to revoke. This registry is its own verifying contract for all of these, and the digest also
    ///      binds a per-contract counter, so an approval authorises exactly one action once.
    bytes32 public constant DOMAIN_REGISTER_WALLET = keccak256("FINAL_REGISTRY_REGISTER_WALLET_v01");
    /// @notice Action domain for registering or rotating an issuer.
    bytes32 public constant DOMAIN_REGISTER_ISSUER = keccak256("FINAL_REGISTRY_REGISTER_ISSUER_v01");
    /// @notice The admission proof-of-possession digest domain.
    /// @dev The HOLDER signs `keccak256(abi.encode(domain, chainid, registry, certHash, recoveryCertHash,
    ///      gateNonce))` with the live transaction key (ML-DSA-87) AND the live access key
    ///      (SLH-DSA-SHAKE-256s) — both families, in the admission transaction, verified by the precompiles.
    ///      Possession lives in the TRANSACTION, never in the artifact, so holding a copy of somebody's
    ///      public certificate admits nothing.
    bytes32 public constant DOMAIN_IDENTITY_ADMISSION = keccak256("FINAL_IDENTITY_ADMISSION_v01");
    /// @notice Action domain for root-plane global certificate revocation, by handle.
    bytes32 public constant DOMAIN_REVOKE_CERTIFICATE =
        keccak256("FINAL_REGISTRY_REVOKE_CERTIFICATE_v01");
    /// @notice Digest domain for an issuer revoking a certificate it signed off chain.
    /// @dev Signed by the issuer's own registered cert-signing keys rather than approved by a quorum, and
    ///      bound to the issuer's own gate nonce, so one issuer's revocations cannot be replayed as
    ///      another's.
    bytes32 public constant DOMAIN_ISSUER_CERT_REVOCATION =
        keccak256("FINAL_ISSUER_CERT_REVOCATION_v01");
    /// @notice Action domain for recording an account's hash-based signing key.
    bytes32 public constant DOMAIN_REGISTER_LMS_KEY = keccak256("FINAL_REGISTRY_REGISTER_LMS_KEY_v01");
    /// @notice Action domain for replacing an identity's capability bitmask.
    bytes32 public constant DOMAIN_SET_ROLES = keccak256("FINAL_REGISTRY_SET_ROLES_v01");
    /// @notice Action domain for retiring an identity.
    bytes32 public constant DOMAIN_REVOKE = keccak256("FINAL_REGISTRY_REVOKE_v01");
    /// @notice Action domain for moving the registrar threshold itself.
    bytes32 public constant DOMAIN_SET_REGISTRAR_THRESHOLD =
        keccak256("FINAL_REGISTRY_SET_REGISTRAR_THRESHOLD_v01");

    /// @notice The algorithm identifier the sender derivation is domain-separated by.
    /// @dev ML-DSA-87, FIPS 204 — the only algorithm this chain's transaction envelope admits. Prefixing it
    ///      means a key of another family can never derive the same sender address.
    uint8 private constant ENVELOPE_ALG_ML_DSA_87 = 4;

    // ------------------------------------------------------------- storage

    /**
     * @title Identity
     * @notice One party's on-chain identity.
     * @dev `version` increments on every mutation, and that increment is what a rotation IS: the record is
     *      replaced rather than appended to, and the version is how a reader on another chain knows which of
     *      two copies it has seen is newer.
     */
    struct Identity {
        /// SHA3-256 of the LIVE certificate's TBS bytes. The revocation handle.
        bytes32 certHash;
        /// SHA3-256 of the RECOVERY certificate's TBS bytes.
        bytes32 recoveryCertHash;
        /// The certificate's 32-byte serial, `16 B entropy ‖ 16 B counter`.
        bytes32 serial;
        /// SHA3-256 of this certificate's public key block. A child names it in
        /// its own `AuthorityKeyId`, which is how the chain links the two.
        bytes32 subjectKeyId;
        /// Capability bitmask. Zero for a registered-but-idle party.
        uint256 roles;
        /// Position on the delegation axis; 0 is the Final Chain root.
        uint8 depth;
        /// Deepest level this key may issue to. `== depth` means it signs no
        /// certificates at all, which is every end entity.
        uint8 maxDelegationDepth;
        /// Milliseconds since the epoch, on this chain's clock. The certificate schema stamps validity in
        /// nanoseconds and the parser converts on the way in, so nothing here ever compares across units.
        uint64 notBefore;
        /// Milliseconds since the epoch, or 0 for "never expires" — which the certificate schema allows and
        /// personal identity certificates use. The bound is exclusive.
        uint64 notAfter;
        /// Monotonic. A rotation that does not advance it is refused.
        uint64 version;
        /// Set by `revoke`. Never unset: a revoked certificate is finished, and
        /// an un-revoke would make every past verification re-openable.
        bool revoked;
        /// Distinguishes "no record" from "a record whose fields are all zero".
        bool registered;
    }

    /**
     * @title Lms Key
     * @notice A hash-based (LMS) signing key held by a registered account.
     * @dev The execution chains' quorums verify LMS rather than ML-DSA, because those chains have no
     *      post-quantum precompiles and check a keccak hash chain instead. Those keys are the authority over
     *      the post-quantum anchor, and therefore over post-quantum execution — which makes "who holds this
     *      fingerprint?" a question the state plane has to be able to answer, exactly as it answers it for
     *      every other key.
     *
     *      Recorded against an account that is ALREADY registered, so an LMS key is a capability of a known
     *      identity rather than a standalone credential. It inherits that identity's revocation: a revoked
     *      account's signer is a revoked signer, with nothing extra to remember to do.
     */
    struct LmsKey {
        /// `I`, hashed into every step of the signature.
        bytes16 keyId;
        /// Merkle tree height. Bound into the fingerprint, because the leaf
        /// commits to node `2^h + q` and a signer who could vary it could vary
        /// the numbering.
        uint8 height;
        /// `T[1]`, the LMS public key.
        bytes32 root;
        /// Monotonic. A rotation that does not advance it is refused, so a
        /// replayed registration cannot reinstate a superseded key.
        uint64 version;
        /// Distinguishes "no key" from "a key whose fields are all zero".
        bool registered;
    }

    /// @notice The hash-based (LMS) signing key an account holds, per chain.
    /// @dev One slot per account AND chain. A single-use hash-based counter is a complete defence only while
    ///      the key it names signs for ONE chain, so the roster is stored the way it is armed: the same
    ///      operator is a different signer on every chain, and a rotation on one says nothing about another.
    mapping(address account => mapping(uint64 chainId => LmsKey)) private _lmsKey;
    /**
     * @title Lms Binding
     * @notice What a signer fingerprint is bound to: the account holding it and the chain it signs for.
     * @dev Two fields in one slot, deliberately. This contract sits within a few bytes of the deployed-code
     *      ceiling, so anything added to this surface has to pay for itself in bytecode first — which is why
     *      checks that no authority consults, such as refusing a zero chain identifier, are left to the
     *      publisher off chain rather than spent here.
     */
    struct LmsBinding {
        /// The account that registered the fingerprint. Zero means no account ever did.
        address account;
        /// The chain that registration was for. Zero alongside a zero account, for a fingerprint never
        /// registered.
        uint64 chainId;
    }

    /// @notice Which account a signer fingerprint belongs to, and which chain it signs for.
    /// @dev The lookup the whole LMS record exists for: an execution chain's roster names fingerprints and
    ///      nothing else, so without this the keys behind those names are unattributable. Written once at
    ///      registration and left in place when the key is superseded, because attribution is history — a
    ///      signature made under a retired key was still made by that operator.
    ///
    ///      The chain it names is what selects the slot {lmsSignerIsLive} resolves the fingerprint against.
    mapping(bytes32 signerId => LmsBinding) private _lmsBinding;

    /// @notice The identity record for an account.
    mapping(address account => Identity) private _identity;
    /// @notice The live transaction key, ML-DSA-87: spending, and every high-cadence protocol action.
    /// @dev All four key slots are stored in FULL rather than as commitments, because the precompiles verify
    ///      against a KEY and a key that arrived in calldata proves nothing about who signed. This is the
    ///      rule every quorum on this chain rests on.
    /// @dev A certificate authority has two keys rather than four, and they live in the two active slots.
    ///      One storage shape rather than two, because every reader would otherwise have to know which kind
    ///      of party it was looking at before it could look.
    mapping(address account => bytes) private _activeTransactionKey;
    /// @notice The live access key, SLH-DSA-SHAKE-256s: identity, rotation and guardianship.
    mapping(address account => bytes) private _activeAccessKey;
    /// @notice The pre-committed recovery transaction key, ML-DSA-87. Empty for a certificate authority.
    mapping(address account => bytes) private _recoveryTransactionKey;
    /// @notice The pre-committed recovery access key, SLH-DSA-SHAKE-256s. Empty for a certificate
    ///         authority.
    mapping(address account => bytes) private _recoveryAccessKey;
    /// @notice The seal key: a service's second SLH-DSA-SHAKE-256s key, which co-signs membership-class
    ///         quorum decisions (the registrar quorum); operational quorum actions take the ML-DSA-87 vote alone.
    /// @dev Empty for every identity whose certificate carries no seal slot, which is every user wallet and
    ///      every certificate authority. An identity with no seal can never contribute to a sealed quorum,
    ///      so {sealableMemberCount} counts this rather than counting role bits.
    mapping(address account => bytes) private _activeSealKey;
    /// @notice The live stage's ML-KEM-1024 encapsulation key, the lattice half of the pair.
    /// @dev Two algorithms per stage — ML-KEM-1024 and HQC-5 — so a break in either family leaves the other
    ///      standing, the same reasoning that pairs the two signature families. The pair is written and
    ///      cleared together, so an account holds both or neither.
    /// @dev Stored as the RAW keys, like the signing keys, because a registry that held only commitments
    ///      could not answer "encapsulate to this party" without a second lookup somewhere less
    ///      authoritative.
    mapping(address account => bytes) private _activeKemMlKem;
    /// @notice The live stage's HQC-5 encapsulation key, the code-based half of the pair.
    mapping(address account => bytes) private _activeKemHqc;
    /// @notice The recovery stage's ML-KEM-1024 encapsulation key. Empty when the account has no recovery
    ///         stage.
    mapping(address account => bytes) private _recoveryKemMlKem;
    /// @notice The recovery stage's HQC-5 encapsulation key. Empty when the account has no recovery stage.
    mapping(address account => bytes) private _recoveryKemHqc;
    /// @notice Reverse index. A certificate identifies exactly one account, so
    /// presenting a `certHash` is enough to find who it belongs to.
    mapping(bytes32 certHash => address account) public accountOfCertificate;
    /// @notice Revocation by certificate, independent of the account record.
    /// A certificate stays revoked even if its account is later re-registered
    /// under a new one.
    mapping(bytes32 certHash => bool) public certificateRevoked;
    /// @notice Who revoked a certificate through the ISSUER half of the lane.
    /// Scoped by the verifier: the entry binds only when the recorded revoker
    /// is the certificate's own issuer. Never gates registration.
    mapping(bytes32 certHash => address) public certificateRevokedBy;

    /// @notice Every registered account, in registration order. Small by
    /// construction — this is services and co-signers, not wallets.
    address[] private _accounts;

    /// @notice Bootstrap authority. Zero once `sealBootstrap` has run.
    address public bootstrapAdmin;
    /// @notice Whether registration still accepts the bootstrap admin.
    bool public bootstrapSealed;

    /// @notice Where identity mutations project the tree-8 leaf, same-tx.
    /// Zero only before {wireStatePlane} — the deploy tooling wires it before
    /// the first registration, and the projection is skipped while unset so
    /// the wiring transaction itself can be ordered freely in the bootstrap
    /// window.
    address public stateTrees;
    /// @notice Where the PERMANENT standing losses — revocation and LMS-key
    /// supersession — are recorded, same-tx. Zero only before {wireStatePlane}.
    address public revocationLog;

    /// @notice Sealed `ROLE_REGISTRAR` approvals a membership mutation needs.
    /// @dev Zero until set, and bootstrap cannot be sealed while it is zero or
    /// unreachable: a registry sealed behind a threshold nobody can meet is a
    /// registry nobody can ever write to again.
    uint256 public registrarThreshold;
    /// @notice Replay counter per verifying contract — this registry for its
    /// own mutations, each state-plane contract for its configuration. Bound
    /// into every registrar digest, so an approval is for exactly one action.
    mapping(address caller => uint64) private _gateNonce;
    /// @notice The identity a Final Chain sender belongs to. See the contract
    /// notes: a sender is derived from the `activeTransaction` key and is not
    /// the account.
    mapping(address sender => address account) public accountOfSender;

    // -------------------------------------------------------------- events

    /// @notice An identity was registered, or an existing one rotated onto a new certificate set.
    /// @param account The identity written.
    /// @param certHash The live certificate's handle.
    /// @param roles The capability bitmask now in force.
    /// @param version The record's monotonic version.
    event IdentityRegistered(
        address indexed account, bytes32 indexed certHash, uint256 roles, uint64 version
    );
    /// @notice An identity's capability bitmask was replaced.
    /// @param account The identity whose roles changed.
    /// @param previousRoles The mask before the change.
    /// @param newRoles The mask now in force.
    event IdentityRolesChanged(address indexed account, uint256 previousRoles, uint256 newRoles);
    /// @notice An account's hash-based signing key for one chain was recorded or rotated.
    /// @param account The identity that holds the key.
    /// @param signerId The fingerprint an execution chain's roster names.
    /// @param chainId The chain the key is armed for.
    /// @param keyId The LMS key identifier.
    /// @param height The Merkle tree height.
    /// @param root The LMS public key.
    /// @param version The lineage counter for this account and chain.
    event LmsKeyRegistered(
        address indexed account,
        bytes32 indexed signerId,
        uint64 indexed chainId,
        bytes16 keyId,
        uint8 height,
        bytes32 root,
        uint64 version
    );
    /// @notice An identity was retired. Irreversible, and its roles are cleared in the same transaction.
    /// @param account The identity that was revoked.
    /// @param certHash The certificate it held at the time.
    event IdentityRevoked(address indexed account, bytes32 indexed certHash);
    /// @notice One revocation-lane entry.
    /// @param certHash The certificate that was revoked.
    /// @param revoker Zero for a root-plane revocation, the issuing identity for an issuer's own.
    event CertificateRevoked(bytes32 indexed certHash, address indexed revoker);
    /// @notice The bootstrap window closed. After this there is no single-caller write path left.
    /// @param sealedBy The bootstrap admin that closed it, immediately before being cleared.
    event BootstrapSealed(address indexed sealedBy);
    /// @notice The one-shot state-plane wiring landed. Emitted at most once in this contract's lifetime.
    /// @param stateTrees The state-trees contract that owns the identity tree.
    /// @param revocationLog The append-only log of retired signer fingerprints.
    event StatePlaneWired(address stateTrees, address revocationLog);
    /// @notice The number of sealed registrar approvals a membership mutation needs was set.
    /// @param threshold The new threshold.
    event RegistrarThresholdSet(uint256 threshold);
    /// @notice A registrar quorum authorized an action.
    /// @param verifyingContract The contract the approvals were collected for, and whose counter was burned.
    /// @param actionDomain The action domain the approvals bound.
    /// @param nonce The counter value the approvals were made over; the next action needs the next one.
    /// @param valid How many approvals verified.
    event RegistrarQuorumApproved(
        address indexed verifyingContract, bytes32 indexed actionDomain, uint64 nonce, uint256 valid
    );

    // -------------------------------------------------------------- errors

    /// @notice The caller holds none of the authority the entry point requires.
    /// @param caller The address that called.
    error NotAuthorized(address caller);
    /// @notice The bootstrap window is already closed. Closing it is irreversible.
    error BootstrapAlreadySealed();
    /// @notice No record claims this account, or a zero address was offered as one.
    /// @param account The address that was named.
    error UnknownAccount(address account);
    /// @notice A certificate's encapsulation key failed the chain's own well-formedness check.
    /// @dev Names the algorithm, because the pair is stored together and "one of these two" is not an
    ///      actionable answer.
    /// @param account The account being registered.
    /// @param algorithmId The algorithm whose key was malformed.
    error MalformedEncapsulationKey(address account, uint16 algorithmId);
    /// @notice The certificate is already bound to a different account. One certificate identifies exactly
    ///         one party.
    /// @param certHash The certificate's handle.
    /// @param boundTo The account that already holds it.
    error CertificateAlreadyBound(bytes32 certHash, address boundTo);
    /// @notice The certificate has been revoked, or the account's own certificate has. Revocation is never
    ///         undone, so this is terminal for that handle.
    /// @param certHash The revoked certificate's handle.
    error CertificateIsRevoked(bytes32 certHash);
    /// @notice A registration or rotation did not advance the record's version. Monotonicity is what stops a
    ///         replayed transaction reinstating credentials their holder has moved off.
    /// @param current The version on record.
    /// @param offered The version the caller presented.
    error VersionNotNewer(uint64 current, uint64 offered);
    /// @notice The named account does not carry `ROLE_CERTIFICATE_AUTHORITY`, or does not currently stand.
    /// @param issuer The account that was named.
    error IssuerNotACertificateAuthority(address issuer);
    /// @notice The named parent has reached its own delegation bound and may issue nothing further.
    /// @param issuer The parent account.
    /// @param depth The parent's depth.
    /// @param maxDelegationDepth The deepest level the parent may issue to.
    error IssuerMayNotSign(address issuer, uint8 depth, uint8 maxDelegationDepth);
    /// @notice A certificate sits at a depth its lineage does not put it at. Levels cannot be skipped,
    ///         because skipping one is how an issuer escapes its own delegation bound.
    /// @param got The depth the certificate declares.
    /// @param want The depth its lineage requires.
    error WrongDepth(uint8 got, uint8 want);
    /// @notice A child certificate claims a deeper delegation bound than the parent that admits it.
    /// @param child The child's `maxDelegationDepth`.
    /// @param issuer The parent's `maxDelegationDepth`.
    error DelegationWidened(uint8 child, uint8 issuer);
    /// @notice The certificate names an authority key that is not its declared parent's subject key.
    /// @param got The authority key identifier the certificate carries.
    /// @param want The parent's subject key identifier.
    error AuthorityKeyIdMismatch(bytes32 got, bytes32 want);
    /// @notice The live and recovery certificates carry different serials, so they describe two different
    ///         certificate sets rather than two stages of one.
    /// @param liveSerial The live certificate's serial.
    /// @param recoverySerial The recovery certificate's serial.
    error StagesDisagree(bytes32 liveSerial, bytes32 recoverySerial);
    /// @notice An LMS tree height outside 1 through 24, the range the verifier admits.
    /// @param height The height offered.
    error LmsHeightOutOfRange(uint8 height);
    /// @notice A zero LMS root commits to no tree and is refused.
    error LmsRootIsZero();
    /// @notice This signer fingerprint already belongs to a different account.
    /// @param signerId The fingerprint offered.
    /// @param boundTo The account that already holds it.
    error LmsKeyAlreadyBound(bytes32 signerId, address boundTo);
    /// @notice Two identities cannot share a transaction key: the sender it derives would be attributable to
    ///         both.
    /// @param sender The derived sender address.
    /// @param boundTo The account that already claims it.
    error SenderAlreadyBound(address sender, address boundTo);
    /// @notice Fewer registrars able to seal than the threshold asks for.
    /// @param sealable How many standing registrars hold a seal key.
    /// @param threshold How many approvals a membership mutation needs.
    error RegistrarThresholdUnreachable(uint256 sealable, uint256 threshold);
    /// @notice A zero registrar threshold was offered, or a quorum was demanded before one was set. A zero
    ///         threshold is a registry with no authority behind its membership.
    error RegistrarThresholdIsZero();
    /// @notice {wireStatePlane} has already run. Both pointers are trust topology and are written once.
    error StatePlaneAlreadyWired();
    /// @notice {wireStatePlane} was handed a zero address for the trees or for the revocation log.
    error ZeroStatePlane();
    /// @notice The holder's proof of possession did not verify: one family failed, or the digest was built
    ///         over the wrong nonce.
    /// @param account The account the admission was for.
    error AdmissionProofInvalid(address account);
    /// @notice The certificate does not name the chain's authority key, so it is not chain-attested.
    /// @param authorityKeyId The authority key identifier that was presented.
    error NotChainAttested(bytes32 authorityKeyId);
    /// @notice The certificate's issuer name is not the chain's own.
    /// @param issuerDnHash The digest of the name that was presented.
    error WrongIssuerDn(bytes32 issuerDnHash);
    /// @notice A chain-attested end entity sits at depth 1 with `maxDelegationDepth == depth`; anything else
    ///         is not an end entity.
    /// @param depth The certificate's position on the delegation axis.
    /// @param maxDelegationDepth The deepest level it may issue to.
    error NotAnEndEntity(uint8 depth, uint8 maxDelegationDepth);
    /// @notice An issuer that cannot sign is an end entity wearing an issuer profile, and belongs in
    ///         {registerWallet}.
    /// @param depth The certificate's position on the delegation axis.
    /// @param maxDelegationDepth The deepest level it may issue to.
    error IssuerCannotSign(uint8 depth, uint8 maxDelegationDepth);
    /// @notice A registered issuer's certificate never expires.
    /// @dev Expiry is the passive half of an issuer's lifecycle, so a zero `NotAfter` is refused here even
    ///      though the certificate schema allows one for an end entity.
    error IssuerMustExpire();
    /// @notice An issuer validity window past {MAX_ISSUER_VALIDITY_MS}.
    /// @param notBefore The certificate's start, in this chain's milliseconds.
    /// @param notAfter The certificate's end, in this chain's milliseconds.
    error IssuerValidityTooLong(uint64 notBefore, uint64 notAfter);
    /// @notice An institution registration whose subject name carries no ISO 3166 country component, or
    ///         whose institution extension is too short to hold one.
    /// @dev Only the trust root is jurisdiction-silent; a registered institution names where it answers for
    ///      itself.
    error JurisdictionMissing();
    /// @notice The subject name's country and the institution extension's `jurisdiction` field disagree, or
    ///         the extension's jurisdiction is not a two-byte country code.
    error JurisdictionMismatch();

    // --------------------------------------------------------- constructor

    /**
     * @notice Deploy the registry with a bootstrap registrar in place.
     * @dev The precompile probe is the point of the constructor. This contract is meaningless on a chain
     *      that cannot verify post-quantum signatures, and deploying it there would produce a registry full
     *      of keys nothing on that chain can check — so it refuses to exist where the precompiles are
     *      absent rather than existing and being trusted.
     *
     *      The admin is the whole authority until {sealBootstrap} runs, because every roster has to be
     *      installed by someone before it can install itself.
     * @param admin The bootstrap registrar. Genesis names the chain deployer.
     */
    constructor(address admin) {
        FinalChainPrecompiles.assertAvailable();
        _setUp(admin);
    }

    /**
     * @notice The constructor's storage write, for a registry behind `FinalChainProxy` — whose upgrade
     *         authority is this registry itself: the proxy is built with its own address as `registry`.
     *         Runs once, in the proxy's constructor; `AlreadyInitialized` afterwards and on a direct deploy.
     * @param admin The bootstrap registrar.
     */
    function initialize(address admin) external {
        _setUp(admin);
    }

    /// @dev The bootstrap admin is storage (cleared by {sealBootstrap}), so a proxy needs it replayed.
    function _setUp(address admin) internal initializer {
        bootstrapAdmin = admin;
    }

    // ----------------------------------------------------------- authority

    /**
     * @notice The authority gate on every membership mutation this registry performs.
     * @dev Bootstrap is a real window, not a formality: every roster in this system has to be installed by
     *      someone before it can install itself, and a design that pretends otherwise ends up with a roster
     *      that cannot be brought into existence at all. It is closed by {sealBootstrap}, irreversibly.
     *
     *      While the window is open the admin writes alone. Once it is closed there is no single-caller path
     *      left — not for a registrar, not for anyone — and every mutation goes through the sealed registrar
     *      quorum, whose approvals carry both signature families.
     * @param actionDomain One of the `DOMAIN_*` constants naming the mutation.
     * @param payloadDigest The mutation's own arguments, folded.
     * @param anchorBlock The block the registrars read the roster at. Ignored while bootstrap is open.
     * @param approvals The sealed registrar quorum. Empty while bootstrap is open.
     */
    function _requireMembershipAuthority(
        bytes32 actionDomain,
        bytes32 payloadDigest,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) private {
        if (!bootstrapSealed && msg.sender == bootstrapAdmin) return;
        _requireRegistrarQuorum(address(this), actionDomain, payloadDigest, anchorBlock, approvals);
    }

    /**
     * @notice The sealed registrar quorum, for the other contracts in the state plane.
     * @dev `msg.sender` — the calling contract — is the verifying contract the digest binds and the counter
     *      it burns, so an approval collected for one contract's configuration cannot be spent on another's.
     *      The caller decides its own bootstrap exemption before calling; this function knows no caller's
     *      admin and applies none.
     *
     *      Anyone may SUBMIT such a transaction. Authority is the approvals, not the sender, which is the
     *      whole point of a quorum.
     * @param actionDomain The caller's own action domain for the change being authorised.
     * @param payloadDigest The change's arguments, folded by the caller.
     * @param anchorBlock The block the registrars read the roster at.
     * @param approvals The registrar approvals, each carrying both families.
     */
    function requireRegistrarQuorum(
        bytes32 actionDomain,
        bytes32 payloadDigest,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireRegistrarQuorum(msg.sender, actionDomain, payloadDigest, anchorBlock, approvals);
    }

    /// @notice Burn one gate nonce and require a sealed registrar quorum over the action.
    /// @dev The digest is `FinalPqQuorum.digest(verifyingContract, actionDomain, anchorBlock,
    ///      keccak256(abi.encode(nonce, payloadDigest)))`. The counter is burned BEFORE verification, so an
    ///      approval set is spent whether or not it turns out to be sufficient.
    ///
    ///      The seal is required rather than optional: membership is the hybrid class, and an approval
    ///      carrying only the lattice vote is not an approval here.
    /// @param verifyingContract The contract the approvals are for, and whose counter is burned.
    /// @param actionDomain One of the `DOMAIN_*` constants, so an approval to grant cannot be replayed to
    ///        revoke.
    /// @param payloadDigest The action's own arguments, folded.
    /// @param anchorBlock The block the registrars read the roster at.
    /// @param approvals The registrar approvals, each carrying both families.
    function _requireRegistrarQuorum(
        address verifyingContract,
        bytes32 actionDomain,
        bytes32 payloadDigest,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) private {
        if (registrarThreshold == 0) revert RegistrarThresholdIsZero();
        uint64 nonce = _gateNonce[verifyingContract];
        _gateNonce[verifyingContract] = nonce + 1;
        bytes32 quorumDigest = FinalPqQuorum.digest(
            verifyingContract, actionDomain, anchorBlock, keccak256(abi.encode(nonce, payloadDigest))
        );
        uint256 valid = FinalPqQuorum.require_(
            this,
            approvals,
            quorumDigest,
            ROLE_REGISTRAR,
            registrarThreshold,
            FinalPqQuorum.ALG_ML_DSA_87,
            anchorBlock,
            true
        );
        emit RegistrarQuorumApproved(verifyingContract, actionDomain, nonce, valid);
    }

    /**
     * @notice Set how many sealed registrar approvals a membership mutation needs.
     * @dev The bootstrap admin while the window is open; the current registrar quorum afterwards, so a
     *      registrar set that grows or shrinks can move the threshold to match itself.
     *
     *      Refuses a threshold the sealable registrars cannot meet, and refuses zero. Both are a registry
     *      that can never be written to again, and the way that presents is every membership mutation
     *      reverting forever with nothing naming the threshold as the cause.
     * @param threshold How many sealed approvals a mutation needs. Must be reachable and non-zero.
     * @param anchorBlock The block the registrars read the roster at. Ignored while bootstrap is open.
     * @param approvals The sealed registrar quorum. Empty while bootstrap is open.
     */
    function setRegistrarThreshold(
        uint256 threshold,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireMembershipAuthority(
            DOMAIN_SET_REGISTRAR_THRESHOLD, keccak256(abi.encode(threshold)), anchorBlock, approvals
        );
        if (threshold == 0) revert RegistrarThresholdIsZero();
        uint256 sealable = sealableMemberCount(ROLE_REGISTRAR);
        if (sealable < threshold) revert RegistrarThresholdUnreachable(sealable, threshold);
        registrarThreshold = threshold;
        emit RegistrarThresholdSet(threshold);
    }

    /// @notice The replay counter the next registrar approval for `caller` must be made over.
    /// @dev One counter per verifying contract, so an approval collected for one contract's configuration
    ///      cannot be spent on another's. A caller reads this to build the digest its registrars will sign.
    /// @param caller The verifying contract the approvals will name — this registry for its own mutations.
    /// @return The value the next approval must bind.
    function gateNonceOf(address caller) external view returns (uint64) {
        return _gateNonce[caller];
    }

    // -------------------------------------------------------- LMS signers

    /**
     * @notice The roster identity of an LMS public key.
     * @dev Byte-identical to `FinalRootAuthority.signerId` on the execution chains. Restated rather than
     *      imported because the two live on different chains and no import would make them one value —
     *      which is precisely why a test pins them together. A drift here would make every lookup miss while
     *      looking perfectly well-formed.
     *
     *      The height is bound into the fingerprint as well as the root, because a leaf commits to a node
     *      number derived from it, so a signer free to vary the height could vary the numbering.
     * @param keyId The LMS key identifier.
     * @param height The Merkle tree height.
     * @param root The LMS public key.
     * @return The fingerprint an execution chain's roster names.
     */
    function lmsSignerId(bytes16 keyId, uint8 height, bytes32 root) public pure returns (bytes32) {
        return keccak256(abi.encode(keyId, height, root));
    }

    /**
     * @notice Record the hash-based (LMS) signing key an already-registered account holds for one chain.
     * @dev Membership-gated, like every other write here.
     *
     *      Deliberately NOT a certificate: an LMS key is a capability of an existing identity, not an
     *      identity of its own. Binding it to an account means it inherits that account's revocation, so
     *      retiring a compromised operator is one action rather than one action per key they hold.
     *
     *      A rotation records the SUPERSEDED fingerprint into the revocation log in the same transaction, so
     *      the execution chains' suspension lane never depends on someone noticing. The superseded
     *      fingerprint is left BOUND to this account rather than cleared, because attribution is history.
     *
     *      A zero `chainId` is a tooling mistake rather than an attack — the slot it occupies is
     *      self-consistent and no authority consults it — so the publisher refuses it off chain and this
     *      contract spends no bytecode on the check.
     * @param account Must already be registered and not revoked.
     * @param chainId The execution chain this key is armed for.
     * @param keyId The LMS key identifier, hashed into every step of a signature under it.
     * @param height The Merkle tree height, 1 through 24.
     * @param root The LMS public key. Zero commits to no tree and is refused.
     * @param version Strictly increasing per account and chain. A rotation that does not advance it is
     *        refused, so a replayed registration cannot reinstate a key the operator has moved off.
     * @param anchorBlock The block the registrars read the roster at. Ignored while bootstrap is open.
     * @param approvals The sealed registrar quorum. Empty while bootstrap is open.
     */
    function registerLmsKey(
        address account,
        uint64 chainId,
        bytes16 keyId,
        uint8 height,
        bytes32 root,
        uint64 version,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireMembershipAuthority(
            DOMAIN_REGISTER_LMS_KEY,
            keccak256(abi.encode(account, chainId, keyId, height, root, version)),
            anchorBlock,
            approvals
        );
        Identity storage id = _identity[account];
        if (!id.registered) revert UnknownAccount(account);
        if (id.revoked) revert CertificateIsRevoked(id.certHash);
        // A zero chain id is a tooling mistake, not an attack: the slot it
        // would occupy is self-consistent and no authority consults it. The
        // publisher refuses it; EIP-170 pressure keeps the check off-chain.
        if (height == 0 || height > 24) revert LmsHeightOutOfRange(height);
        if (root == bytes32(0)) revert LmsRootIsZero();

        // Version lineage is PER account and chain: the same operator is a different signer on every chain,
        // so one chain starting at version 1 says nothing about another already being at version 3.
        LmsKey storage existing = _lmsKey[account][chainId];
        // An empty slot holds version 0, so this alone also refuses a version-0
        // registration — versions start at 1.
        if (version <= existing.version) {
            revert VersionNotNewer(existing.version, version);
        }

        bytes32 signerId = lmsSignerId(keyId, height, root);
        address boundTo = _lmsBinding[signerId].account;
        if (boundTo != address(0) && boundTo != account) {
            revert LmsKeyAlreadyBound(signerId, boundTo);
        }

        // The fingerprint being superseded, captured before the slot moves —
        // `existing` is a storage pointer and reads the NEW key afterwards.
        bytes32 superseded = existing.registered
            ? lmsSignerId(existing.keyId, existing.height, existing.root)
            : bytes32(0);

        // The superseded fingerprint is left bound to this account rather than
        // cleared. It is history: a signature made under the old key was made
        // by this operator, and a lookup that stopped resolving would make that
        // unprovable after the fact.
        _lmsKey[account][chainId] = LmsKey(keyId, height, root, version, true);
        _lmsBinding[signerId] = LmsBinding(account, chainId);
        emit LmsKeyRegistered(account, signerId, chainId, keyId, height, root, version);

        // Supersession is a PERMANENT transition — the old fingerprint stops
        // being this slot's current key and nothing re-registers it (a
        // re-registration of the same material is the same fingerprint, which
        // the guard below leaves alone). Recorded same-tx so the execution
        // chains' suspension lane never depends on someone noticing.
        if (superseded != bytes32(0) && superseded != signerId) {
            _recordRevokedSigner(superseded);
        }
        _projectIdentity(account);
    }

    /// @notice The LMS key an account holds for one chain, if any.
    /// @dev Keyed per account AND per chain, because a single-use hash-based counter is only complete while
    ///      the key it names signs for one chain. `registered` is the field to branch on; the zero struct
    ///      means no key rather than a key of zeroes.
    /// @param account The identity to read.
    /// @param chainId The chain the key is armed for.
    /// @return The stored key, copied to memory.
    function lmsKeyOf(address account, uint64 chainId) external view returns (LmsKey memory) {
        return _lmsKey[account][chainId];
    }

    /// @notice What a fingerprint is bound to: the account that registered it and the chain it signs for.
    /// @dev The binding survives supersession, because attribution is history: a signature made under a
    ///      retired key was still made by that operator, and a lookup that stopped resolving would make that
    ///      unprovable after the fact. Standing is a separate question, answered by {lmsSignerIsLive}.
    ///
    ///      The revocation log's permanence gate reads this to find the slot a fingerprint belongs to; that
    ///      slot's current key is what separates a superseded fingerprint, which is permanent and
    ///      recordable, from a merely lapsed one, which renewal undoes.
    /// @param signerId The fingerprint to resolve.
    /// @return account The account that registered it, or zero for a fingerprint never registered.
    /// @return chainId The chain that registration was for, or zero alongside a zero account.
    function lmsBindingOf(bytes32 signerId) external view returns (address account, uint64 chainId) {
        LmsBinding storage binding = _lmsBinding[signerId];
        return (binding.account, binding.chainId);
    }

    /**
     * @notice Whether a signer fingerprint is held by a standing, unrevoked account.
     * @dev The question a verifier actually has. An execution chain's authority roster names fingerprints
     *      and learns nothing else about them, so without this the keys behind those names are
     *      unanswerable from the state plane.
     *
     *      Standing is asked through {isActive} rather than by spelling the conditions out again, because a
     *      second spelling is how two answers drift: an expired identity already holds no role, and a signer
     *      lookup that disagreed would leave a roster satisfiable by an operator the rest of the registry
     *      has stopped honouring.
     *
     *      Live means the CURRENT key of the fingerprint's own account-and-chain slot, not merely one this
     *      account ever held. A superseded fingerprint stays attributable but stops being live, and a
     *      rotation on one chain says nothing about the same operator's key on another.
     * @param signerId The fingerprint an authority roster names.
     * @return live Whether the fingerprint is that slot's current key and the account still stands.
     * @return account The account the fingerprint is bound to, or zero when none ever registered it.
     */
    function lmsSignerIsLive(bytes32 signerId) external view returns (bool live, address account) {
        LmsBinding storage binding = _lmsBinding[signerId];
        account = binding.account;
        if (account == address(0)) return (false, address(0));
        // `isActive`, not a registered/revoked pair spelled out here. The
        // certificate validity window is part of standing: an expired identity
        // already holds no role, and a signer lookup that disagreed would leave
        // a roster satisfiable by an operator the rest of the registry has
        // stopped honouring. Spelling the condition out a second time is how
        // the two drift apart.
        if (!isActive(account)) return (false, account);
        // The CURRENT key of the fingerprint's own (account, chain) slot, not
        // merely one this account ever held: a superseded fingerprint stays
        // attributable but stops being live, and a rotation on one chain says
        // nothing about the same operator's key on another.
        LmsKey storage k = _lmsKey[account][binding.chainId];
        live = k.registered && lmsSignerId(k.keyId, k.height, k.root) == signerId;
    }

    /// @notice Close the bootstrap window. Irreversible.
    /// @dev Refuses while the registrar quorum is unset or unreachable, because sealing then would leave a
    ///      registry nobody can ever write to again — including to fix the threshold that locked it. The
    ///      count is of registrars that can SEAL: a certificate authority carrying the registrar role is
    ///      registered from a certificate with no seal slot and can never contribute an approval, so
    ///      counting role bits alone would seal onto a quorum that looks reachable and is not.
    ///
    ///      Clears the admin as well as setting the flag, so no single-caller path survives the seal.
    function sealBootstrap() external {
        if (msg.sender != bootstrapAdmin) revert NotAuthorized(msg.sender);
        if (bootstrapSealed) revert BootstrapAlreadySealed();
        if (registrarThreshold == 0) revert RegistrarThresholdIsZero();
        uint256 sealable = sealableMemberCount(ROLE_REGISTRAR);
        if (sealable < registrarThreshold) {
            revert RegistrarThresholdUnreachable(sealable, registrarThreshold);
        }
        bootstrapSealed = true;
        bootstrapAdmin = address(0);
        emit BootstrapSealed(msg.sender);
    }

    // ------------------------------------------------- state-plane wiring

    /**
     * @notice Wire the state trees and the revocation log, once, inside the bootstrap window.
     * @dev One-shot because both pointers are TRUST TOPOLOGY: the trees pointer decides where the
     *      wallet-creation admission set is written, and the log pointer decides where permanent standing
     *      losses are recorded. A re-wireable pointer would be a key over both.
     *
     *      It cannot be a constructor argument, because both of those contracts take THIS registry as one of
     *      theirs. The deploy tooling calls it in the same nonce-fixed block that deploys them, before any
     *      identity is registered, which is why the projection is silently skipped while the pointers are
     *      zero rather than reverting.
     * @param stateTrees_ The state-trees contract that owns tree 8. Zero is refused.
     * @param revocationLog_ The append-only log of retired signer fingerprints. Zero is refused.
     */
    function wireStatePlane(address stateTrees_, address revocationLog_) external {
        if (bootstrapSealed || msg.sender != bootstrapAdmin) revert NotAuthorized(msg.sender);
        if (stateTrees != address(0) || revocationLog != address(0)) revert StatePlaneAlreadyWired();
        if (stateTrees_ == address(0) || revocationLog_ == address(0)) revert ZeroStatePlane();
        stateTrees = stateTrees_;
        revocationLog = revocationLog_;
        emit StatePlaneWired(stateTrees_, revocationLog_);
    }

    /// @notice Refresh `account`'s tree-8 leaf in the state trees, same transaction.
    /// @dev Skipped while the plane is unwired, which is a bootstrap-window state the deploy tooling closes
    ///      before the first registration, and never otherwise. The leaf VALUE is derived by the trees
    ///      contract from this registry's post-mutation state, so there is nothing here to get wrong beyond
    ///      forgetting to call it — which is why every mutation calls it, including the one that cannot
    ///      change the leaf.
    /// @param account The identity whose leaf is stale.
    function _projectIdentity(address account) private {
        address trees = stateTrees;
        if (trees == address(0)) return;
        address[] memory one = new address[](1);
        one[0] = account;
        IIdentityLeafSink(trees).syncIdentityLeaves(one);
    }

    /// @notice Record a permanently retired signer fingerprint into the revocation log, same transaction.
    /// @dev Skipped while the log is unwired, and skipped when somebody already recorded the fingerprint
    ///      through the log's permissionless door — the log refuses a duplicate, and a membership mutation
    ///      must not be revertible by a stranger who front-ran its bookkeeping.
    /// @param signerId The fingerprint that has lost standing for good.
    function _recordRevokedSigner(bytes32 signerId) private {
        address log = revocationLog;
        if (log == address(0)) return;
        if (IRevocationRecorder(log).recorded(signerId)) return;
        IRevocationRecorder(log).record(signerId);
    }

    // -------------------------------------------------------- registration

    /**
     * @title Admission Proof
     * @notice The holder's proof of possession at admission: both live-stage families over the admission
     *         digest.
     * @dev There is no root keypair and no issuer signature on this path. The chain admits, and the two
     *      signatures presented at creation are the HOLDER's, verified by the precompiles inside the same
     *      transaction that writes the record. Possession lives in the TRANSACTION, never in the artifact:
     *      a public certificate is a document anyone may hold, so presenting one proves nothing.
     */
    struct AdmissionProof {
        /// The holder's ML-DSA-87 signature under the live TRANSACTION key, over the admission digest.
        bytes mlDsaSignature;
        /// The holder's SLH-DSA-SHAKE-256s signature under the live ACCESS key, over the same digest. Two
        /// families over one message, so neither a lattice break nor a hash-function break alone admits an
        /// identity.
        bytes slhDsaSignature;
    }

    /**
     * @notice Register or rotate a Final Wallet identity from its two public certificates.
     * @dev **Both stages, together.** A wallet has four keys in two stages and the recovery pair is
     *      PRE-COMMITTED — written at wallet initialization from the same certificate set that determined
     *      the wallet's address, which is why enabling post-quantum mode later takes no key arguments. The
     *      two certificates must share a serial: a serial is per certificate SET, so two stages that
     *      disagree about it are two different wallets.
     *
     *      **Chain-attested means pinned, per stage:** the chain's issuer name and authority key, depth
     *      exactly 1 so the certificate hangs directly under the chain, and `maxDelegationDepth == depth` so
     *      the holder issues nothing. That immutable pair is what {identityTreeLeafOf} discriminates record
     *      kinds by.
     *
     *      Issuance authority is the registrar quorum and possession is the holder's own proof; there is no
     *      root keypair anywhere and no certificate-authority signature over this admission.
     * @param account The wallet address the certificate set derives.
     * @param liveTbs The live certificate's TBS bytes: the live transaction and access keys.
     * @param recoveryTbs The recovery certificate's TBS bytes: the pre-committed recovery pair.
     * @param proof The holder's two signatures over the admission digest — the live transaction key
     *        (ML-DSA-87) and the live access key (SLH-DSA-SHAKE-256s), both verified in the precompiles
     *        inside this transaction.
     * @param roles Capability bitmask. The one thing the certificates do not say, because capability is this
     *        system's decision rather than the certificate's.
     * @param version Monotonic. A rotation that does not advance it is refused.
     * @param anchorBlock The block the registrars read the roster at. Ignored while bootstrap is open.
     * @param approvals The sealed registrar quorum. Empty while bootstrap is open. The digest binds the
     *        account, both certificates' bytes, the roles and the version.
     * @return certHash The handle the live certificate is now known by.
     */
    function registerWallet(
        address account,
        bytes calldata liveTbs,
        bytes calldata recoveryTbs,
        AdmissionProof calldata proof,
        uint256 roles,
        uint64 version,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external returns (bytes32 certHash) {
        // Read BEFORE the authority check: the quorum path burns this counter
        // inside `_requireRegistrarQuorum`, and the proof must bind the value
        // the round was built over. The bootstrap path burns it explicitly in
        // `_requireAdmissionProof`, so an admission is one-shot in both regimes.
        uint64 admissionNonce = _gateNonce[address(this)];
        _requireMembershipAuthority(
            DOMAIN_REGISTER_WALLET,
            keccak256(
                abi.encode(account, keccak256(liveTbs), keccak256(recoveryTbs), roles, version)
            ),
            anchorBlock,
            approvals
        );

        FinalCertificate.Parsed memory l = FinalCertificate.parseLive(liveTbs);
        FinalCertificate.Parsed memory r = FinalCertificate.parseRecovery(recoveryTbs);
        if (l.serial != r.serial) revert StagesDisagree(l.serial, r.serial);

        _requireChainAttestedEndEntity(l);
        _requireChainAttestedEndEntity(r);
        _requireAdmissionProof(account, l, r.certHash, proof, admissionNonce);

        certHash = l.certHash;
        _write(account, l, r, roles, version, false);
    }

    /**
     * @notice Register or rotate an ISSUER: a third party, or one of this system's own intermediates, that
     *         signs certificates off chain with the keys registered here.
     * @dev Admission is chain-native like any identity — the registrar quorum authorises, and the holder's
     *      own proof of possession establishes that the party controls the keys it is claiming. The
     *      delegation rules survive as LINEAGE: a nested issuer's depth, delegation bound and
     *      `AuthorityKeyId` must chain to its registered parent. No parent signs anything; this chain's
     *      admission IS the issuance.
     *
     *      A registered issuer always expires, and its window is bounded by {MAX_ISSUER_VALIDITY_MS}.
     *
     *      An institution must carry its real ISO 3166 country in its subject name, matching the
     *      `jurisdiction` field of its institution extension. That is enforced at the door because a
     *      verifier's legal recourse starts with knowing where an issuer answers for itself.
     *
     *      `ROLE_CERTIFICATE_AUTHORITY` is added to whatever `roles` asks for, rather than being required in
     *      it: the capability is what this entry point means, so it cannot be forgotten in an argument.
     * @param account The issuer's account on this chain.
     * @param tbs The issuer certificate's TBS bytes: two cert-signing keys, ML-DSA-87 and
     *        SLH-DSA-SHAKE-256s, and no recovery stage — renewing an issuer is re-issuing, a governance act
     *        rather than a key rotation.
     * @param parent The registered parent issuer for a nested intermediate; zero for an issuer hanging
     *        directly under the chain.
     * @param proof The issuer's own two cert-signing keys over the admission digest. The recovery-handle
     *        slot in that digest is zero, because there is no recovery stage to bind.
     * @param roles Capability bitmask, over and above the certificate-authority bit this call adds.
     * @param version Monotonic. A rotation that does not advance it is refused.
     * @param anchorBlock The block the registrars read the roster at. Ignored while bootstrap is open.
     * @param approvals The sealed registrar quorum. Empty while bootstrap is open. The digest binds the
     *        account, the certificate bytes, the parent, the roles and the version.
     * @return certHash The handle the registered certificate is now known by.
     */
    function registerIssuer(
        address account,
        bytes calldata tbs,
        address parent,
        AdmissionProof calldata proof,
        uint256 roles,
        uint64 version,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external returns (bytes32 certHash) {
        uint64 admissionNonce = _gateNonce[address(this)];
        _requireMembershipAuthority(
            DOMAIN_REGISTER_ISSUER,
            keccak256(abi.encode(account, keccak256(tbs), parent, roles, version)),
            anchorBlock,
            approvals
        );

        FinalCertificate.Parsed memory c = FinalCertificate.parseCa(tbs);
        // An issuer that cannot sign is an end entity wearing a profile —
        // and an end entity belongs in `registerWallet`.
        if (c.depth == 0 || c.maxDelegationDepth <= c.depth) {
            revert IssuerCannotSign(c.depth, c.maxDelegationDepth);
        }
        if (c.notAfter == 0) revert IssuerMustExpire();
        if (c.notAfter - c.notBefore > MAX_ISSUER_VALIDITY_MS) {
            revert IssuerValidityTooLong(c.notBefore, c.notAfter);
        }
        if (c.issuerDnHash != CHAIN_ISSUER_DN_HASH) revert WrongIssuerDn(c.issuerDnHash);
        _requireLineage(parent, c);
        _requireJurisdiction(c);
        _requireAdmissionProof(account, c, bytes32(0), proof, admissionNonce);

        certHash = c.certHash;
        _write(account, c, c, roles | ROLE_CERTIFICATE_AUTHORITY, version, true);
    }

    /// @notice The validity ceiling a registered issuer's certificate may not exceed, in this chain's
    ///         milliseconds: two 366-day years.
    /// @dev Expiry is the passive half of an issuer's lifecycle — the touchpoint that proves an issuer is
    ///      still there without anyone having to act — so a registered issuer always carries a real
    ///      `NotAfter` and a bounded window. Renewal re-issues under the same registered keys with a version
    ///      bump rather than extending a certificate in place.
    uint64 public constant MAX_ISSUER_VALIDITY_MS = 2 * 366 days * 1000;

    /// @notice Pin one stage of a chain-attested end-entity certificate.
    /// @dev Three checks, run once per stage: the certificate names the chain's authority key, it carries the
    ///      chain's issuer name, and its depth pair is exactly that of an end entity — depth 1, directly
    ///      under the chain, issuing nothing. The depth pair is immutable per version, which is why
    ///      {identityTreeLeafOf} discriminates record kinds by it rather than by a role bit.
    /// @param c The parsed certificate stage.
    function _requireChainAttestedEndEntity(FinalCertificate.Parsed memory c) private pure {
        if (c.authorityKeyId != CHAIN_AUTHORITY_KEY_ID) revert NotChainAttested(c.authorityKeyId);
        if (c.issuerDnHash != CHAIN_ISSUER_DN_HASH) revert WrongIssuerDn(c.issuerDnHash);
        if (c.depth != 1 || c.maxDelegationDepth != c.depth) {
            revert NotAnEndEntity(c.depth, c.maxDelegationDepth);
        }
    }

    /// @notice Check a nested issuer's lineage to its registered parent.
    /// @dev Delegation is governed by DEPTH, not by a boolean: a parent may sign only while
    ///      `depth < maxDelegationDepth`, a child sits exactly one level down so it cannot skip levels to
    ///      escape that bound, and its own bound may never widen past its parent's. The child's
    ///      `AuthorityKeyId` must equal the parent's `SubjectKeyId`, which is the link the chain follows.
    ///
    ///      A zero `parent` means the issuer hangs directly under the chain: it must then name the chain's
    ///      own authority key and sit at depth 1. No parent SIGNS anything here — admission by this chain is
    ///      the issuance, and lineage is what keeps the delegation bounds honest across it.
    /// @param parent The registered parent issuer, or zero for one directly under the chain.
    /// @param c The parsed issuer certificate.
    function _requireLineage(address parent, FinalCertificate.Parsed memory c) private view {
        if (parent == address(0)) {
            if (c.authorityKeyId != CHAIN_AUTHORITY_KEY_ID) {
                revert NotChainAttested(c.authorityKeyId);
            }
            if (c.depth != 1) revert WrongDepth(c.depth, 1);
            return;
        }
        Identity storage ca = _identity[parent];
        if (!hasRole(parent, ROLE_CERTIFICATE_AUTHORITY)) {
            revert IssuerNotACertificateAuthority(parent);
        }
        // Delegation is governed by depth, not by a boolean. `Depth <
        // MaxDelegationDepth` permits signing, and a child sits exactly one
        // level down — an issuer cannot skip levels to escape its own bound.
        if (ca.depth >= ca.maxDelegationDepth) {
            revert IssuerMayNotSign(parent, ca.depth, ca.maxDelegationDepth);
        }
        if (c.depth != ca.depth + 1) revert WrongDepth(c.depth, ca.depth + 1);
        if (c.maxDelegationDepth > ca.maxDelegationDepth) {
            revert DelegationWidened(c.maxDelegationDepth, ca.maxDelegationDepth);
        }
        if (c.authorityKeyId != ca.subjectKeyId) {
            revert AuthorityKeyIdMismatch(c.authorityKeyId, ca.subjectKeyId);
        }
    }

    /// @notice Refuse an issuer whose subject name carries no jurisdiction, or one that disagrees with its
    ///         institution extension.
    /// @dev An issuer that answers for itself somewhere is an issuer a verifier has recourse against, so a
    ///      registered institution must name its jurisdiction and must name it once. Only the trust root is
    ///      jurisdiction-silent, because the root is the worldwide network rather than a legal entity.
    ///
    ///      The rule is a real ISO 3166 alpha-2 `C=` component in the subject name, equal to the
    ///      `jurisdiction` field of the certificate's institution extension. The name is in canonical
    ///      comma-separated form, so `C=` matches at the start or immediately after a comma, and the
    ///      component value is exactly two bytes — a longer one is a different component that happens to
    ///      start with the same letter.
    /// @param c The parsed issuer certificate.
    function _requireJurisdiction(FinalCertificate.Parsed memory c) private pure {
        bytes memory dn = c.subjectDn;
        bytes2 country;
        bool found = false;
        for (uint256 i = 0; i + 4 <= dn.length; i++) {
            if ((i == 0 || dn[i - 1] == ",") && dn[i] == "C" && dn[i + 1] == "=") {
                // Exactly two bytes, then end-of-DN or the next component.
                if (i + 4 < dn.length && dn[i + 4] != ",") revert JurisdictionMissing();
                country = bytes2(bytes.concat(dn[i + 2], dn[i + 3]));
                found = true;
                break;
            }
        }
        if (!found) revert JurisdictionMissing();

        // Institution extension: legalNameLength ‖ legalName ‖
        // registrationNoLength ‖ registrationNo ‖ jurisdictionLength ‖
        // jurisdiction. The jurisdiction must EQUAL the DN's country.
        bytes memory ext = c.institutionExt;
        if (ext.length < 6) revert JurisdictionMissing();
        uint256 q = 2 + (uint256(uint8(ext[0])) << 8 | uint256(uint8(ext[1])));
        if (ext.length < q + 2) revert JurisdictionMissing();
        q += 2 + (uint256(uint8(ext[q])) << 8 | uint256(uint8(ext[q + 1])));
        if (ext.length < q + 2) revert JurisdictionMissing();
        uint256 jLen = uint256(uint8(ext[q])) << 8 | uint256(uint8(ext[q + 1]));
        q += 2;
        if (jLen != 2 || ext.length < q + 2) revert JurisdictionMismatch();
        if (bytes2(bytes.concat(ext[q], ext[q + 1])) != country) revert JurisdictionMismatch();
    }

    /// @notice Verify the holder's proof of possession over the admission digest.
    /// @dev Both live-stage families, in the precompiles, inside this transaction: an ML-DSA-87 signature
    ///      under the certificate's transaction key and an SLH-DSA-SHAKE-256s signature under its access
    ///      key. Possession lives in the TRANSACTION rather than in the artifact, so holding a copy of
    ///      somebody's public certificate proves nothing.
    ///
    ///      The keys come out of the certificate being admitted, not out of calldata, which is what makes
    ///      this a proof rather than a self-signed assertion.
    ///
    ///      Burns the gate nonce on the bootstrap path — the quorum path burned it already — so an admission
    ///      is one-shot in both regimes and a captured proof cannot be replayed into a second registration.
    /// @param account The account being admitted; named in the revert so a failure is attributable.
    /// @param live The parsed live-stage certificate whose keys verify the proof.
    /// @param recoveryCertHash The recovery certificate's handle, bound into the digest; zero for an issuer.
    /// @param proof The holder's two signatures.
    /// @param admissionNonce The gate-nonce value the digest was built over.
    function _requireAdmissionProof(
        address account,
        FinalCertificate.Parsed memory live,
        bytes32 recoveryCertHash,
        AdmissionProof calldata proof,
        uint64 admissionNonce
    ) private {
        bytes memory message = abi.encodePacked(
            keccak256(
                abi.encode(
                    DOMAIN_IDENTITY_ADMISSION,
                    block.chainid,
                    address(this),
                    live.certHash,
                    recoveryCertHash,
                    admissionNonce
                )
            )
        );
        if (
            !FinalChainPrecompiles.verifyMlDsa87(live.transactionKey, message, proof.mlDsaSignature)
                || !FinalChainPrecompiles.verifySlhDsa(live.accessKey, message, proof.slhDsaSignature)
        ) revert AdmissionProofInvalid(account);
        if (_gateNonce[address(this)] == admissionNonce) {
            _gateNonce[address(this)] = admissionNonce + 1;
        }
    }

    /**
     * @notice Commit one parsed certificate set to storage and project the result.
     * @dev The single write path behind both registration entry points, so a wallet record and an issuer
     *      record cannot diverge in how they are stored. Every authorization, parse and pin has already run;
     *      what is left is the ordering that keeps the record consistent with its indexes.
     *
     *      A rotation RELEASES the previous certificate's binding rather than revoking it: a superseded
     *      certificate and a compromised one are different facts, and revocation is the louder of the two.
     *      The sender binding moves with the transaction key for the same reason — a rotation is the account
     *      disowning that key, and a gate that still resolved the old sender would honour a retired key.
     *
     *      A certificate already bound to another account is refused, and so is a version that does not
     *      advance, so neither a replayed registration nor a stolen certificate can take a record over.
     * @param account The identity being written. Zero is refused.
     * @param live The parsed live-stage certificate; for an issuer, its single certificate.
     * @param recovery The parsed recovery-stage certificate; for an issuer, the same value, discarded.
     * @param roles The complete capability bitmask to store.
     * @param version Monotonic per account. Must exceed the stored value.
     * @param isCa Whether this is a certificate authority, which stores no recovery, seal or
     *        encapsulation material.
     */
    function _write(
        address account,
        FinalCertificate.Parsed memory live,
        FinalCertificate.Parsed memory recovery,
        uint256 roles,
        uint64 version,
        bool isCa
    ) private {
        if (account == address(0)) revert UnknownAccount(account);
        if (certificateRevoked[live.certHash]) revert CertificateIsRevoked(live.certHash);

        address boundTo = accountOfCertificate[live.certHash];
        if (boundTo != address(0) && boundTo != account) {
            revert CertificateAlreadyBound(live.certHash, boundTo);
        }

        Identity storage id = _identity[account];
        if (!id.registered) {
            _accounts.push(account);
            id.registered = true;
        } else {
            if (version <= id.version) revert VersionNotNewer(id.version, version);
            if (id.revoked) revert CertificateIsRevoked(id.certHash);
            // A rotation releases the previous certificate's binding. It is NOT
            // revoked — a superseded certificate and a compromised one are
            // different facts and revocation is the louder of the two.
            if (id.certHash != live.certHash) delete accountOfCertificate[id.certHash];
        }

        id.certHash = live.certHash;
        id.recoveryCertHash = recovery.certHash;
        id.serial = live.serial;
        id.subjectKeyId = live.subjectKeyId;
        id.roles = roles;
        id.depth = live.depth;
        id.maxDelegationDepth = live.maxDelegationDepth;
        id.notBefore = live.notBefore;
        id.notAfter = live.notAfter;
        id.version = version;

        // The sender binding moves with the transaction key. The old sender is
        // released rather than kept: a rotation is the account disowning that
        // key, and a gate that still resolved it would honour a retired key.
        address sender = senderFor(live.transactionKey);
        address senderBoundTo = accountOfSender[sender];
        if (senderBoundTo != address(0) && senderBoundTo != account) {
            revert SenderAlreadyBound(sender, senderBoundTo);
        }
        if (_activeTransactionKey[account].length != 0) {
            address previousSender = senderFor(_activeTransactionKey[account]);
            if (previousSender != sender) delete accountOfSender[previousSender];
        }
        accountOfSender[sender] = account;

        _activeTransactionKey[account] = live.transactionKey;
        _activeAccessKey[account] = live.accessKey;
        // A CA has no recovery pair; the two active slots are all it has.
        _recoveryTransactionKey[account] = isCa ? bytes("") : recovery.transactionKey;
        _recoveryAccessKey[account] = isCa ? bytes("") : recovery.accessKey;
        // Cleared on a rotation to a certificate without one, for the same
        // reason the encapsulation pair is: a stale seal surviving a rotation
        // would let a retired key keep co-signing execution.
        _activeSealKey[account] = isCa ? bytes("") : live.sealKey;

        // The encapsulation pair, validated before it is stored.
        //
        // **The registry is where a sender looks up "encapsulate to this
        // party", so a malformed key here is not a bad record — it is an
        // account nobody can seal an intent to.** The discovery would happen at
        // the first attempt, and on the hybrid path it would happen as a pair
        // silently reduced to one family, which is identical on the wire. The
        // precompiles make it a refusal at registration instead.
        //
        // Neither is a re-implementation of the KEM: `0x0203` runs FIPS 203
        // §7.2's own encapsulation-key check and `0x0207` runs the structural
        // check HQC-5's encoding admits. Encapsulation is a sender operation
        // and decapsulation needs the secret key, so nothing more belongs here.
        //
        // A CA is sealed to by nobody and carries no encapsulation stage, so
        // its slots are cleared rather than checked.
        _storeKemPair(account, isCa, live.kemMlKem, live.kemHqc, true);
        _storeKemPair(account, isCa, recovery.kemMlKem, recovery.kemHqc, false);

        accountOfCertificate[live.certHash] = account;

        emit IdentityRegistered(account, live.certHash, roles, version);
        // Same-tx: a registration or rotation is visible to every execution
        // chain's admission set the moment it is visible here.
        _projectIdentity(account);
    }

    /**
     * @notice Store one stage's encapsulation pair, or clear it.
     * @dev Empty is legitimate and is not the same as absent-and-wrong: a certificate authority has no
     *      encapsulation stage, and a certificate may be issued without one. The parser has already refused
     *      the half-populated case, so by here the pair is both or neither.
     *
     *      Cleared rather than left alone on a rotation to an empty pair. A stale key surviving a rotation is
     *      a sender encapsulating to a credential the account has disowned, and the message then never
     *      decrypts — the failure mode with no error attached, and the one this pairing exists to avoid.
     * @param account The identity being written.
     * @param isCa Whether the record is a certificate authority, which carries no encapsulation stage.
     * @param mlKem The stage's ML-KEM-1024 key, or empty.
     * @param hqc The stage's HQC-5 key, or empty.
     * @param isLive Whether this is the live stage; false selects the recovery slots.
     */
    function _storeKemPair(address account, bool isCa, bytes memory mlKem, bytes memory hqc, bool isLive)
        private
    {
        if (isCa || mlKem.length == 0) {
            delete (isLive ? _activeKemMlKem : _recoveryKemMlKem)[account];
            delete (isLive ? _activeKemHqc : _recoveryKemHqc)[account];
            return;
        }
        if (!FinalChainPrecompiles.isWellFormedMlKem1024(mlKem)) {
            revert MalformedEncapsulationKey(account, FinalCertificate.ALG_ML_KEM_1024);
        }
        if (!FinalChainPrecompiles.isWellFormedHqc5(hqc)) {
            revert MalformedEncapsulationKey(account, FinalCertificate.ALG_HQC_5);
        }
        if (isLive) {
            _activeKemMlKem[account] = mlKem;
            _activeKemHqc[account] = hqc;
        } else {
            _recoveryKemMlKem[account] = mlKem;
            _recoveryKemHqc[account] = hqc;
        }
    }

    /// @notice Grant or withdraw capabilities without rotating keys.
    /// @dev Separate from registration because the two have different cadences: a role changes when a
    ///      service's job changes, a key changes when it is compromised or aged out. Folding them together
    ///      would force a key rotation to express a role change, which is the more dangerous of the two
    ///      operations doing the work of the safer one.
    /// @param account Must already be registered and not revoked.
    /// @param roles The complete new capability bitmask; it replaces the old one rather than merging.
    /// @param anchorBlock The block the registrars read the roster at.
    /// @param approvals The sealed registrar quorum. Empty while bootstrap is open.
    function setRoles(
        address account,
        uint256 roles,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireMembershipAuthority(
            DOMAIN_SET_ROLES, keccak256(abi.encode(account, roles)), anchorBlock, approvals
        );
        Identity storage id = _identity[account];
        if (!id.registered) revert UnknownAccount(account);
        if (id.revoked) revert CertificateIsRevoked(id.certHash);
        uint256 previous = id.roles;
        id.roles = roles;
        _requireRegistrarQuorumReachable();
        emit IdentityRolesChanged(account, previous, roles);
        // Roles are not in the tree-8 leaf, so this rewrites the same value —
        // kept anyway so "every identity mutation projects" has no exceptions
        // to remember.
        _projectIdentity(account);
    }

    /// @notice Refuse a mutation that would leave the registrar quorum unreachable.
    /// @dev Once bootstrap is sealed, that is the one change nothing could ever undo: a registry whose
    ///      threshold exceeds its sealable membership can never be written to again, including to fix
    ///      itself. Checked AFTER the write so the count reflects the mutation being attempted.
    function _requireRegistrarQuorumReachable() private view {
        if (!bootstrapSealed) return;
        uint256 sealable = sealableMemberCount(ROLE_REGISTRAR);
        if (sealable < registrarThreshold) {
            revert RegistrarThresholdUnreachable(sealable, registrarThreshold);
        }
    }

    /// @notice Revoke an identity and its certificate. Irreversible.
    /// @dev Clears the roles as well as setting the flag. Both are checked everywhere, but leaving a revoked
    ///      record carrying roles invites a future reader that checks only one of them. The fingerprints of
    ///      the named LMS slots are recorded into the revocation log after the flag lands, so the log's own
    ///      permanence gate sees the transition it requires.
    /// @param account The identity to retire.
    /// @param chainIds The chains whose LMS-key slots this account holds. The registrars supply the list and
    ///        the approval digest binds it, because a mapping cannot enumerate its own keys. A chain with no
    ///        slot is skipped, and a fingerprint an incomplete list missed stays permanently recordable
    ///        through the revocation log's permissionless door, since a revoked account never regains
    ///        standing.
    /// @param anchorBlock The block the registrars read the roster at.
    /// @param approvals The sealed registrar quorum. Empty while bootstrap is open.
    function revoke(
        address account,
        uint64[] calldata chainIds,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireMembershipAuthority(
            DOMAIN_REVOKE, keccak256(abi.encode(account, chainIds)), anchorBlock, approvals
        );
        Identity storage id = _identity[account];
        if (!id.registered) revert UnknownAccount(account);
        id.revoked = true;
        id.roles = 0;
        certificateRevoked[id.certHash] = true;
        _requireRegistrarQuorumReachable();
        emit IdentityRevoked(account, id.certHash);
        // AFTER the flag lands, so the log's own gate sees the permanent
        // transition it requires.
        for (uint256 i = 0; i < chainIds.length; i++) {
            LmsKey storage k = _lmsKey[account][chainIds[i]];
            if (k.registered) _recordRevokedSigner(lmsSignerId(k.keyId, k.height, k.root));
        }
        _projectIdentity(account);
    }

    /**
     * @notice Root-plane GLOBAL certificate revocation, by `certHash`.
     * @dev The half of the revocation lane that gates registration and covers break-glass: any certificate —
     *      registered here, issued off chain, or never seen — can be killed by handle under the registrar
     *      quorum, because the handle is all a break-glass caller may have.
     *
     *      When the handle is a registered identity's CURRENT certificate the identity falls with it: flag,
     *      roles cleared, same-transaction projection. So revoking by handle is never weaker than {revoke};
     *      it only skips the LMS-slot enumeration, and those fingerprints stay permanently recordable
     *      through the revocation log's own permissionless door.
     * @param certHash The certificate to revoke. Need not correspond to any record.
     * @param anchorBlock The block the registrars read the roster at.
     * @param approvals The sealed registrar quorum. Empty while bootstrap is open.
     */
    function revokeCertificate(
        bytes32 certHash,
        uint64 anchorBlock,
        FinalPqQuorum.Approval[] calldata approvals
    ) external {
        _requireMembershipAuthority(
            DOMAIN_REVOKE_CERTIFICATE, keccak256(abi.encode(certHash)), anchorBlock, approvals
        );
        certificateRevoked[certHash] = true;
        address bound = accountOfCertificate[certHash];
        if (bound != address(0)) {
            Identity storage id = _identity[bound];
            if (!id.revoked) {
                id.revoked = true;
                id.roles = 0;
                _requireRegistrarQuorumReachable();
                emit IdentityRevoked(bound, certHash);
                _projectIdentity(bound);
            }
        }
        emit CertificateRevoked(certHash, address(0));
    }

    /**
     * @notice The issuing identity's half of the revocation lane: a registered issuer revokes a certificate
     *         it signed off chain, by `certHash`.
     * @dev This records WHO revoked, and a verifier honours the entry only when the recorded revoker is the
     *      certificate's own issuer — which the verifier knows, because it holds the certificate. It
     *      deliberately does NOT set the global `certificateRevoked` flag: that flag gates registration, and
     *      letting any registered issuer set it for an arbitrary handle would be a griefing lane over other
     *      people's certificates.
     *
     *      Anyone may SUBMIT. Authority is the two signatures — the issuer's registered cert-signing keys
     *      over a digest binding this registry, this chain, the handle and the issuer's own gate nonce, both
     *      verified in the precompiles inside this transaction. The keys come from storage, so a submitter
     *      cannot supply the pair its own signatures verify under.
     *
     *      One-way: the first revoker of a handle is recorded and a second write is refused, because
     *      "revoked twice by two parties" is two facts where this lane models one.
     * @param issuer The registered certificate authority making the statement.
     * @param certHash The certificate being revoked.
     * @param proof The issuer's own ML-DSA-87 and SLH-DSA-SHAKE-256s signatures over the revocation digest.
     */
    function revokeIssuedCertificate(
        address issuer,
        bytes32 certHash,
        AdmissionProof calldata proof
    ) external {
        if (!hasRole(issuer, ROLE_CERTIFICATE_AUTHORITY)) {
            revert IssuerNotACertificateAuthority(issuer);
        }
        if (certificateRevokedBy[certHash] != address(0)) revert CertificateIsRevoked(certHash);
        uint64 nonce = _gateNonce[issuer];
        _gateNonce[issuer] = nonce + 1;
        bytes memory message = abi.encodePacked(
            keccak256(
                abi.encode(
                    DOMAIN_ISSUER_CERT_REVOCATION,
                    block.chainid,
                    address(this),
                    issuer,
                    certHash,
                    nonce
                )
            )
        );
        if (
            !FinalChainPrecompiles.verifyMlDsa87(
                _activeTransactionKey[issuer], message, proof.mlDsaSignature
            )
                || !FinalChainPrecompiles.verifySlhDsa(
                    _activeAccessKey[issuer], message, proof.slhDsaSignature
                )
        ) revert AdmissionProofInvalid(issuer);
        certificateRevokedBy[certHash] = issuer;
        emit CertificateRevoked(certHash, issuer);
    }

    // ---------------------------------------------------------------- views

    /// @notice The full identity record.
    /// @dev Returns the zero struct for an address no record claims, so `registered` is the field to branch
    ///      on rather than any of the hashes.
    /// @param account The identity to read.
    /// @return The stored record, copied to memory.
    function identityOf(address account) external view returns (Identity memory) {
        return _identity[account];
    }

    /// @notice The live transaction key, ML-DSA-87: what a quorum vote is verified against.
    /// @dev Read from STORAGE by every quorum on this chain, never from a caller's argument — a key supplied
    ///      as calldata proves nothing, because anyone holding a keypair can sign under it.
    /// @param account The identity to read.
    /// @return The raw public key, or empty when the account holds none.
    function activeTransactionKeyOf(address account) external view returns (bytes memory) {
        return _activeTransactionKey[account];
    }

    /// @notice The live access key, SLH-DSA-SHAKE-256s: identity, rotation, and guardianship.
    /// @dev A different hardness assumption from the transaction key, so a lattice break leaves the key that
    ///      governs identity standing intact.
    /// @param account The identity to read.
    /// @return The raw public key, or empty when the account holds none.
    function activeAccessKeyOf(address account) external view returns (bytes memory) {
        return _activeAccessKey[account];
    }

    /// @notice The seal key, SLH-DSA-SHAKE-256s: what `FinalPqQuorum` verifies an approval's seal against.
    /// @dev A service's second hash-based key, distinct from its access key, so a quorum decision carries
    ///      one signature from each hardness assumption. Empty when the identity carries no seal, in which
    ///      case it cannot take part in a sealed quorum at all — which is why {sealableMemberCount} counts
    ///      this rather than counting role bits.
    /// @param account The identity to read.
    /// @return The raw public key, or empty when the account holds no seal.
    function activeSealKeyOf(address account) external view returns (bytes memory) {
        return _activeSealKey[account];
    }

    /// @notice The recovery-stage transaction key, ML-DSA-87.
    /// @dev Authorizes rotating this account's own credentials and nothing else — acting as a guardian is an
    ///      ordinary action for an account and uses the live keys. Empty for a certificate authority.
    /// @param account The identity to read.
    /// @return The raw public key, or empty when the account holds none.
    function recoveryTransactionKeyOf(address account) external view returns (bytes memory) {
        return _recoveryTransactionKey[account];
    }

    /// @notice The recovery-stage access key, SLH-DSA-SHAKE-256s.
    /// @dev The other half of the pre-committed recovery stage. Empty for a certificate authority, which has
    ///      no recovery stage at all.
    /// @param account The identity to read.
    /// @return The raw public key, or empty when the account holds none.
    function recoveryAccessKeyOf(address account) external view returns (bytes memory) {
        return _recoveryAccessKey[account];
    }

    /// @notice The four signing-key commitments, in the order tree 1's leaf wants them.
    /// @dev keccak, not SHA3: these feed `FinalWalletFactory.accountStateLeafHash`, which every execution
    ///      chain verifies with, and that one hashes with keccak. An account missing a slot commits to the
    ///      hash of the empty string rather than reverting, so the leaf stays buildable for a certificate
    ///      authority, which holds no recovery pair.
    /// @param account The identity to commit to.
    /// @return liveAccess Commitment to the live access key.
    /// @return liveTransaction Commitment to the live transaction key.
    /// @return recoveryAccess Commitment to the recovery access key.
    /// @return recoveryTransaction Commitment to the recovery transaction key.
    function keyCommitments(address account)
        external
        view
        returns (
            bytes32 liveAccess,
            bytes32 liveTransaction,
            bytes32 recoveryAccess,
            bytes32 recoveryTransaction
        )
    {
        liveAccess = keccak256(_activeAccessKey[account]);
        liveTransaction = keccak256(_activeTransactionKey[account]);
        recoveryAccess = keccak256(_recoveryAccessKey[account]);
        recoveryTransaction = keccak256(_recoveryTransactionKey[account]);
    }

    /**
     * @notice The tree-8 leaf `account` currently earns: the execution chains' identity leaf while the
     *         identity stands, zero once it does not.
     * @dev The leaf VALUE is `keccak256(DOMAIN_IDENTITY_LEAF ‖ serial ‖ keysHash)` — byte-identical to
     *      `IdentityRootModule.identityLeafHash`, which is also the `certHash` inside a wallet's address
     *      derivation — with `keysHash` folded exactly as the certificate issuer folds it:
     *      `keccak256(activeAccess ‖ activeTransaction ‖ recoveryAccess ‖ recoveryTransaction ‖ activeKem ‖
     *      recoveryKem)`, six commitment words packed in slot order. The issuing tooling and this function
     *      are pinned against each other by test over the premined certificate fixtures, because a wallet
     *      whose address was derived from a different fold is a wallet no chain can admit.
     *
     *      Zero — the empty slot's own value, unprovable as a leaf because no certificate hashes to it — for
     *      anything that must not admit a wallet creation: a revoked identity, one outside its validity
     *      window, and any certificate authority. The authority exclusion is STRUCTURAL rather than a role
     *      read: an end entity has `depth == maxDelegationDepth` because it issues nothing, an authority
     *      never does, and that pair is immutable per version where `roles` is not.
     *
     *      Lives here rather than on the state-trees contract that consumes it because every input is this
     *      contract's storage, and the trees contract has no bytecode headroom to spare.
     * @param account The identity to project. Reverts for an account with no record at all.
     * @return The tree-8 leaf value, or zero while the identity does not stand.
     */
    function identityTreeLeafOf(address account) external view returns (bytes32) {
        Identity storage id = _identity[account];
        if (!id.registered) revert UnknownAccount(account);
        if (id.revoked || !_withinValidity(id)) return bytes32(0);
        if (id.depth != id.maxDelegationDepth) {
            // An ISSUER exists in tree 8 under its own domain, so its record is stapleable for offline
            // licence verification while the distinct domain keeps it out of wallet admission. `certHash`
            // suffices — it covers the whole TBS and the verifier holds the certificate — `version` makes
            // supersession move the leaf, and the third word RESERVES the issuer's own certificate-tree
            // anchor, zero until one is wired. Zero-on-revoke above is load-bearing for both record kinds:
            // a fresh staple is an unrevoked statement.
            return keccak256(
                abi.encodePacked(DOMAIN_ISSUER_LEAF, id.certHash, uint64(id.version), bytes32(0))
            );
        }
        bytes32 liveKem = keccak256(
            abi.encodePacked(DOMAIN_KEM_BUNDLE, _activeKemMlKem[account], _activeKemHqc[account]));
        bytes32 recoveryKem = keccak256(
            abi.encodePacked(DOMAIN_KEM_BUNDLE, _recoveryKemMlKem[account], _recoveryKemHqc[account]));
        bytes32 keysHash = keccak256(
            abi.encodePacked(
                keccak256(_activeAccessKey[account]),
                keccak256(_activeTransactionKey[account]),
                keccak256(_recoveryAccessKey[account]),
                keccak256(_recoveryTransactionKey[account]),
                liveKem,
                recoveryKem
            )
        );
        return keccak256(abi.encodePacked(DOMAIN_IDENTITY_LEAF, id.serial, keysHash));
    }

    /// @notice Per-stage encapsulation commitments, in the order the account-state leaf wants them.
    /// @dev One word per STAGE, folded over both of that stage's encapsulation public keys under
    ///      `DOMAIN_KEM_BUNDLE`. The pair is the unit — an account holds both keys or neither — so
    ///      committing to them separately would model a state the protocol does not recognise, and every
    ///      downstream record would carry two words where one says the same thing.
    ///
    ///      An account whose certificate carries no encapsulation stage folds the empty string here rather
    ///      than reverting: the projection into the state trees must keep succeeding for it, and a leaf that
    ///      cannot be built is a party that cannot be revoked.
    /// @param account The identity to commit to.
    /// @return liveKem The live stage's encapsulation commitment.
    /// @return recoveryKem The recovery stage's encapsulation commitment.
    function kemCommitments(address account)
        external
        view
        returns (bytes32 liveKem, bytes32 recoveryKem)
    {
        liveKem = keccak256(
            abi.encodePacked(DOMAIN_KEM_BUNDLE, _activeKemMlKem[account], _activeKemHqc[account]));
        recoveryKem = keccak256(
            abi.encodePacked(DOMAIN_KEM_BUNDLE, _recoveryKemMlKem[account], _recoveryKemHqc[account]));
    }

    /// @notice The live-stage encapsulation keys themselves, for a party composing a sealed message.
    /// @dev Returns both halves of the pair together because the pair is the unit: encapsulating to one
    ///      family alone is indistinguishable on the wire from a hybrid, and silently dropping the hedge is
    ///      the failure this pairing exists to prevent. Empty for an account with no encapsulation stage.
    /// @param account The party to encapsulate to.
    /// @return activeMlKem The lattice half, ML-KEM-1024.
    /// @return activeHqc The code-based half, HQC-5.
    function kemKeysOf(address account)
        external
        view
        returns (bytes memory activeMlKem, bytes memory activeHqc)
    {
        return (_activeKemMlKem[account], _activeKemHqc[account]);
    }

    // ------------------------------------------------------------- senders

    /**
     * @notice The sender address a transaction key produces on this chain.
     * @dev `keccak256(uint8(4) ‖ publicKey)[12:]` — byte-identical to what the node derives from a
     *      post-quantum transaction envelope and to the backend's own derivation. The leading algorithm byte
     *      is what domain-separates it, so a key of another family can never derive the same address.
     *
     *      Pure, so a client can compute the address from a certificate before the identity is registered —
     *      which is what lets an admission transaction be funded and submitted from the very sender it is
     *      about to bind.
     * @param transactionKey The raw ML-DSA-87 public key.
     * @return The sender address that key signs from.
     */
    function senderFor(bytes memory transactionKey) public pure returns (address) {
        return address(uint160(uint256(keccak256(abi.encodePacked(ENVELOPE_ALG_ML_DSA_87, transactionKey)))));
    }

    /// @notice The sender `account`'s transactions arrive from.
    /// @dev The forward direction of {accountOfSender}, derived rather than stored, so it cannot disagree
    ///      with the transaction key on record.
    /// @param account The identity to resolve.
    /// @return The derived sender, or zero for an account with no transaction key on record.
    function senderOf(address account) external view returns (address) {
        bytes storage key = _activeTransactionKey[account];
        if (key.length == 0) return address(0);
        return senderFor(key);
    }

    /// @notice {hasRole} for a `msg.sender`: resolves the sender to its identity first.
    /// @dev The form every `msg.sender` gate on this chain uses. A sender is derived from a transaction key
    ///      and holds no authority itself, so asking it directly would be asking the wrong address. False for
    ///      a sender no identity claims.
    /// @param sender The address a transaction arrived from.
    /// @param roleMask The capability required.
    /// @return Whether the identity behind that sender stands and carries the whole mask.
    function senderHasRole(address sender, uint256 roleMask) external view returns (bool) {
        address account = accountOfSender[sender];
        return account != address(0) && hasRole(account, roleMask);
    }

    /// @notice How many accounts carrying `roleMask` also hold a seal key — the members that can take part
    ///         in a sealed quorum.
    /// @dev The count every membership threshold is checked against, because membership approvals are the
    ///      hybrid class and a member with no seal can never contribute one. A certificate authority
    ///      carrying `ROLE_REGISTRAR` is registered from a certificate with no seal slot, so it is counted
    ///      out here rather than being discovered at the first quorum that fails to reach its threshold.
    /// @param roleMask The capability the quorum is over.
    /// @return sealable How many standing accounts carry the mask and hold a seal key.
    function sealableMemberCount(uint256 roleMask) public view returns (uint256 sealable) {
        uint256 n = _accounts.length;
        for (uint256 i = 0; i < n; i++) {
            address a = _accounts[i];
            if (hasRole(a, roleMask) && _activeSealKey[a].length != 0) sealable++;
        }
    }

    /// @notice Number of registered accounts.
    /// @dev Never decreases: revocation clears a record's roles and sets its flag but leaves it in the list,
    ///      so an index handed out once keeps pointing at the same account for good.
    /// @return How many accounts have ever been registered.
    function accountCount() external view returns (uint256) {
        return _accounts.length;
    }

    /// @notice Registered account by index, in registration order.
    /// @dev Reverts on an out-of-range index rather than answering zero, so a caller paging the list cannot
    ///      mistake the end of it for a hole in the middle.
    /// @param index Position in the registration-ordered list, below {accountCount}.
    /// @return The account at that position.
    function accountAt(uint256 index) external view returns (address) {
        return _accounts[index];
    }

    /// @notice Every account carrying every bit in `roleMask`.
    /// @dev A view, so the linear scan over the account list costs nothing to a caller reading off chain.
    ///      Callers that need a roster inside a transaction pass the member list explicitly instead — see
    ///      `FinalPqQuorum`, which takes signers rather than searching for them, so a quorum's cost does not
    ///      grow with the size of the registry.
    /// @param roleMask The capability to filter on.
    /// @return found The matching accounts, in registration order.
    function accountsWithRole(uint256 roleMask) external view returns (address[] memory found) {
        uint256 n = _accounts.length;
        address[] memory buf = new address[](n);
        uint256 count;
        for (uint256 i = 0; i < n; i++) {
            if (hasRole(_accounts[i], roleMask)) {
                buf[count++] = _accounts[i];
            }
        }
        found = new address[](count);
        for (uint256 i = 0; i < count; i++) {
            found[i] = buf[i];
        }
    }

    /**
     * @notice How many accounts could satisfy a quorum for `roleMask` right now.
     * @dev The number a threshold has to be reachable against. A threshold above it is not a strict quorum,
     *      it is a quorum that cannot be met — and the way that presents is an operation reverting forever
     *      with nothing naming the roster as the cause. Counts standing alone; use {sealableMemberCount} for
     *      a quorum that also needs a seal.
     * @param roleMask The capability the quorum is over.
     * @return live How many standing accounts carry the whole mask.
     */
    function liveMemberCount(uint256 roleMask) public view returns (uint256 live) {
        uint256 n = _accounts.length;
        for (uint256 i = 0; i < n; i++) {
            if (hasRole(_accounts[i], roleMask)) live++;
        }
    }

    /**
     * @notice Whether `account` currently carries every bit in `roleMask`.
     * @dev Every gate in this system asks this one question, so every gate gets the same answer: registered,
     *      not revoked, inside its validity window, and holding the capability. A caller that checked only
     *      the role bit would accept an expired certificate.
     *
     *      `roleMask == 0` is false. A zero mask asks nothing and must not read as "yes" — that is the shape
     *      of an uninitialised configuration variable, and the one reading it must not be a universal pass.
     *
     *      Every bit in the mask must be present, so a mask naming two capabilities asks for both rather than
     *      either.
     * @param account The account to test.
     * @param roleMask One or more `ROLE_*` bits, OR-ed together.
     * @return Whether the account stands and carries the whole mask.
     */
    function hasRole(address account, uint256 roleMask) public view returns (bool) {
        if (roleMask == 0) return false;
        Identity storage id = _identity[account];
        if (!id.registered || id.revoked) return false;
        if (id.roles & roleMask != roleMask) return false;
        return _withinValidity(id);
    }

    /// @notice Whether `account` is registered, unrevoked and in date, regardless of capability.
    /// @dev The standing half of {hasRole}, for callers that care that a party is honoured at all rather
    ///      than that it holds a particular capability. {lmsSignerIsLive} asks this rather than spelling the
    ///      three conditions out a second time, because a second spelling is how two answers drift apart.
    /// @param account The account to test. An address no record claims answers false.
    /// @return Whether the identity currently stands.
    function isActive(address account) public view returns (bool) {
        Identity storage id = _identity[account];
        return id.registered && !id.revoked && _withinValidity(id);
    }

    /// @notice Whether a record's certificate is inside its validity window right now.
    /// @dev Both bounds are milliseconds on this chain's clock and both are optional: a zero `notBefore`
    ///      means valid from issuance and a zero `notAfter` means never expires, which the certificate
    ///      schema allows and personal identity certificates use. The upper bound is exclusive, so a
    ///      certificate stops being honoured on the millisecond it names rather than after it.
    /// @param id The record to test, taken as a storage pointer so no copy of a multi-word struct is made.
    /// @return Whether the window admits the current block time.
    function _withinValidity(Identity storage id) private view returns (bool) {
        if (id.notBefore != 0 && FinalChainTime.nowMs() < id.notBefore) return false;
        if (id.notAfter != 0 && FinalChainTime.nowMs() >= id.notAfter) return false;
        return true;
    }


    // ------------------------------------------------------------------ sweep

    /// @inheritdoc FinalSweep
    /// @dev The registry's own configuration gate, in the `msg.sender` form a no-argument seam can express:
    ///      the bootstrap admin alone while the window is open, a live registrar afterwards.
    ///
    ///      The rest of the state plane inherits this rule from `FinalPlaneSweep`, which reads it off a
    ///      registry pointer. This contract answers it from its own storage because it IS that registry, and
    ///      importing the shared mixin here would make this file import a file that imports it back.
    ///
    ///      The sealed half of the gate is a K-of-N over `ROLE_REGISTRAR` whose approvals arrive in calldata,
    ///      which `sweepAsset`'s shared signature has no room for; what survives is membership in that same
    ///      roster. The narrowing is safe because the other two gates hold regardless: a sweep moves surplus
    ///      only, this contract owes nothing, so there is nothing behind the line to reach — and the
    ///      destination is not the caller's to invent.
    function _requireSweepAuthority() internal view override {
        if (!bootstrapSealed && msg.sender == bootstrapAdmin) return;
        if (hasRole(msg.sender, ROLE_REGISTRAR)) return;
        revert SweepUnauthorized(msg.sender);
    }

    /// @inheritdoc FinalSweep
    /// @dev The bootstrap admin, and the proven authority that called. The first of those is zero once the
    ///      window is sealed, which `FinalSweep` refuses as a destination, so a sealed registry can only
    ///      sweep to the registrar that authorised the sweep.
    function _sweepDestinations() internal view override returns (address, address) {
        return (bootstrapAdmin, msg.sender);
    }

    /// @dev Nothing is reserved because nothing is owed: the registry holds
    /// certificates and role bits, has no payable entrypoint and no custody
    /// line. Anything it carries arrived by accident.
}

contracts/finalchain/FinalPqQuorum.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
//
// Change Date: 2029-01-01
// Change License: GPL-2.0-or-later
//
// Additional Use Grant:
// 1. Any person or entity may deploy and operate this quorum as part of a
//    Final DeFi Protocol chain, and may inherit it to gate an action behind a
//    post-quantum K-of-N.
// 2. Integrators, auditors, and node operators may read its membership and
//    thresholds and independently re-verify any approval it recorded, as part
//    of their integration with the Final DeFi Protocol.
// 3. For the avoidance of doubt, this Grant does NOT permit the commercial
//    deployment of a Fork of this quorum or a competing identity or
//    authorization plane derived from it without permission prior to the
//    Change Date.
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;

import {FinalChainPrecompiles} from "./FinalChainPrecompiles.sol";
import {FinalIdentityRegistry} from "./FinalIdentityRegistry.sol";

/**
 * @title FinalPqQuorum
 * @notice K-of-N approval where the signatures are post-quantum and the chain
 *         is what checks them.
 *
 * @dev This library is the reason Final Chain exists in this design.
 *
 * `FinalBackend/src/pq/credential.js` carries a rule it had to enforce in code
 * because nothing else could: **a surface whose signature is verified on chain
 * cannot be PQ.** A co-signer approval reaching `FinalRootAuthority` is checked
 * by ECDSA/ERC-1271 in Solidity, so a PQ co-signer would produce approvals the
 * contract cannot read, and the quorum would stop reaching threshold with
 * nothing in any log naming the cause. `PQ_SURFACE` and `assertBackendVerified`
 * exist to keep anyone from crossing that line by accident.
 *
 * Here the line is gone. The precompiles verify ML-DSA-87 and
 * SLH-DSA-SHAKE-256s natively, so a quorum can be PQ *and* on chain, and
 * "the backend says these four signatures verified" becomes "these four
 * signatures verify, and any node re-derives that independently".
 *
 * ## Three rules, each closing a specific hole
 *
 * 1. **Keys come from the registry, never from calldata.** A key passed as an
 *    argument proves nothing — anyone with a keypair can sign under it. This is
 *    the difference between a 4-of-5 quorum and a 1-of-1 held by whoever built
 *    the transaction.
 *
 * 2. **Signers strictly ascending.** One comparison per entry rejects duplicates
 *    outright, so a single member cannot supply four approvals and satisfy a
 *    threshold of four. The alternative — an O(n²) seen-check — is the same
 *    guarantee with more ways to get it wrong.
 *
 * 3. **The digest binds chain id and verifying contract.** Without both, an
 *    approval collected for one contract is replayable against another with the
 *    same payload shape, and an approval from the test chain is replayable on
 *    the production one. These co-signers hold one key across environments.
 *
 * ## Which algorithm
 *
 * The stack splits its keys by hardness assumption, not by convenience:
 * ML-DSA-87 (lattice) signs transactions, SLH-DSA-SHAKE-256s (hash-based) signs
 * identity. Two families, so one cryptanalytic result cannot take both.
 *
 * So an action inherits the class of what it authorizes. Advancing a state root
 * is operational and high-cadence: transaction class. Registering or revoking
 * an identity is the thing the access class exists for. `ALG_ANY` is available
 * and should be used sparingly — accepting either means a break in one family
 * takes the quorum.
 *
 * A MEMBERSHIP action — the registrar quorum that admits, re-roles or revokes
 * an identity and upgrades a plane contract — takes both: the ML-DSA-87
 * approval and a `seal`, an SLH-DSA-SHAKE-256s signature over the same digest
 * by the member's `activeSeal` key. The seal key is its own slot — never the
 * access key — so the process that seals cannot also rotate the identity it
 * seals for. Every OPERATIONAL action — a bundle root or payload appended to
 * the log, a settlement leaf, an account-state write, a tree write, a PHI
 * movement — takes the ML-DSA-87 approval alone (the user's ruling of 12 Sep
 * 2026, arch/quorum-signing-ml-dsa.md Q1/Q4): the SLH-DSA family is exercised
 * at the boundary where a member JOINS — the joiner's own proof of possession
 * over the admission digest, verified here through `0x0205` — and by a holder
 * on its ledger actions, not on every bundle. An SLH-DSA seal costs a Cloud Run
 * co-signer about forty seconds per digest, and the fleet paid it once per
 * member per bundle; ML-DSA-87 signs in milliseconds under the key the member
 * already votes with.
 *
 * Every digest binds an `anchorBlock`: the block at which the members read
 * tree 1 to decide who is in the round. Binding it means every approval in a
 * round was made against ONE roster view, and the window in `require_` means a
 * view older than `ANCHOR_WINDOW` blocks is refused rather than honoured.
 *
 * The practical cost is worth stating: an SLH-DSA signature is 29,792 bytes, so
 * a 4-of-5 membership-class quorum is ~119 KB of calldata. That is affordable
 * here only because this is our own chain and membership changes are rare. Do
 * not carry this pattern to a chain where it is not.
 */
library FinalPqQuorum {
    /// @notice ML-DSA-87 — FIPS 204. Algorithm ids are the FIPS numbers: the
    /// same ids `FinalCertificate` and the backend registry use, and the numbers
    /// the precompile addresses end in (`0x0204`).
    uint8 internal constant ALG_ML_DSA_87 = 4;
    /// @notice SLH-DSA-SHAKE-256s — FIPS 205 (`0x0205`).
    uint8 internal constant ALG_SLH_DSA_SHAKE_256S = 5;
    /// @notice Either scheme is acceptable for this action.
    uint8 internal constant ALG_ANY = 0;

    /// @notice How far behind the chain head an approval's anchor may sit.
    /// @dev Members evaluate roster membership against tree 1 AT the anchor
    /// block. 600 blocks is ten minutes at the chain's one-second cadence —
    /// generous against a round that takes seconds, and short enough that a
    /// roster rotated away is refused rather than counted.
    uint64 internal constant ANCHOR_WINDOW = 600;

    /// @dev Domain separator for every quorum digest. Distinct from any
    /// EIP-712 domain in the stack: these are not typed-data signatures and
    /// must not be confusable with one.
    bytes32 internal constant DOMAIN_PQ_QUORUM = keccak256("FINAL_CHAIN_PQ_QUORUM_v01");

    /// @notice One member's approval.
    struct Approval {
        /// The member's account, which is also the key it is looked up by.
        address signer;
        /// `ALG_ML_DSA_87` or `ALG_SLH_DSA_SHAKE_256S`.
        uint8 algorithm;
        /// Over the 32-byte digest from `digest()`, verbatim. Both schemes
        /// hash internally, so the digest is not re-hashed before signing.
        bytes signature;
        /// SLH-DSA-SHAKE-256s over the same digest, by the member's `activeSeal`
        /// key. Required by the membership class (the registrar quorum); an
        /// operational action never reads it, so it is empty there.
        bytes seal;
    }

    /// @notice Thrown when fewer valid approvals were supplied than the action requires.
    /// @param valid Approvals that verified.
    /// @param required Approvals the action demands.
    error ThresholdNotMet(uint256 valid, uint256 required);
    /// @notice Thrown when approvals are not in strictly ascending signer order.
    /// @dev Ascending order is what makes duplicate detection a single comparison instead of a quadratic scan,
    ///      so it is the rule that stops one signer being counted twice toward a threshold.
    /// @param previous The preceding signer.
    /// @param next The signer that failed to exceed it.
    error SignersNotAscending(address previous, address next);
    /// @notice Thrown when an approving signer does not hold the role this action is gated on.
    /// @param signer The approving signer.
    /// @param roleMask The role the action requires.
    error SignerLacksRole(address signer, uint256 roleMask);
    /// @notice Thrown when an approval is signed under an algorithm this action does not accept.
    /// @param signer The approving signer.
    /// @param got The algorithm the approval declared.
    /// @param required The algorithm the action demands.
    error WrongAlgorithm(address signer, uint8 got, uint8 required);
    /// @notice Thrown when an approval's signature fails verification in the precompile.
    /// @param signer The approving signer.
    /// @param algorithm The algorithm it was verified under.
    error BadSignature(address signer, uint8 algorithm);
    /// @notice Thrown when an approval's access seal fails verification.
    /// @param signer The approving signer.
    error BadSeal(address signer);
    /// @notice Thrown when an approval anchors to a block this chain has not reached.
    /// @param anchorBlock The block the approval anchored to.
    /// @param blockNumber The current block.
    error AnchorAhead(uint64 anchorBlock, uint256 blockNumber);
    /// @notice Thrown when an approval's anchor is older than the accepted window.
    /// @dev Bounding the window is what stops an approval collected once being replayed indefinitely later.
    /// @param anchorBlock The block the approval anchored to.
    /// @param blockNumber The current block.
    error AnchorStale(uint64 anchorBlock, uint256 blockNumber);
    /// @notice Thrown when an action is gated on a threshold of zero.
    /// @dev Refused rather than treated as "no approvals needed": a zero threshold is always a
    ///      misconfiguration, and reading it as permissive would silently remove the quorum.
    error ThresholdIsZero();

    /**
     * @notice The message every member of this quorum signs.
     * @param verifyingContract The contract consuming the approvals. Binding it
     *        stops an approval collected for one contract being replayed
     *        against another with the same payload shape.
     * @param actionDomain What is being authorized — a per-action constant, so
     *        an approval for "advance the accounts tree" cannot be replayed as
     *        one for "revoke an identity".
     * @param anchorBlock The Final Chain block the members read tree 1 at to
     *        decide the roster. Bound here so every approval in a round names
     *        the same view; checked against `ANCHOR_WINDOW` by `require_`.
     * @param payloadDigest The action's own committed content. Callers MUST
     *        include a nonce or a monotonic counter in it; nothing here can
     *        tell a replay of round 7 from a fresh round 7.
     */
    function digest(
        address verifyingContract,
        bytes32 actionDomain,
        uint64 anchorBlock,
        bytes32 payloadDigest
    ) internal view returns (bytes32) {
        return keccak256(
            abi.encode(
                DOMAIN_PQ_QUORUM,
                block.chainid,
                verifyingContract,
                actionDomain,
                anchorBlock,
                payloadDigest
            )
        );
    }

    /**
     * @notice Reverts unless at least `threshold` distinct members holding
     *         `roleMask` have signed `quorumDigest`.
     * @param registry Where public keys and roles come from. Not a parameter
     *        for flexibility — a parameter so the caller's own immutable
     *        registry address is what is used, rather than one from calldata.
     * @param requiredAlgorithm `ALG_ANY` to accept either scheme.
     * @param anchorBlock The anchor the digest was built over. Refused if it is
     *        ahead of this block or more than `ANCHOR_WINDOW` behind it.
     * @param requireSeal Whether every approval must also carry a valid `seal`
     *        by the member's `activeSeal` key — the membership class (the
     *        registrar quorum). Operational actions pass `false`.
     * @return valid The number of approvals that verified, which is at least
     *         `threshold` if this returns at all.
     *
     * @dev Every failure reverts with the offending signer named. A quorum that
     * silently skipped bad approvals and counted the rest would let a
     * misconfigured co-signer sit broken indefinitely: the threshold would keep
     * being met by the others and nothing would say one member had stopped
     * contributing. That is exactly the failure this program has already had,
     * in `fanOut`, where a per-chain advance failure was recorded and execution
     * continued.
     */
    function require_(
        FinalIdentityRegistry registry,
        Approval[] calldata approvals,
        bytes32 quorumDigest,
        uint256 roleMask,
        uint256 threshold,
        uint8 requiredAlgorithm,
        uint64 anchorBlock,
        bool requireSeal
    ) internal view returns (uint256 valid) {
        if (threshold == 0) revert ThresholdIsZero();
        if (anchorBlock > block.number) revert AnchorAhead(anchorBlock, block.number);
        if (block.number - anchorBlock > ANCHOR_WINDOW) revert AnchorStale(anchorBlock, block.number);

        bytes memory message = abi.encodePacked(quorumDigest);
        address previous = address(0);

        uint256 n = approvals.length;
        for (uint256 i = 0; i < n; i++) {
            Approval calldata a = approvals[i];

            // Strictly ascending. `address(0)` as the initial value works
            // because it can never be a registered signer.
            if (a.signer <= previous) revert SignersNotAscending(previous, a.signer);
            previous = a.signer;

            if (!registry.hasRole(a.signer, roleMask)) revert SignerLacksRole(a.signer, roleMask);

            if (requiredAlgorithm != ALG_ANY && a.algorithm != requiredAlgorithm) {
                revert WrongAlgorithm(a.signer, a.algorithm, requiredAlgorithm);
            }

            if (!_verify(registry, a, message)) revert BadSignature(a.signer, a.algorithm);
            if (requireSeal && !_verifySeal(registry, a, message)) revert BadSeal(a.signer);

            valid++;
        }

        if (valid < threshold) revert ThresholdNotMet(valid, threshold);
    }

    /// @notice Non-reverting form, for views and for callers that want to
    /// report rather than refuse.
    function count(
        FinalIdentityRegistry registry,
        Approval[] calldata approvals,
        bytes32 quorumDigest,
        uint256 roleMask,
        uint8 requiredAlgorithm,
        uint64 anchorBlock,
        bool requireSeal
    ) internal view returns (uint256 valid) {
        if (anchorBlock > block.number || block.number - anchorBlock > ANCHOR_WINDOW) return 0;
        bytes memory message = abi.encodePacked(quorumDigest);
        address previous = address(0);
        uint256 n = approvals.length;
        for (uint256 i = 0; i < n; i++) {
            Approval calldata a = approvals[i];
            if (a.signer <= previous) return valid;
            previous = a.signer;
            if (!registry.hasRole(a.signer, roleMask)) continue;
            if (requiredAlgorithm != ALG_ANY && a.algorithm != requiredAlgorithm) continue;
            if (!_verify(registry, a, message)) continue;
            if (requireSeal && !_verifySeal(registry, a, message)) continue;
            valid++;
        }
    }

    /// @dev The seal: SLH-DSA-SHAKE-256s by the member's `activeSeal` key over
    /// the same digest. A member with no seal key on record cannot seal, and an
    /// approval with no seal bytes is not one.
    function _verifySeal(
        FinalIdentityRegistry registry,
        Approval calldata a,
        bytes memory message
    ) private view returns (bool) {
        bytes memory key = registry.activeSealKeyOf(a.signer);
        if (key.length == 0 || a.seal.length == 0) return false;
        return FinalChainPrecompiles.verifySlhDsa(key, message, a.seal);
    }

    /// @dev Verifies one approval against the key the REGISTRY holds for that signer, never against a key
    ///      supplied in the approval. A key passed as an argument proves nothing, because anyone holding a
    ///      keypair can sign under it; reading from storage is what makes the verdict re-derivable from public
    ///      state rather than a claim by whoever assembled the call.
    /// @param registry The identity registry that holds each signer's live keys.
    /// @param a The approval being verified.
    /// @param message The exact bytes the approval must cover.
    /// @return valid True when the signature verifies under the signer's live key for the declared algorithm.
    function _verify(
        FinalIdentityRegistry registry,
        Approval calldata a,
        bytes memory message
    ) private view returns (bool) {
        // The LIVE pair, always. The recovery pair authorizes rotating this
        // account's own credentials and NOTHING else — a quorum that accepted
        // it would hand the recovery keys everyday authority, which is exactly
        // the separation the two stages exist to draw.
        if (a.algorithm == ALG_ML_DSA_87) {
            return FinalChainPrecompiles.verifyMlDsa87(
                registry.activeTransactionKeyOf(a.signer), message, a.signature
            );
        }
        if (a.algorithm == ALG_SLH_DSA_SHAKE_256S) {
            return FinalChainPrecompiles.verifySlhDsa(
                registry.activeAccessKeyOf(a.signer), message, a.signature
            );
        }
        // Any other id is a refusal, never a default — including the KEM ids
        // (3, 7) and the reserved FN-DSA id (6), none of which is a signature
        // scheme this quorum verifies.
        return false;
    }
}

contracts/proxy/FinalProxy.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
//
// Change Date: 2029-01-01
// Change License: GPL-2.0-or-later
//
// Additional Use Grant:
// 1. Any person or entity may deploy and operate this shared upgradeable proxy
//    as part of the Final DeFi Protocol to host chain-global wallet-side
//    contracts such as `Final Gateway`, `Final Chance Vault`, the
//    `Final Recovery Authority`, and the `Final Recovery Module`.
// 2. Protocol operators, integrators, dApps, relayers, and end users may call
//    through this proxy, rely on the addresses it exposes, and rotate the
//    implementation behind an instance they are authorized to upgrade, when
//    interacting with the Final DeFi Protocol.
// 3. For the avoidance of doubt, this Grant does NOT permit the commercial
//    deployment of a Fork of this proxy or a competing wallet/relayer stack
//    derived from it without permission prior to the Change Date.
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;

import {StorageSlot} from "@openzeppelin/contracts/utils/StorageSlot.sol";
import {IFinalAccessManager} from "../IFinalAccessManager.sol";

/**
 * @title IProxyAccessManagerProbe
 * @notice Minimal read surface for the access-manager pointer an implementation publishes
 *         (`FinalAccessController.accessManager`).
 * @dev Declared here rather than imported so this proxy carries no compile-time dependency on the
 *      implementation tree it hosts. The proxy invokes it on ITSELF, so the call lands on the
 *      fallback and is answered by whichever implementation is currently installed. Implementations
 *      are not required to expose this view; when one does not, the probe reverts and the caller
 *      reads that as "no pointer published" rather than propagating the failure.
 */
interface IProxyAccessManagerProbe {
    /// @notice Returns the access manager the installed implementation authorizes its own role checks against.
    /// @return The implementation-side access manager, as the implementation reports it.
    function accessManager() external view returns (address);
}

/**
 * @title Final Proxy Base
 * @notice Shared delegation core behind every Final proxy: the chain-global upgradeable proxy and the
 *         factory-bound per-account proxies alike.
 * @dev The whole design of this base is that a concrete proxy owes it exactly one thing — an answer to
 *      "which implementation serves this call". Everything else about how an implementation is chosen
 *      (an ERC-1967 slot, a beacon, a factory view) belongs to the subclass, which is why the address
 *      resolver is `virtual` and nothing here reads storage.
 *
 *      Management logic is deliberately kept off this base. A delegated call therefore costs one
 *      implementation resolution plus a `delegatecall`, with no role check, no reentrancy guard and no
 *      branch that a plain proxy fallback would not also pay. Any authority a concrete proxy needs is
 *      added by that proxy under selectors it owns, never here.
 *
 *      Both `fallback` and `receive` delegate, so an implementation sees native transfers as well as
 *      calldata-bearing calls and can account for them. Concrete proxies that want a cheap plain
 *      transfer override `receive` to accept without delegating; overriding costs the implementation
 *      the ability to observe those transfers, which is the trade that override makes.
 *
 *      Nothing here is upgradeable in itself: this base declares no storage at all, so it can never
 *      collide with an implementation's layout. Proxies that need their own state read and write
 *      unstructured slots derived from a namespaced string, for the same reason.
 */
abstract contract FinalProxyBase {
    /// @notice Thrown when a call is delegated while no implementation address can be resolved.
    /// @dev The fail-closed posture of the whole proxy family. A zero resolution means an unseeded
    ///      pointer, an uninitialized proxy, or a resolver whose source is not yet live; delegating
    ///      to the zero address would instead succeed silently, because a call to an address with no
    ///      code returns success with empty returndata. Reverting turns that into a visible failure.
    error ImplementationNotSet();

    /// @notice Delegates every call carrying calldata to the resolved implementation.
    /// @dev `payable` so an implementation may take value alongside calldata; the value stays with
    ///      this proxy, since `delegatecall` executes the implementation's code in this contract's
    ///      own storage and balance context.
    fallback() external payable virtual {
        _delegate();
    }

    /// @notice Receives native value and delegates so the implementation can observe the transfer.
    /// @dev Delegating an empty-calldata call reaches the implementation's own `receive`, which is what
    ///      lets an implementation account for incoming value. Subclasses that override this to accept
    ///      without delegating trade that visibility for the gas of the resolution and the delegate hop.
    receive() external payable virtual {
        _delegate();
    }

    /// @notice Resolves the implementation address that should serve the current call.
    /// @dev The single extension point of this base. Implementations of this function must be `view`
    ///      and must fail closed — returning zero rather than a stale or attacker-chosen address when
    ///      their source of truth is unavailable — because a zero is caught here and anything else is
    ///      executed with full authority over this proxy's storage.
    /// @return implementation Address to delegate to, or zero when none is resolvable.
    function _proxyImplementation() internal view virtual returns (address implementation);

    /// @notice Delegates the current call verbatim and returns or reverts with the raw result.
    /// @dev Calldata, returndata and revert data all pass through untouched, so callers cannot tell a
    ///      delegated call from a direct one — the property every ABI consumer of a Final proxy relies on.
    ///
    ///      The assembly copies calldata over memory starting at offset 0 and ends the frame with
    ///      `return` or `revert`, so it never yields control back to Solidity and the scratch space and
    ///      free-memory pointer it clobbers are never read again. It is intentionally not annotated
    ///      `("memory-safe")`, because it does write below the free-memory pointer; the terminal
    ///      `return`/`revert` is what makes that safe, not the annotation.
    function _delegate() internal {
        address implementationAddress = _proxyImplementation();
        if (implementationAddress == address(0)) revert ImplementationNotSet();

        assembly {
            calldatacopy(0, 0, calldatasize())
            let result := delegatecall(gas(), implementationAddress, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch result
            case 0 {
                revert(0, returndatasize())
            }
            default {
                return(0, returndatasize())
            }
        }
    }

    /// @notice Executes one delegatecall against a named implementation and bubbles up any revert reason.
    /// @dev The non-terminal counterpart to `_delegate`: it returns to Solidity, so the caller can keep
    ///      working after the call — which is what initialization and upgrade flows need in order to run
    ///      their post-conditions. Revert data is re-raised byte-for-byte so a failing implementation
    ///      initializer surfaces its own custom error rather than an opaque proxy-level failure.
    ///
    ///      This helper performs NO validation of `implementationAddress`. Callers must have established
    ///      that the target carries code first, because a delegatecall into a codeless address succeeds
    ///      with empty returndata and would make a broken upgrade look like a completed one.
    /// @param implementationAddress Target implementation address, already validated by the caller.
    /// @param data Calldata to execute via delegatecall.
    /// @return returndata Raw delegatecall returndata, undecoded.
    function _delegateCall(address implementationAddress, bytes memory data) internal returns (bytes memory returndata) {
        (bool success, bytes memory result) = implementationAddress.delegatecall(data);
        if (!success) {
            assembly {
                revert(add(result, 0x20), mload(result))
            }
        }

        return result;
    }
}

/**
 * @title Final Factory Bound Proxy Base
 * @notice Shared one-time factory binding for the per-account proxies whose implementation is resolved
 *         through `Final Wallet Factory` rather than held in a slot of their own.
 * @dev These proxies are bound to their factory in the constructor and never again. There is no public
 *      initializer and no rotation path: the binding is written during creation, in the same transaction
 *      that brings the proxy into existence, so there is no window in which an unbound proxy sits on
 *      chain waiting for someone to claim it. That closes the entire class of initializer front-running
 *      that a two-step deploy-then-initialize proxy has to defend against.
 *
 *      Because the binding is permanent, upgradeability lives entirely on the far side of it: the proxy
 *      asks its resolver for the current implementation on every call, and rotating that pointer migrates
 *      every deployed proxy at once without any user-facing address changing. A per-proxy admin would
 *      have given each account its own upgrade surface and its own way to diverge; there is none here.
 *
 *      All state lives in unstructured slots derived from namespaced strings, never in declared variables.
 *      That is mandatory rather than stylistic: these proxies share their storage with an implementation
 *      that declares its own layout from slot 0, and a declared variable here would occupy a slot the
 *      implementation also believes it owns.
 */
abstract contract FinalFactoryBoundProxyBase is FinalProxyBase {
    /// @dev Unstructured slot holding the bound factory address. Namespaced so it cannot collide with
    ///      the layout of the implementation that shares this proxy's storage.
    bytes32 private constant FACTORY_SLOT = keccak256("final.proxy.factory");
    /// @dev Unstructured slot holding the one-shot binding latch. Kept separate from `FACTORY_SLOT`
    ///      so "bound" is recorded independently of the value, and a binding can never be replayed by
    ///      reasoning about a zero address.
    bytes32 private constant FACTORY_INITIALIZED_SLOT = keccak256("final.proxy.factoryInitialized");

    /// @notice Thrown when factory binding is attempted on a proxy that is already bound.
    /// @dev Unreachable through the canonical constructor-only path; it exists so the latch is enforced
    ///      by the code rather than by the convention that only a constructor ever calls the binder.
    error FactoryProxyAlreadyInitialized();
    /// @notice Thrown when the zero address is offered as the factory.
    /// @dev A zero factory would make every implementation resolution revert, permanently bricking a
    ///      proxy whose binding cannot be rotated.
    error InvalidFactory();

    /// @notice Emitted once, at creation, when the proxy binds to its factory.
    /// @dev Emitted exactly once in the lifetime of a proxy, which makes it the canonical indexing
    ///      signal that a given address is a Final proxy bound to a given factory.
    /// @param factory The factory this proxy is permanently bound to.
    event ProxyFactoryInitialized(address indexed factory);

    /// @notice Returns the factory this proxy is permanently bound to.
    /// @return factory The bound factory address.
    function proxyFactory() public view returns (address factory) {
        return _proxyFactory();
    }

    /// @notice Returns the bound factory address under the uppercase ABI shape integrators read.
    /// @dev Deliberately named against the usual casing convention: the per-account proxies expose the
    ///      binding as `FACTORY()`, and off-chain consumers probe every Final proxy through that one
    ///      selector. Renaming it would change the selector and break those readers.
    /// @return factory The bound factory address.
    // forge-lint: disable-next-line(mixed-case-function)
    function FACTORY() public view returns (address factory) {
        return _proxyFactory();
    }

    /// @notice Returns true once the factory pointer has been bound.
    /// @dev Always true for any proxy an observer can call, because binding happens in the constructor;
    ///      it is exposed so the latch is externally verifiable rather than assumed.
    /// @return initialized True when the one-shot binding has run.
    function proxyFactoryInitialized() external view returns (bool initialized) {
        return _proxyFactoryInitialized();
    }

    /// @notice Performs the one-time, irreversible factory binding.
    /// @dev Intended to be called only from a concrete proxy's constructor, so the proxy is already bound
    ///      by the time its address exists on chain and no one else can bind it first. The latch is set
    ///      before the pointer is written, so a re-entrant call reached through any future code path finds
    ///      the proxy already bound.
    /// @param factory Factory that will resolve this proxy's implementation for the rest of its life.
    function _initializeFactoryProxy(address factory) internal {
        if (_proxyFactoryInitialized()) revert FactoryProxyAlreadyInitialized();
        if (factory == address(0)) revert InvalidFactory();
        StorageSlot.getBooleanSlot(FACTORY_INITIALIZED_SLOT).value = true;
        StorageSlot.getAddressSlot(FACTORY_SLOT).value = factory;
        emit ProxyFactoryInitialized(factory);
    }

    /// @notice Reads the bound factory address out of its unstructured slot.
    /// @return factory The bound factory address, or zero on a proxy that was never bound.
    function _proxyFactory() internal view returns (address factory) {
        return StorageSlot.getAddressSlot(FACTORY_SLOT).value;
    }

    /// @notice Reads the one-shot binding latch out of its unstructured slot.
    /// @return initialized True once `_initializeFactoryProxy` has run.
    function _proxyFactoryInitialized() internal view returns (bool initialized) {
        return StorageSlot.getBooleanSlot(FACTORY_INITIALIZED_SLOT).value;
    }
}

/**
 * @title Final Proxy
 * @notice The shared upgradeable proxy that every chain-global Final contract sits behind, including
 *         `Final Gateway`, `Final Chance Vault`, the `Final Recovery Authority` and the
 *         `Final Recovery Module`.
 * @dev **Deployment and initialization.** This proxy is installed as raw runtime and initialized by
 *      `proxyInitialize` in the same top-level transaction that creates it — the atomic
 *      deploy-and-call path of `Final Deployer`. The initializer enforces that itself by accepting
 *      only the pinned deployer address as caller, which removes the "first initializer wins" race
 *      that raw proxy runtime otherwise has, while preserving the deployer-address-plus-salt vanity
 *      model. An instance sitting uninitialized on chain therefore cannot be claimed by anyone: the
 *      only address that may initialize it is the deployer that created it.
 *
 *      **Addressing.** Each instance's address is derived from the deployer address and its salt
 *      alone, so this proxy's own compiled output does NOT participate in any address derivation.
 *      That is the difference between this proxy and the per-account proxies the factory deploys with
 *      `CREATE2`: changing this file's compiled runtime changes what future instances execute, not
 *      where they live, and never disturbs an already-deployed instance, which keeps its own runtime
 *      forever.
 *
 *      **Storage.** Every slot this proxy owns is unstructured — an ERC-1967 implementation slot, and
 *      namespaced `keccak256` slots for everything else. Not one is a declared variable, because the
 *      implementation shares this contract's storage and declares its own layout from slot 0. Adding a
 *      declared variable here would silently reinterpret implementation state.
 *
 *      **Selector namespacing.** Every management entrypoint is prefixed `proxy`, so the proxy's own
 *      surface cannot shadow an implementation selector. A collision would be invisible: the proxy
 *      answers first and the implementation function simply becomes unreachable.
 *
 *      **Two authority planes.** Upgrades are authorized either through `Final Access Manager` roles or,
 *      once sealed, through a single pinned authority contract. The seal exists because role-based
 *      authorization admits the access manager's owner on every role, so no arrangement of roles can
 *      keep the most privileged key out of the upgrade path. Sealing replaces the role check with a
 *      plain address equality, which nothing in the role plane can satisfy. It is one-way.
 *
 *      **What this proxy deliberately does not do.** It holds no admin address, applies no timelock,
 *      and validates nothing about what an implementation does — only that it carries code, and that
 *      this proxy is still alive after any initializer runs. Delay and quorum belong to the sealed
 *      authority contract, not here, so the upgrade path itself stays small enough to audit whole.
 */
contract FinalProxy is FinalProxyBase {
    /// @dev The one address permitted to initialize an instance: the deterministic deployer that
    ///      creates it and calls `proxyInitialize` in the same transaction. Identical on every chain,
    ///      which is what makes this a compile-time constant rather than a constructor argument — this
    ///      proxy is deployed as raw runtime and has no constructor to receive one.
    address private constant CANONICAL_FINAL_DEPLOYER = 0x000000000060910aE3DCbc5c65F41eD29EE6634e;
    /// @dev The standard ERC-1967 implementation slot, `keccak256("eip1967.proxy.implementation") - 1`.
    ///      Held at the standard location so explorers, wallets and upgrade tooling can read the current
    ///      implementation of any instance without knowing anything about this contract.
    bytes32 private constant IMPLEMENTATION_SLOT = 0x360894A13BA1A3210667C828492DB98DCA3E2076CC3735A920A3CA505D382BBC;
    /// @dev Unstructured slot for the access manager that authorizes THIS proxy's control plane. Distinct
    ///      from the pointer the implementation keeps for its own role checks; see `proxyAccessManagerPointers`.
    bytes32 private constant ACCESS_MANAGER_SLOT = keccak256("final.proxy.accessManager");
    /// @dev Unstructured slot for the role that authorizes both upgrades and control-plane mutations.
    bytes32 private constant PRIMARY_UPGRADE_ROLE_SLOT = keccak256("final.proxy.primaryUpgradeRole");
    /// @dev Unstructured slot for the optional second role. It authorizes implementation upgrades only,
    ///      never a change to governance, so delegating routine upgrades never delegates the ability to
    ///      rewrite who may upgrade. Zero disables the second role entirely.
    bytes32 private constant SECONDARY_UPGRADE_ROLE_SLOT = keccak256("final.proxy.secondaryUpgradeRole");
    /// @dev Unstructured slot for the one-shot initialization latch.
    bytes32 private constant INITIALIZED_SLOT = keccak256("final.proxy.initialized");
    /// @dev Unstructured slot for the sealed upgrade authority; zero while this proxy still answers to
    ///      the role plane. Read and written through raw `sload`/`sstore` rather than a declared variable
    ///      for the reason every slot here is unstructured: the implementation occupies the same storage
    ///      and declares its layout from slot 0.
    bytes32 private constant SEALED_UPGRADE_AUTHORITY_SLOT = keccak256("final.proxy.sealedUpgradeAuthority");

    /// @notice Thrown when anyone other than the pinned deterministic deployer calls `proxyInitialize`.
    /// @dev This is what makes raw-runtime deployment safe: an uninitialized instance is claimable only
    ///      by the address that created it, in the transaction that created it.
    error OnlyFinalDeployer();
    /// @notice Thrown when a caller holds neither upgrade role on the configured access manager.
    error OnlyUpgradeAuthority();
    /// @notice Thrown when a zero primary upgrade role is supplied.
    /// @dev A zero primary role would authorize nobody and could not be repaired, since repairing it is
    ///      itself gated on the primary role.
    error InvalidUpgradeRole();
    /// @notice Thrown when `proxyInitialize` is called on an already-initialized instance.
    error ProxyAlreadyInitialized();
    /// @notice Thrown when the zero address is offered as an implementation.
    error InvalidImplementation();
    /// @notice Thrown when the offered implementation address carries no code.
    /// @dev Delegating into a codeless address succeeds with empty returndata, so an unchecked upgrade to
    ///      one would leave every call to this proxy silently returning nothing instead of failing.
    error ImplementationHasNoCode();
    /// @notice Thrown when the zero address is offered as an access manager.
    error InvalidAccessManager();
    /// @notice Thrown when the offered access manager carries no code.
    /// @dev Every future authorization routes through the manager's `hasRole` / `hasAnyRole` views; a
    ///      codeless manager makes those calls revert and strands the control plane.
    error AccessManagerHasNoCode();
    /// @notice Thrown when this proxy no longer carries code after an implementation initializer ran.
    /// @dev Checked after every delegated initializer and upgrade call, so an implementation cannot
    ///      destroy the proxy inside the very transaction that installs it and report success.
    error ProxyDestroyed();
    /// @notice Thrown when the proposed access manager would not grant the primary upgrade role to the
    ///         authority the caller named, which would leave this proxy's control plane unreachable.
    /// @param newAccessManager The manager that was proposed and rejected.
    /// @param survivingAuthority The account the caller asserted would still hold the primary role.
    error AuthorityWouldNotSurvive(address newAccessManager, address survivingAuthority);
    /// @notice Thrown when a manager swap names the zero address as the surviving authority.
    /// @dev The assertion is mandatory: without a named survivor there is nothing to check the incoming
    ///      manager against, and the swap becomes the unverified lockout the check exists to prevent.
    error InvalidSurvivingAuthority();
    /// @notice Thrown when the proposed sealed authority carries no code.
    /// @dev An externally owned account as sealed authority would reinstate exactly what sealing removes
    ///      — a single key with instant, undelayed upgrade authority — and make it permanent, which is
    ///      strictly worse than the role plane it replaces.
    error SealedAuthorityHasNoCode();
    /// @notice Thrown when the zero address is offered as the sealed authority.
    /// @dev Sealing is one-way with no disarm, so a zero seal would freeze this proxy's implementation
    ///      irreversibly.
    error InvalidSealedAuthority();
    /// @notice Thrown when a sealed proxy is addressed by anyone other than its sealed authority.
    /// @dev Carries the caller because the role plane no longer explains the refusal: `hasRole` may well
    ///      be true for that caller, and on a sealed proxy that has stopped meaning anything.
    /// @param caller The rejected caller.
    error OnlySealedUpgradeAuthority(address caller);

    /// @notice Emitted whenever the implementation this proxy delegates to changes, including at initialization.
    /// @dev The ERC-1967 upgrade signal; indexers track the live implementation of an instance from this alone.
    /// @param implementation The implementation now installed.
    event ProxyUpgraded(address indexed implementation);
    /// @notice Emitted when the access manager gating this proxy's control plane is replaced.
    /// @dev Emitted at initialization with a zero `previousAccessManager`. It reports only this proxy's own
    ///      pointer; the implementation rotates its business-logic pointer under its own event.
    /// @param previousAccessManager The outgoing manager, zero at initialization.
    /// @param newAccessManager The manager now authorizing this proxy's control plane.
    event ProxyAccessManagerUpdated(address indexed previousAccessManager, address indexed newAccessManager);
    /// @notice Emitted when either upgrade role is reassigned, including at initialization.
    /// @dev Both roles are reported together because they are always written together, and a monitor needs
    ///      the pair to tell a narrowing of authority from a widening of it.
    /// @param previousPrimaryRole The outgoing primary role, zero at initialization.
    /// @param newPrimaryRole The role now required for upgrades and control-plane mutations.
    /// @param previousSecondaryRole The outgoing secondary role, zero at initialization.
    /// @param newSecondaryRole The optional additional role now accepted for implementation upgrades only.
    event ProxyUpgradeRolesUpdated(
        bytes32 indexed previousPrimaryRole,
        bytes32 indexed newPrimaryRole,
        bytes32 previousSecondaryRole,
        bytes32 newSecondaryRole
    );
    /// @notice Emitted when this proxy's upgrade and control planes are pinned to a single authority contract.
    /// @dev The one observable that distinguishes a role-gated instance from a sealed one. `previousAuthority`
    ///      is zero on the arming call and the outgoing authority on an incumbent-driven succession.
    /// @param previousAuthority The outgoing sealed authority, zero when the seal is first armed.
    /// @param newAuthority The contract that now holds this proxy's upgrade and control planes.
    event ProxyUpgradeAuthoritySealed(address indexed previousAuthority, address indexed newAuthority);

    /// @notice Initializes this proxy and, optionally, the implementation behind it, in one call.
    /// @dev Callable only by the pinned deterministic deployer, which creates this proxy and calls straight
    ///      into this function within the same top-level transaction. Restricting the caller — rather than
    ///      relying on being first — is what makes deploying raw proxy runtime safe: there is no interval
    ///      during which an unrelated account can initialize an instance and take its control plane.
    ///
    ///      `initData` is delegated to the implementation AFTER this proxy's own state is written, so an
    ///      implementation initializer observes the finished proxy configuration, and a proxy that survives
    ///      the call is checked to still carry code before the transaction is allowed to succeed.
    /// @param implementationAddress Initial implementation; must carry code.
    /// @param accessManagerAddress Access manager that will authorize upgrades and control-plane changes.
    /// @param primaryUpgradeRole Role required for upgrades and for every control-plane mutation. Never zero.
    /// @param secondaryUpgradeRole Optional additional role accepted for implementation upgrades only; zero
    ///        disables it and puts this proxy in single-role mode.
    /// @param initData Optional calldata delegated to the implementation once the proxy state is written.
    ///        Empty skips the delegated call entirely.
    function proxyInitialize(
        address implementationAddress,
        address accessManagerAddress,
        bytes32 primaryUpgradeRole,
        bytes32 secondaryUpgradeRole,
        bytes memory initData
    ) external payable virtual {
        if (msg.sender != CANONICAL_FINAL_DEPLOYER) revert OnlyFinalDeployer();
        _initializeProxy(
            implementationAddress,
            accessManagerAddress,
            primaryUpgradeRole,
            secondaryUpgradeRole,
            initData
        );
    }

    /// @notice Restricts a function to whoever may replace this proxy's implementation.
    /// @dev The wider of the two gates: on an unsealed proxy either upgrade role satisfies it, so routine
    ///      implementation rotation can be delegated without also delegating governance. On a sealed proxy
    ///      it collapses to the sealed authority, exactly like the control gate.
    modifier onlyProxyUpgradeAuthority() {
        _onlyProxyUpgradeAuthority();
        _;
    }

    /// @notice Restricts a function to whoever may rewrite this proxy's governance.
    /// @dev The narrower gate, guarding the manager pointer, the roles, and the seal itself. The secondary
    ///      upgrade role is refused here, which is what keeps "may ship a new implementation" from implying
    ///      "may decide who ships implementations".
    modifier onlyProxyControlAuthority() {
        _onlyProxyControlAuthority();
        _;
    }

    /// @notice Requires the caller to be authorized to replace the implementation.
    /// @dev Reads the seal first and returns early when it is armed, so a sealed proxy never consults the
    ///      access manager at all — the role plane is not merely outvoted, it is not asked.
    function _onlyProxyUpgradeAuthority() internal view {
        address sealed_ = sealedUpgradeAuthority();
        if (sealed_ != address(0)) {
            if (msg.sender != sealed_) revert OnlySealedUpgradeAuthority(msg.sender);
            return;
        }
        if (!_canManageProxy(msg.sender)) revert OnlyUpgradeAuthority();
    }

    /// @notice Requires the caller to be authorized to rewrite this proxy's governance.
    /// @dev Accepts only the primary upgrade role. The secondary role is an implementation-upgrade
    ///      capability and nothing more, so a holder of it can ship code but cannot change who is allowed
    ///      to ship code.
    ///
    ///      Once sealed, the control plane moves with the upgrade plane. Leaving `proxySetAccessManager`
    ///      or `proxySetUpgradeRoles` on the role plane would let anyone who bypasses roles rewrite the
    ///      governance the seal exists to remove them from, which would make the seal decorative.
    function _onlyProxyControlAuthority() internal view {
        address sealed_ = sealedUpgradeAuthority();
        if (sealed_ != address(0)) {
            if (msg.sender != sealed_) revert OnlySealedUpgradeAuthority(msg.sender);
            return;
        }
        address manager = _accessManager();
        bytes32 primaryUpgradeRole = StorageSlot.getBytes32Slot(PRIMARY_UPGRADE_ROLE_SLOT).value;
        if (!IFinalAccessManager(manager).hasRole(primaryUpgradeRole, msg.sender)) revert OnlyUpgradeAuthority();
    }

    /// @notice Returns whether this proxy has completed its one-time initialization.
    /// @dev False means no implementation, no manager and no roles are set, so every delegated call
    ///      reverts `ImplementationNotSet` and the instance is inert rather than dangerous.
    /// @return initialized True once `proxyInitialize` has run.
    function proxyInitialized() external view returns (bool initialized) {
        return _proxyInitialized();
    }

    /// @notice Returns the implementation this proxy currently delegates to.
    /// @dev Reads the ERC-1967 slot directly, so it answers even for a caller that knows nothing about
    ///      this contract's other storage.
    /// @return implementation Current implementation address.
    function proxyImplementation() external view returns (address implementation) {
        return _proxyImplementation();
    }

    /// @notice Returns the access manager that authorizes this proxy's own control plane.
    /// @dev Not necessarily the manager the implementation uses for its business-logic role checks; see
    ///      `proxyAccessManagerPointers` for both at once. Meaningless while the proxy is sealed, because
    ///      a sealed proxy stops consulting it.
    /// @return manager The access manager gating upgrades and control-plane mutations.
    function proxyAccessManager() external view returns (address manager) {
        return _accessManager();
    }

    /// @notice Returns the role required for upgrades and for every control-plane mutation.
    /// @return role Primary upgrade role identifier.
    function proxyPrimaryUpgradeRole() external view returns (bytes32 role) {
        return StorageSlot.getBytes32Slot(PRIMARY_UPGRADE_ROLE_SLOT).value;
    }

    /// @notice Returns the optional additional role accepted for implementation upgrades only.
    /// @dev Zero means single-role mode: only the primary role may upgrade. A non-zero value never grants
    ///      any control-plane authority.
    /// @return role Secondary upgrade role identifier, or zero.
    function proxySecondaryUpgradeRole() external view returns (bytes32 role) {
        return StorageSlot.getBytes32Slot(SECONDARY_UPGRADE_ROLE_SLOT).value;
    }

    /// @notice Replaces the access manager that authorizes this proxy's control plane, proving first that
    ///         the swap leaves a named account still able to use it.
    /// @dev The incoming manager must already carry code, because every later authorization — upgrades and
    ///      further manager swaps alike — routes through its `hasRole` / `hasAnyRole` views.
    ///
    ///      Code alone is not enough. Every control-plane call after this one authorizes against the NEW
    ///      manager, including this function itself, so installing a manager that grants
    ///      `primaryUpgradeRole` to nobody live permanently ends upgradeability of this proxy: the only way
    ///      back is gated by the manager that was just installed. That failure is silent and irreversible,
    ///      committed by a transaction that succeeds. `survivingAuthority` is the guard against it — the
    ///      account the caller asserts will still hold the primary role afterwards, checked against the
    ///      incoming manager before the pointer moves.
    ///
    ///      The survivor is an explicit parameter rather than an implicit `msg.sender` check because a
    ///      manager rotation is precisely the case where the caller may keep nothing: a deployment key
    ///      installs a manager under which authority lives with the plane's root authority. Checking
    ///      `msg.sender` would reject the one rotation this design exists to perform. Pass `msg.sender` to
    ///      retain control, or the incoming governance address to hand it over.
    /// @param newAccessManager The manager that will authorize this proxy's control plane. Must carry code.
    /// @param survivingAuthority Account that must hold `primaryUpgradeRole` under `newAccessManager`.
    ///        Never zero.
    function proxySetAccessManager(address newAccessManager, address survivingAuthority)
        external
        onlyProxyControlAuthority
    {
        _requireAccessManager(newAccessManager);
        if (survivingAuthority == address(0)) revert InvalidSurvivingAuthority();
        bytes32 primaryUpgradeRole = StorageSlot.getBytes32Slot(PRIMARY_UPGRADE_ROLE_SLOT).value;
        if (!IFinalAccessManager(newAccessManager).hasRole(primaryUpgradeRole, survivingAuthority)) {
            revert AuthorityWouldNotSurvive(newAccessManager, survivingAuthority);
        }
        address previousAccessManager = _accessManager();
        StorageSlot.getAddressSlot(ACCESS_MANAGER_SLOT).value = newAccessManager;
        emit ProxyAccessManagerUpdated(previousAccessManager, newAccessManager);
    }

    /// @notice Reports both access-manager pointers this proxy's storage carries, and whether they agree.
    /// @dev They are genuinely separate storage locations serving separate planes, and nothing keeps them
    ///      in step:
    ///
    ///      - `upgradeAuthority` is this proxy's own unstructured slot (`final.proxy.accessManager`),
    ///        consulted by the upgrade entrypoints and by `proxySetAccessManager`, which is also the only
    ///        thing that rotates it.
    ///      - `businessAuthority` is `FinalAccessController.accessManager`, a declared variable in the
    ///        IMPLEMENTATION's layout — so it lives in this proxy's storage, at the implementation's slot
    ///        — and is consulted by every role check in the business logic. Only the implementation's own
    ///        setter rotates it.
    ///
    ///      Rotating one and forgetting the other leaves upgrade authority and business authority
    ///      answering to different managers. Nothing on chain forbids that, and during a staged migration
    ///      it is briefly the intended state, so this view reports the divergence for monitoring and
    ///      post-rotation assertions rather than blocking it.
    ///
    ///      The implementation pointer is read by staticcalling this address, which lands on the fallback
    ///      and delegates, so the proxy learns the value without knowing which slot holds it. Not every
    ///      implementation is required to publish one; a failed probe yields a zero `businessAuthority`,
    ///      which reads as divergence whenever an upgrade authority is set.
    /// @return upgradeAuthority Manager gating this proxy's control plane.
    /// @return businessAuthority Manager gating the implementation's role checks, or zero when the
    ///         implementation publishes no such pointer.
    /// @return diverged True when the two pointers disagree.
    function proxyAccessManagerPointers()
        external
        view
        returns (address upgradeAuthority, address businessAuthority, bool diverged)
    {
        upgradeAuthority = _accessManager();
        // Read the implementation's pointer through this proxy itself rather than by guessing its slot,
        // so the answer stays correct across implementation layout changes. A failure is treated as
        // "publishes no pointer", never propagated: this view must stay callable on every instance.
        try IProxyAccessManagerProbe(address(this)).accessManager() returns (address implManager) {
            businessAuthority = implManager;
        } catch {
            businessAuthority = address(0);
        }
        diverged = businessAuthority != upgradeAuthority;
    }

    /// @notice Reassigns the roles that authorize upgrades and control-plane mutations on this proxy.
    /// @dev The primary role is required to be non-zero, because a zero primary role would authorize nobody
    ///      and could not be repaired — repairing it is itself gated on the primary role. The secondary role
    ///      has no such floor: zero is the meaningful value that turns off the second role and puts this
    ///      proxy in single-role mode.
    ///
    ///      This changes who may upgrade, not what is installed. It does not verify that anyone actually
    ///      holds the incoming primary role under the configured manager, so pairing a role change with a
    ///      manager change is done through `proxySetAccessManager`, which does check.
    /// @param newPrimaryUpgradeRole Role required for upgrades and control-plane mutations. Never zero.
    /// @param newSecondaryUpgradeRole Additional role accepted for implementation upgrades only; zero
    ///        disables it.
    function proxySetUpgradeRoles(
        bytes32 newPrimaryUpgradeRole,
        bytes32 newSecondaryUpgradeRole
    ) external onlyProxyControlAuthority {
        if (newPrimaryUpgradeRole == bytes32(0)) revert InvalidUpgradeRole();
        bytes32 previousPrimaryRole = StorageSlot.getBytes32Slot(PRIMARY_UPGRADE_ROLE_SLOT).value;
        bytes32 previousSecondaryRole = StorageSlot.getBytes32Slot(SECONDARY_UPGRADE_ROLE_SLOT).value;
        StorageSlot.getBytes32Slot(PRIMARY_UPGRADE_ROLE_SLOT).value = newPrimaryUpgradeRole;
        StorageSlot.getBytes32Slot(SECONDARY_UPGRADE_ROLE_SLOT).value = newSecondaryUpgradeRole;
        emit ProxyUpgradeRolesUpdated(previousPrimaryRole, newPrimaryUpgradeRole, previousSecondaryRole, newSecondaryUpgradeRole);
    }

    /// @notice Returns the sealed upgrade authority, or zero while this proxy still answers to the role plane.
    /// @dev The single observable that tells a reader which authority model an instance is under. A non-zero
    ///      answer means role membership no longer grants anything here, however privileged.
    /// @return authority The contract holding this proxy's upgrade and control planes, or zero if unsealed.
    function sealedUpgradeAuthority() public view returns (address authority) {
        bytes32 slot = SEALED_UPGRADE_AUTHORITY_SLOT;
        // Plain load of the namespaced slot. Memory-safe because it reads storage and touches no memory.
        assembly ("memory-safe") {
            authority := sload(slot)
        }
    }

    /// @notice Carves this proxy's upgrade and control planes out of the role plane and pins them to
    ///         `newAuthority`, permanently.
    ///
    /// @dev **Why the role plane is not enough.** Role checks resolve through the access manager, and
    ///      `FinalAccessManager.hasRole` answers true for its owner on every known role. Whoever holds
    ///      access-manager ownership can therefore upgrade any chain-global proxy instantly, with no delay
    ///      and no scheduling event to observe — which behind the recovery authority means installing an
    ///      implementation that attests anything. No arrangement of roles repairs this: the owner
    ///      short-circuit lives inside `hasRole` itself, so every role-based split still admits the most
    ///      privileged key.
    ///
    ///      Once sealed, both planes accept only a caller equal to `sealedUpgradeAuthority()` — a plain
    ///      address equality that no role and no access-manager owner can satisfy. Resistance to the
    ///      privileged key is structural here rather than a matter of policy. Bootstrapping still happens
    ///      on the role plane, so an instance can be deployed and configured before any second factor
    ///      exists, and is then armed.
    ///
    ///      **Sealing is one-way and there is no disarm.** Succession is incumbent-only: once armed, the
    ///      pointer moves only when the current authority itself calls this function, so no role-plane path
    ///      back exists. That is the property being bought, and it is also the hazard — seal to something
    ///      that cannot act and this proxy's implementation is frozen for good.
    ///
    ///      Two rails guard against that, and both are enforced because neither suffices alone: zero is
    ///      refused, and the target must carry code. The code requirement is not merely a brick-guard. An
    ///      externally owned account as the sealed authority would reinstate exactly what sealing removes
    ///      — one key with instant, undelayed upgrade authority — and make it permanent, which is worse
    ///      than the role plane it replaced. The intended target is the plane's root authority
    ///      (`FinalRootAuthority`), a contract whose every act is a signed quorum round.
    ///
    ///      **Reach.** A deployed proxy carries its own runtime for life, so an instance whose runtime does
    ///      not contain this function can never be sealed. Such instances are secured instead by holding
    ///      access-manager ownership in a contract rather than a key.
    /// @param newAuthority Contract that will hold this proxy's upgrade and control planes. Must carry code
    ///        and must be able to act, since nothing can undo the seal.
    function proxySealUpgradeAuthority(address newAuthority) external onlyProxyControlAuthority {
        if (newAuthority == address(0)) revert InvalidSealedAuthority();
        if (newAuthority.code.length == 0) revert SealedAuthorityHasNoCode();
        // Incumbent-only succession needs no check here: once the slot is non-zero the control-plane gate
        // already requires the caller to BE the sealed authority, so an armed proxy cannot be re-pointed
        // from the role plane.
        address previous = sealedUpgradeAuthority();
        bytes32 slot = SEALED_UPGRADE_AUTHORITY_SLOT;
        // Plain store to the namespaced slot. Memory-safe because it writes storage and touches no memory.
        assembly ("memory-safe") {
            sstore(slot, newAuthority)
        }
        emit ProxyUpgradeAuthoritySealed(previous, newAuthority);
    }

    /// @notice Points this proxy at a new implementation without running any initializer.
    /// @dev The plain upgrade path, for an implementation that needs no migration step. The new address is
    ///      required to carry code; nothing about its storage layout is or can be checked here, so layout
    ///      compatibility with the state already in this proxy is the upgrader's responsibility.
    /// @param newImplementation Implementation to install. Must carry code.
    function proxyUpgradeTo(address newImplementation) external onlyProxyUpgradeAuthority {
        _upgradeTo(newImplementation);
    }

    /// @notice Points this proxy at a new implementation and delegates one initialization call into it.
    /// @dev The implementation slot is written BEFORE the delegated call, so a migration routine observes
    ///      the post-upgrade state and any reentrant call during it resolves to the new implementation
    ///      rather than the old one. The two steps are one transaction, so no window exists in which the
    ///      new code is installed but unmigrated.
    ///
    ///      A liveness check follows the call: an implementation that destroys this proxy inside its own
    ///      initializer must not be able to report success, on any chain where that destruction still takes
    ///      effect for a contract created in the same transaction.
    /// @param newImplementation Implementation to install. Must carry code.
    /// @param data Calldata delegated to the new implementation once it is installed.
    /// @return Raw returndata from the delegated call, undecoded.
    function proxyUpgradeToAndCall(address newImplementation, bytes calldata data)
        external
        payable
        onlyProxyUpgradeAuthority
        returns (bytes memory)
    {
        _upgradeTo(newImplementation);
        bytes memory returndata = _delegateCall(newImplementation, data);
        _requireLiveProxy();
        return returndata;
    }

    /// @notice Performs the one-time proxy initialization and the optional implementation bootstrap.
    /// @dev Ordering carries the safety here. Every argument is validated first, then the whole proxy state
    ///      — latch, implementation, manager, roles — is written, and only then is `initData` delegated. The
    ///      latch going down before the delegated call is what stops an implementation initializer from
    ///      re-entering and claiming the proxy a second time under different terms.
    ///
    ///      The three configuration events are emitted before the delegated call as well, so the log order
    ///      of a bootstrapped instance reads proxy-first: an indexer sees the proxy configured, then
    ///      whatever the implementation itself emits.
    /// @param implementationAddress Initial implementation; must carry code.
    /// @param accessManagerAddress Access manager for the control plane; must carry code.
    /// @param primaryUpgradeRole Role required for upgrades and control-plane mutations. Never zero.
    /// @param secondaryUpgradeRole Additional role accepted for implementation upgrades only; zero disables it.
    /// @param initData Optional calldata delegated to the implementation after the proxy state is written.
    function _initializeProxy(
        address implementationAddress,
        address accessManagerAddress,
        bytes32 primaryUpgradeRole,
        bytes32 secondaryUpgradeRole,
        bytes memory initData
    ) internal {
        if (_proxyInitialized()) revert ProxyAlreadyInitialized();
        _requireImplementation(implementationAddress);
        _requireAccessManager(accessManagerAddress);
        if (primaryUpgradeRole == bytes32(0)) revert InvalidUpgradeRole();

        StorageSlot.getBooleanSlot(INITIALIZED_SLOT).value = true;
        StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = implementationAddress;
        StorageSlot.getAddressSlot(ACCESS_MANAGER_SLOT).value = accessManagerAddress;
        StorageSlot.getBytes32Slot(PRIMARY_UPGRADE_ROLE_SLOT).value = primaryUpgradeRole;
        StorageSlot.getBytes32Slot(SECONDARY_UPGRADE_ROLE_SLOT).value = secondaryUpgradeRole;

        emit ProxyUpgraded(implementationAddress);
        emit ProxyAccessManagerUpdated(address(0), accessManagerAddress);
        emit ProxyUpgradeRolesUpdated(bytes32(0), primaryUpgradeRole, bytes32(0), secondaryUpgradeRole);

        if (initData.length > 0) {
            _delegateCall(implementationAddress, initData);
            _requireLiveProxy();
        }
    }

    /// @notice Checks whether an account holds either upgrade role on the configured access manager.
    /// @dev Only reached on an unsealed proxy; the seal is tested by the callers before this is consulted.
    ///      A zero secondary role selects the single-role query rather than passing zero into `hasAnyRole`,
    ///      so an unset second role can never be satisfied by an account that happens to hold "no role".
    /// @param account Account to check against the access manager.
    /// @return True when the account may replace this proxy's implementation.
    function _canManageProxy(address account) private view returns (bool) {
        address manager = _accessManager();
        bytes32 primaryUpgradeRole = StorageSlot.getBytes32Slot(PRIMARY_UPGRADE_ROLE_SLOT).value;
        bytes32 secondaryUpgradeRole = StorageSlot.getBytes32Slot(SECONDARY_UPGRADE_ROLE_SLOT).value;

        if (secondaryUpgradeRole == bytes32(0)) {
            return IFinalAccessManager(manager).hasRole(primaryUpgradeRole, account);
        }

        return IFinalAccessManager(manager).hasAnyRole(primaryUpgradeRole, secondaryUpgradeRole, account);
    }

    /// @notice Reads the one-shot initialization latch.
    /// @return initialized True once `proxyInitialize` has run on this instance.
    function _proxyInitialized() internal view returns (bool initialized) {
        return StorageSlot.getBooleanSlot(INITIALIZED_SLOT).value;
    }

    /// @notice Validates and writes the implementation slot, then announces the change.
    /// @dev The single write path for the implementation pointer, so validation cannot be skipped by any
    ///      caller and every change is accompanied by its event. Only addresses carrying code are accepted,
    ///      which keeps the proxy from being pointed at an externally owned or empty address whose calls
    ///      would silently succeed with no returndata.
    /// @param newImplementation Implementation to install. Must carry code.
    function _upgradeTo(address newImplementation) private {
        _requireImplementation(newImplementation);
        StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value = newImplementation;
        emit ProxyUpgraded(newImplementation);
    }

    /// @notice Resolves the delegation target from the ERC-1967 implementation slot.
    /// @dev Zero before initialization, which the base turns into `ImplementationNotSet` so an
    ///      unconfigured instance is inert rather than delegating into nothing.
    /// @return The implementation address currently installed.
    function _proxyImplementation() internal view override returns (address) {
        return StorageSlot.getAddressSlot(IMPLEMENTATION_SLOT).value;
    }

    /// @notice Reads the access manager gating this proxy's control plane.
    /// @return The configured access manager, distinct from any pointer the implementation keeps.
    function _accessManager() private view returns (address) {
        return StorageSlot.getAddressSlot(ACCESS_MANAGER_SLOT).value;
    }

    /// @notice Requires an implementation address that is non-zero and carries code.
    /// @dev Both halves matter and fail differently, so they are separate errors: zero is a caller mistake,
    ///      while a codeless non-zero address is usually a wrong-chain or not-yet-deployed target.
    /// @param implementationAddress Candidate implementation.
    function _requireImplementation(address implementationAddress) private view {
        if (implementationAddress == address(0)) revert InvalidImplementation();
        if (implementationAddress.code.length == 0) revert ImplementationHasNoCode();
    }

    /// @notice Requires an access manager address that is non-zero and carries code.
    /// @dev Enforced on both the initial manager and every replacement, because all later authorization
    ///      calls into it and a manager without code would make the control plane unreachable.
    /// @param accessManagerAddress Candidate access manager.
    function _requireAccessManager(address accessManagerAddress) private view {
        if (accessManagerAddress == address(0)) revert InvalidAccessManager();
        if (accessManagerAddress.code.length == 0) revert AccessManagerHasNoCode();
    }

    /// @notice Requires this proxy to still carry code after a delegated initializer or upgrade call.
    /// @dev Run after every delegated call this contract makes on its own behalf. Without it, an
    ///      implementation could destroy the proxy from inside its initializer and leave a transaction that
    ///      reports success behind an address that answers nothing.
    function _requireLiveProxy() private view {
        if (address(this).code.length == 0) revert ProxyDestroyed();
    }
}

contracts/utils/FinalSweep.sol

// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
// Licensed under the Business Source License 1.1 (the "License")
//
// Change Date: 2029-01-01
// Change License: GPL-2.0-or-later
//
// Additional Use Grant:
// 1. Any person or entity may inherit this sweep surface into contracts that
//    integrate with the Final DeFi Protocol, in order to recover assets sent to
//    them by mistake.
// 2. Protocol operators and integrators may call the sweep entrypoints it
//    declares, subject to each inheriting contract's own authority and reserved
//    balance rules, as part of their integration with the Final DeFi Protocol.
// 3. For the avoidance of doubt, this Grant does NOT permit the commercial
//    deployment of a Fork of this sweep surface or a competing asset-recovery
//    plane derived from it without permission prior to the Change Date.
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;

/// @notice The asset kinds a sweep can move. `Native` ignores `asset` and
/// `id`; `Erc20` ignores `id`; `Erc721` reads `id` as the token id and moves
/// exactly one; `Erc1155` reads both.
enum SweepKind { Native, Erc20, Erc721, Erc1155 }

/**
 * @title Final Sweep
 * @notice One sweep surface, on every contract of ours that can end up holding
 *         an asset it does not owe to anybody.
 *
 * @dev Assets arrive at protocol contracts that were never meant to hold them:
 * a bridge delivers to the wrong leg, a user sends an ERC-20 to a registry, an
 * airdrop lands on the gateway, an NFT is safe-transferred into the vault. Left
 * alone that value is destroyed. The sweep is how it comes back — and the
 * single rule it must never break is that a sweep moves SURPLUS and nothing
 * else.
 *
 * Three seams make that rule per-contract:
 *
 *  - `_requireSweepAuthority()` — the treasury role, expressed in whatever
 *    access plane the host contract already has (`FinalAccessController` roles,
 *    a cross-chain authority, a quorum). No new authority is introduced.
 *  - `_sweepDestinations()` — where a sweep may pay. Ours is a two-address
 *    answer because a contract normally has exactly two legitimate ones (the
 *    gateway and the treasury); a contract with one returns it twice.
 *    `FinalGateway` overrides `_requireSweepDestination` outright: the gateway
 *    is the drain of the whole system and sweeps ONWARD to anywhere.
 *  - `_sweepReserved(kind, asset, id)` — the part of the raw balance that is
 *    NOT surplus: fee deposits, the pending-settlement bucket, searcher
 *    collateral, settlement custody, vaulted entries, locked PHI. The default
 *    is zero, which is correct for a contract that custodies nothing; every
 *    contract that custodies something overrides it and is the one place the
 *    liability is stated.
 *
 * The surplus is measured LIVE against the raw balance at call time, so a
 * re-entrant destination re-measures against a balance that already fell —
 * there is no cached figure to double-spend. Nothing here writes storage, so
 * there is no state for a callback to observe half-updated either.
 *
 * The three ERC-721/ERC-1155 receiver hooks are part of the same surface and
 * for the same reason: `safeTransferFrom` reverts into a contract that does not
 * answer them, so without these an NFT sent to one of ours does not land at
 * all — which is not safety, it is a different way to lose it.
 */
abstract contract FinalSweep {
    /// @notice `msg.sender` does not hold this contract's sweep authority.
    error SweepUnauthorized(address caller);
    /// @notice `to` is neither of this contract's sweep destinations.
    error SweepDestinationNotAllowed(address to);
    /// @notice The requested amount is above the surplus: the difference is
    /// owed to somebody (a deposit, a custody total, a vaulted entry).
    error SweepAboveSurplus(address asset, uint256 requested, uint256 surplus);
    /// @notice A sweep of nothing.
    error SweepZeroAmount();
    /// @notice The transfer leg failed, or the token returned `false`.
    error SweepTransferFailed(address asset);

    /// @notice `amount` of `asset` (`id` for the non-fungible kinds) left this
    /// contract for `to` under the sweep authority.
    event AssetSwept(SweepKind indexed kind, address indexed asset, address indexed to, uint256 id, uint256 amount);

    // ─────────────────────────────── seams ───────────────────────────────

    /// @dev Reverts unless `msg.sender` may sweep. The host contract's own
    /// treasury role — never a new one.
    function _requireSweepAuthority() internal view virtual;

    /// @dev The (at most two) addresses a sweep may pay. A contract with one
    /// legitimate destination returns it twice.
    function _sweepDestinations() internal view virtual returns (address a, address b);

    /// @dev The part of the raw balance that is owed and therefore never
    /// sweepable. Zero for a contract that custodies nothing.
    function _sweepReserved(SweepKind, address, uint256) internal view virtual returns (uint256) {
        return 0;
    }

    /// @dev Destination policy. Overridden by `FinalGateway`, which may sweep
    /// onward to anywhere.
    function _requireSweepDestination(address to) internal view virtual {
        (address a, address b) = _sweepDestinations();
        if (to == address(0) || (to != a && to != b)) revert SweepDestinationNotAllowed(to);
    }

    // ────────────────────────────── surface ──────────────────────────────

    /// @notice The surplus of `asset` (`id` for the non-fungible kinds) — the
    /// raw balance above everything this contract owes. What a sweep may move,
    /// readable before calling one.
    function sweepableSurplus(SweepKind kind, address asset, uint256 id) public view returns (uint256 surplus) {
        uint256 raw = _rawBalance(kind, asset, id);
        uint256 reserved = _sweepReserved(kind, asset, id);
        return raw > reserved ? raw - reserved : 0;
    }

    /// @notice Move `amount` of an asset this contract does not owe to `to`.
    /// @dev Role-gated, destination-gated and bounded by the live surplus. The
    /// three gates are independent: a treasury key cannot pay a destination
    /// the contract does not recognize, and neither key nor destination can
    /// reach a wei that backs a liability.
    /// @param kind Which asset kind is being moved.
    /// @param asset Token contract; ignored for `Native`.
    /// @param id Token id for `Erc721` / `Erc1155`; ignored otherwise.
    /// @param amount Amount to move. `type(uint256).max` means the whole
    ///   surplus, which is what an operator draining a stray balance wants and
    ///   what avoids a race with an inflow landing between the read and the call.
    /// @param to Destination.
    /// @return moved Amount actually moved.
    function sweepAsset(SweepKind kind, address asset, uint256 id, uint256 amount, address to)
        external
        returns (uint256 moved)
    {
        _requireSweepAuthority();
        _requireSweepDestination(to);

        uint256 surplus = sweepableSurplus(kind, asset, id);
        moved = amount == type(uint256).max ? surplus : amount;
        if (moved == 0) revert SweepZeroAmount();
        if (moved > surplus) revert SweepAboveSurplus(asset, moved, surplus);

        if (kind == SweepKind.Native) {
            (bool ok,) = payable(to).call{value: moved}("");
            if (!ok) revert SweepTransferFailed(address(0));
        } else if (kind == SweepKind.Erc20) {
            _callToken(asset, abi.encodeWithSelector(0xa9059cbb, to, moved)); // transfer(address,uint256)
        } else if (kind == SweepKind.Erc721) {
            // `transferFrom`, not `safeTransferFrom`: a rescue must not fail
            // because the treasury destination declines a hook. Which
            // destination is legitimate is already decided above.
            moved = 1;
            _callToken(asset, abi.encodeWithSelector(0x23b872dd, address(this), to, id)); // transferFrom
        } else {
            _callToken(
                asset,
                abi.encodeWithSelector(0xf242432a, address(this), to, id, moved, "") // safeTransferFrom(...)
            );
        }
        emit AssetSwept(kind, asset, to, id, moved);
    }

    // ───────────────────────────── receivers ─────────────────────────────

    /// @notice Accept safe ERC-721 transfers, so one sent here is recoverable
    /// rather than rejected at the door.
    function onERC721Received(address, address, uint256, bytes calldata) external pure virtual returns (bytes4) {
        return 0x150b7a02;
    }

    /// @notice Accept safe ERC-1155 single transfers.
    function onERC1155Received(address, address, uint256, uint256, bytes calldata)
        external
        pure
        virtual
        returns (bytes4)
    {
        return 0xf23a6e61;
    }

    /// @notice Accept safe ERC-1155 batch transfers.
    function onERC1155BatchReceived(address, address, uint256[] calldata, uint256[] calldata, bytes calldata)
        external
        pure
        virtual
        returns (bytes4)
    {
        return 0xbc197c81;
    }

    // ───────────────────────────── internals ─────────────────────────────

    /// @dev The raw held amount, before anything owed is subtracted.
    function _rawBalance(SweepKind kind, address asset, uint256 id) internal view returns (uint256) {
        if (kind == SweepKind.Native) return address(this).balance;
        if (kind == SweepKind.Erc20) {
            (bool ok, bytes memory ret) = asset.staticcall(abi.encodeWithSelector(0x70a08231, address(this)));
            return (ok && ret.length >= 32) ? abi.decode(ret, (uint256)) : 0;
        }
        if (kind == SweepKind.Erc721) {
            (bool ok, bytes memory ret) = asset.staticcall(abi.encodeWithSelector(0x6352211e, id)); // ownerOf
            return (ok && ret.length >= 32 && abi.decode(ret, (address)) == address(this)) ? 1 : 0;
        }
        (bool ok1155, bytes memory ret1155) =
            asset.staticcall(abi.encodeWithSelector(0x00fdd58e, address(this), id)); // balanceOf(address,uint256)
        return (ok1155 && ret1155.length >= 32) ? abi.decode(ret1155, (uint256)) : 0;
    }

    /// @dev One transfer leg, tolerant of the legacy no-return ERC-20 shape the
    /// way `FinalDeployer`'s rescue helpers are: success is "the call did not
    /// revert AND it did not return `false`".
    function _callToken(address token, bytes memory data) private {
        if (token.code.length == 0) revert SweepTransferFailed(token);
        (bool ok, bytes memory ret) = token.call(data);
        if (!ok || (ret.length != 0 && !abi.decode(ret, (bool)))) revert SweepTransferFailed(token);
    }
}

node_modules/@openzeppelin/contracts/utils/StorageSlot.sol

// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.1.0) (utils/StorageSlot.sol)
// This file was procedurally generated from scripts/generate/templates/StorageSlot.js.

pragma solidity ^0.8.20;

/**
 * @dev Library for reading and writing primitive types to specific storage slots.
 *
 * Storage slots are often used to avoid storage conflict when dealing with upgradeable contracts.
 * This library helps with reading and writing to such slots without the need for inline assembly.
 *
 * The functions in this library return Slot structs that contain a `value` member that can be used to read or write.
 *
 * Example usage to set ERC-1967 implementation slot:
 * ```solidity
 * contract ERC1967 {
 *     // Define the slot. Alternatively, use the SlotDerivation library to derive the slot.
 *     bytes32 internal constant _IMPLEMENTATION_SLOT = 0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;
 *
 *     function _getImplementation() internal view returns (address) {
 *         return StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value;
 *     }
 *
 *     function _setImplementation(address newImplementation) internal {
 *         require(newImplementation.code.length > 0);
 *         StorageSlot.getAddressSlot(_IMPLEMENTATION_SLOT).value = newImplementation;
 *     }
 * }
 * ```
 *
 * TIP: Consider using this library along with {SlotDerivation}.
 */
library StorageSlot {
    struct AddressSlot {
        address value;
    }

    struct BooleanSlot {
        bool value;
    }

    struct Bytes32Slot {
        bytes32 value;
    }

    struct Uint256Slot {
        uint256 value;
    }

    struct Int256Slot {
        int256 value;
    }

    struct StringSlot {
        string value;
    }

    struct BytesSlot {
        bytes value;
    }

    /**
     * @dev Returns an `AddressSlot` with member `value` located at `slot`.
     */
    function getAddressSlot(bytes32 slot) internal pure returns (AddressSlot storage r) {
        assembly ("memory-safe") {
            r.slot := slot
        }
    }

    /**
     * @dev Returns a `BooleanSlot` with member `value` located at `slot`.
     */
    function getBooleanSlot(bytes32 slot) internal pure returns (BooleanSlot storage r) {
        assembly ("memory-safe") {
            r.slot := slot
        }
    }

    /**
     * @dev Returns a `Bytes32Slot` with member `value` located at `slot`.
     */
    function getBytes32Slot(bytes32 slot) internal pure returns (Bytes32Slot storage r) {
        assembly ("memory-safe") {
            r.slot := slot
        }
    }

    /**
     * @dev Returns a `Uint256Slot` with member `value` located at `slot`.
     */
    function getUint256Slot(bytes32 slot) internal pure returns (Uint256Slot storage r) {
        assembly ("memory-safe") {
            r.slot := slot
        }
    }

    /**
     * @dev Returns a `Int256Slot` with member `value` located at `slot`.
     */
    function getInt256Slot(bytes32 slot) internal pure returns (Int256Slot storage r) {
        assembly ("memory-safe") {
            r.slot := slot
        }
    }

    /**
     * @dev Returns a `StringSlot` with member `value` located at `slot`.
     */
    function getStringSlot(bytes32 slot) internal pure returns (StringSlot storage r) {
        assembly ("memory-safe") {
            r.slot := slot
        }
    }

    /**
     * @dev Returns an `StringSlot` representation of the string storage pointer `store`.
     */
    function getStringSlot(string storage store) internal pure returns (StringSlot storage r) {
        assembly ("memory-safe") {
            r.slot := store.slot
        }
    }

    /**
     * @dev Returns a `BytesSlot` with member `value` located at `slot`.
     */
    function getBytesSlot(bytes32 slot) internal pure returns (BytesSlot storage r) {
        assembly ("memory-safe") {
            r.slot := slot
        }
    }

    /**
     * @dev Returns an `BytesSlot` representation of the bytes storage pointer `store`.
     */
    function getBytesSlot(bytes storage store) internal pure returns (BytesSlot storage r) {
        assembly ("memory-safe") {
            r.slot := store.slot
        }
    }
}

abi

[
  {
    "type": "constructor",
    "inputs": [
      {
        "name": "registry_",
        "type": "address",
        "internalType": "contract FinalIdentityRegistry"
      },
      {
        "name": "implementation_",
        "type": "address",
        "internalType": "address"
      },
      {
        "name": "initData",
        "type": "bytes",
        "internalType": "bytes"
      }
    ],
    "stateMutability": "nonpayable"
  },
  {
    "type": "fallback",
    "stateMutability": "payable"
  },
  {
    "type": "receive",
    "stateMutability": "payable"
  },
  {
    "type": "function",
    "name": "DOMAIN_PROXY_UPGRADE",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "bytes32",
        "internalType": "bytes32"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "implementation",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "address",
        "internalType": "address"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "registry",
    "inputs": [],
    "outputs": [
      {
        "name": "",
        "type": "address",
        "internalType": "contract FinalIdentityRegistry"
      }
    ],
    "stateMutability": "view"
  },
  {
    "type": "function",
    "name": "upgradeTo",
    "inputs": [
      {
        "name": "newImplementation",
        "type": "address",
        "internalType": "address"
      },
      {
        "name": "initData",
        "type": "bytes",
        "internalType": "bytes"
      },
      {
        "name": "anchorBlock",
        "type": "uint64",
        "internalType": "uint64"
      },
      {
        "name": "approvals",
        "type": "tuple[]",
        "internalType": "struct FinalPqQuorum.Approval[]",
        "components": [
          {
            "name": "signer",
            "type": "address",
            "internalType": "address"
          },
          {
            "name": "algorithm",
            "type": "uint8",
            "internalType": "uint8"
          },
          {
            "name": "signature",
            "type": "bytes",
            "internalType": "bytes"
          },
          {
            "name": "seal",
            "type": "bytes",
            "internalType": "bytes"
          }
        ]
      }
    ],
    "outputs": [],
    "stateMutability": "nonpayable"
  },
  {
    "type": "event",
    "name": "Upgraded",
    "inputs": [
      {
        "name": "previous",
        "type": "address",
        "indexed": true,
        "internalType": "address"
      },
      {
        "name": "current",
        "type": "address",
        "indexed": true,
        "internalType": "address"
      }
    ],
    "anonymous": false
  },
  {
    "type": "error",
    "name": "ImplementationHasNoCode",
    "inputs": [
      {
        "name": "implementation",
        "type": "address",
        "internalType": "address"
      }
    ]
  },
  {
    "type": "error",
    "name": "ImplementationNotSet",
    "inputs": []
  },
  {
    "type": "error",
    "name": "ImplementationUnchanged",
    "inputs": [
      {
        "name": "implementation",
        "type": "address",
        "internalType": "address"
      }
    ]
  },
  {
    "type": "error",
    "name": "InvalidProxyArg",
    "inputs": []
  }
]

read contract

bytecode · 1,908 bytes

0x60806040526004361015610018575b3661070957610709565b5f3560e01c80635c60da1b1461005757806378d4103b146100525780637b1039991461004d5763f02a474f0361000e5761016e565b6100d1565b610097565b34610089575f366003190112610089575f5160206107545f395f51905f52546001600160a01b03166080908152602090f35b5f80fd5b5f91031261008957565b34610089575f3660031901126100895760206040517f7f6cbd803755b2eff336a8194e29019d89d975cb558dd54f8582a673b8fc87728152f35b34610089575f366003190112610089576040517f000000000000000000000000c19d888a2f7ba65a8dcf8d03ad8f1af5cdc664306001600160a01b03168152602090f35b6001600160a01b0381160361008957565b6044359067ffffffffffffffff8216820361008957565b9181601f840112156100895782359167ffffffffffffffff8311610089576020808501948460051b01011161008957565b346100895760803660031901126100895760043561018b81610115565b60243567ffffffffffffffff811161008957366023820112156100895780600401359067ffffffffffffffff8211610089573660248383010111610089576101d1610126565b6064359267ffffffffffffffff841161008957610202946101f8602495369060040161013d565b9590940190610432565b005b634e487b7160e01b5f52604160045260245ffd5b90601f8019910116810190811067ffffffffffffffff82111761023a57604052565b610204565b90816020910312610089575180151581036100895790565b6040513d5f823e3d90fd5b90816020910312610089575161027781610115565b90565b67ffffffffffffffff811161023a57601f01601f191660200190565b9291926102a28261027a565b916102b06040519384610218565b829481845281830111610089578281602093845f960137010152565b9035601e198236030181121561008957016020813591019167ffffffffffffffff821161008957813603831361008957565b908060209392818452848401375f828201840152601f01601f1916010190565b93919067ffffffffffffffff839260808701927f7f6cbd803755b2eff336a8194e29019d89d975cb558dd54f8582a673b8fc877288526020880152166040860152608060608601525260a083019060a08160051b85010193835f91607e1982360301905b848410610393575050505050505090565b90919293949596609f1982820301875287358381121561008957840180356103ba81610115565b6001600160a01b03168252602081013560ff811692908390036100895761042360209282600195858095015261041561040a6103f960408501856102cc565b6080604086015260808501916102fe565b9260608101906102cc565b9160608185039101526102fe565b99019701959401929190610382565b6040516328305db160e21b8152919590949293909290917f000000000000000000000000c19d888a2f7ba65a8dcf8d03ad8f1af5cdc664306001600160a01b031690602081600481855afa908115610621575f9161068c575b508015610626575b610587575b5050506001600160a01b0383169050801561057857823b1561055c575f5160206107545f395f51905f52546001600160a01b0316818114610540575f5160206107545f395f51905f5280546001600160a01b0319166001600160a01b0386161790557f5d611f318680d00598bb735d61bacf0c514c6b50e1e5ad30040a4df2b12791c75f80a38061052857505050565b61053d92610537913691610296565b906106bb565b50565b6301c1229760e11b5f526001600160a01b03841660045260245ffd5b639e172b1d60e01b5f526001600160a01b03831660045260245ffd5b633726979f60e21b5f5260045ffd5b610592368689610296565b8051602091820120604080516001600160a01b038a169381019384528082019290925281526105c2606082610218565b51902092813b15610089575f80946105f0604051978896879586946322f3f44760e11b86526004860161031e565b03925af1801561062157610607575b808080610498565b806106155f61061b93610218565b8061008d565b5f6105ff565b610257565b5060405163f5778b0360e01b8152602081600481855afa908115610621575f9161065d575b506001600160a01b0316331415610493565b61067f915060203d602011610685575b6106778183610218565b810190610262565b5f61064b565b503d61066d565b6106ae915060203d6020116106b4575b6106a68183610218565b81019061023f565b5f61048b565b503d61069c565b5f918291602082519201905af43d15610701573d906106d98261027a565b916106e76040519384610218565b82523d5f602084013e5b156106f95790565b602081519101fd5b6060906106f1565b5f5160206107545f395f51905f52546001600160a01b03168015610744575f8091368280378136915af43d5f803e15610740573d5ff35b3d5ffd5b6340dde93560e01b5f5260045ffdfe360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc
No CBOR metadata tail — this bytecode was built with cbor_metadata off, the setting our own contracts pin for CREATE2 address invariance.

disassembly

pcopoperand
0000PUSH10x80
0002PUSH10x40
0004MSTORE
0005PUSH10x04
0007CALLDATASIZE
0008LT
0009ISZERO
000aPUSH20x0018
000dJUMPI
000eJUMPDEST
000fCALLDATASIZE
0010PUSH20x0709
0013JUMPI
0014PUSH20x0709
0017JUMP
0018JUMPDEST
0019PUSH0
001aCALLDATALOAD
001bPUSH10xe0
001dSHR
001eDUP1
001fPUSH40x5c60da1b
0024EQ
0025PUSH20x0057
0028JUMPI
0029DUP1
002aPUSH40x78d4103b
002fEQ
0030PUSH20x0052
0033JUMPI
0034DUP1
0035PUSH40x7b103999
003aEQ
003bPUSH20x004d
003eJUMPI
003fPUSH40xf02a474f
0044SUB
0045PUSH20x000e
0048JUMPI
0049PUSH20x016e
004cJUMP
004dJUMPDEST
004ePUSH20x00d1
0051JUMP
0052JUMPDEST
0053PUSH20x0097
0056JUMP
0057JUMPDEST
0058CALLVALUE
0059PUSH20x0089
005cJUMPI
005dPUSH0
005eCALLDATASIZE
005fPUSH10x03
0061NOT
0062ADD
0063SLT
0064PUSH20x0089
0067JUMPI
0068PUSH0
0069MLOAD
006aPUSH10x20
006cPUSH20x0754
006fPUSH0
0070CODECOPY
0071PUSH0
0072MLOAD
0073SWAP1
0074PUSH0
0075MSTORE
0076SLOAD
0077PUSH10x01
0079PUSH10x01
007bPUSH10xa0
007dSHL
007eSUB
007fAND
0080PUSH10x80
0082SWAP1
0083DUP2
0084MSTORE
0085PUSH10x20
0087SWAP1
0088RETURN
0089JUMPDEST
008aPUSH0
008bDUP1
008cREVERT
008dJUMPDEST
008ePUSH0
008fSWAP2
0090SUB
0091SLT
0092PUSH20x0089
0095JUMPI
0096JUMP
0097JUMPDEST
0098CALLVALUE
0099PUSH20x0089
009cJUMPI
009dPUSH0
009eCALLDATASIZE
009fPUSH10x03
00a1NOT
00a2ADD
00a3SLT
00a4PUSH20x0089
00a7JUMPI
00a8PUSH10x20
00aaPUSH10x40
00acMLOAD
00adPUSH320x7f6cbd803755b2eff336a8194e29019d89d975cb558dd54f8582a673b8fc8772
00ceDUP2
00cfMSTORE
00d0RETURN
00d1JUMPDEST
00d2CALLVALUE
00d3PUSH20x0089
00d6JUMPI
00d7PUSH0
00d8CALLDATASIZE
00d9PUSH10x03
00dbNOT
00dcADD
00ddSLT
00dePUSH20x0089
00e1JUMPI
00e2PUSH10x40
00e4MLOAD
00e5PUSH320x000000000000000000000000c19d888a2f7ba65a8dcf8d03ad8f1af5cdc66430
0106PUSH10x01
0108PUSH10x01
010aPUSH10xa0
010cSHL
010dSUB
010eAND
010fDUP2
0110MSTORE
0111PUSH10x20
0113SWAP1
0114RETURN
0115JUMPDEST
0116PUSH10x01
0118PUSH10x01
011aPUSH10xa0
011cSHL
011dSUB
011eDUP2
011fAND
0120SUB
0121PUSH20x0089
0124JUMPI
0125JUMP
0126JUMPDEST
0127PUSH10x44
0129CALLDATALOAD
012aSWAP1
012bPUSH80xffffffffffffffff
0134DUP3
0135AND
0136DUP3
0137SUB
0138PUSH20x0089
013bJUMPI
013cJUMP
013dJUMPDEST
013eSWAP2
013fDUP2
0140PUSH10x1f
0142DUP5
0143ADD
0144SLT
0145ISZERO
0146PUSH20x0089
0149JUMPI
014aDUP3
014bCALLDATALOAD
014cSWAP2
014dPUSH80xffffffffffffffff
0156DUP4
0157GT
0158PUSH20x0089
015bJUMPI
015cPUSH10x20
015eDUP1
015fDUP6
0160ADD
0161SWAP5
0162DUP5
0163PUSH10x05
0165SHL
0166ADD
0167ADD
0168GT
0169PUSH20x0089
016cJUMPI
016dJUMP
016eJUMPDEST
016fCALLVALUE
0170PUSH20x0089
0173JUMPI
0174PUSH10x80
0176CALLDATASIZE
0177PUSH10x03
0179NOT
017aADD
017bSLT
017cPUSH20x0089
017fJUMPI
0180PUSH10x04
0182CALLDATALOAD
0183PUSH20x018b
0186DUP2
0187PUSH20x0115
018aJUMP
018bJUMPDEST
018cPUSH10x24
018eCALLDATALOAD
018fPUSH80xffffffffffffffff
0198DUP2
0199GT
019aPUSH20x0089
019dJUMPI
019eCALLDATASIZE
019fPUSH10x23
01a1DUP3
01a2ADD
01a3SLT
01a4ISZERO
01a5PUSH20x0089
01a8JUMPI
01a9DUP1
01aaPUSH10x04
01acADD
01adCALLDATALOAD
01aeSWAP1
01afPUSH80xffffffffffffffff
01b8DUP3
01b9GT
01baPUSH20x0089
01bdJUMPI
01beCALLDATASIZE
01bfPUSH10x24
01c1DUP4
01c2DUP4
01c3ADD
01c4ADD
01c5GT
01c6PUSH20x0089
01c9JUMPI
01caPUSH20x01d1
01cdPUSH20x0126
01d0JUMP
01d1JUMPDEST
01d2PUSH10x64
01d4CALLDATALOAD
01d5SWAP3
01d6PUSH80xffffffffffffffff
01dfDUP5
01e0GT
01e1PUSH20x0089
01e4JUMPI
01e5PUSH20x0202
01e8SWAP5
01e9PUSH20x01f8
01ecPUSH10x24
01eeSWAP6
01efCALLDATASIZE
01f0SWAP1
01f1PUSH10x04
01f3ADD
01f4PUSH20x013d
01f7JUMP
01f8JUMPDEST
01f9SWAP6
01faSWAP1
01fbSWAP5
01fcADD
01fdSWAP1
01fePUSH20x0432
0201JUMP
0202JUMPDEST
0203STOP
0204JUMPDEST
0205PUSH40x4e487b71
020aPUSH10xe0
020cSHL
020dPUSH0
020eMSTORE
020fPUSH10x41
0211PUSH10x04
0213MSTORE
0214PUSH10x24
0216PUSH0
0217REVERT
0218JUMPDEST
0219SWAP1
021aPUSH10x1f
021cDUP1
021dNOT
021eSWAP2
021fADD
0220AND
0221DUP2
0222ADD
0223SWAP1
0224DUP2
0225LT
0226PUSH80xffffffffffffffff
022fDUP3
0230GT
0231OR
0232PUSH20x023a
0235JUMPI
0236PUSH10x40
0238MSTORE
0239JUMP
023aJUMPDEST
023bPUSH20x0204
023eJUMP
023fJUMPDEST
0240SWAP1
0241DUP2
0242PUSH10x20
0244SWAP2
0245SUB
0246SLT
0247PUSH20x0089
024aJUMPI
024bMLOAD
024cDUP1
024dISZERO
024eISZERO
024fDUP2
0250SUB
0251PUSH20x0089
0254JUMPI
0255SWAP1
0256JUMP
0257JUMPDEST
0258PUSH10x40
025aMLOAD
025bRETURNDATASIZE
025cPUSH0
025dDUP3
025eRETURNDATACOPY
025fRETURNDATASIZE
0260SWAP1
0261REVERT
0262JUMPDEST
0263SWAP1
0264DUP2
0265PUSH10x20
0267SWAP2
0268SUB
0269SLT
026aPUSH20x0089
026dJUMPI
026eMLOAD
026fPUSH20x0277
0272DUP2
0273PUSH20x0115
0276JUMP
0277JUMPDEST
0278SWAP1
0279JUMP
027aJUMPDEST
027bPUSH80xffffffffffffffff
0284DUP2
0285GT
0286PUSH20x023a
0289JUMPI
028aPUSH10x1f
028cADD
028dPUSH10x1f
028fNOT
0290AND
0291PUSH10x20
0293ADD
0294SWAP1
0295JUMP
0296JUMPDEST
0297SWAP3
0298SWAP2
0299SWAP3
029aPUSH20x02a2
029dDUP3
029ePUSH20x027a
02a1JUMP
02a2JUMPDEST
02a3SWAP2
02a4PUSH20x02b0
02a7PUSH10x40
02a9MLOAD
02aaSWAP4
02abDUP5
02acPUSH20x0218
02afJUMP
02b0JUMPDEST
02b1DUP3
02b2SWAP5
02b3DUP2
02b4DUP5
02b5MSTORE
02b6DUP2
02b7DUP4
02b8ADD
02b9GT
02baPUSH20x0089
02bdJUMPI
02beDUP3
02bfDUP2
02c0PUSH10x20
02c2SWAP4
02c3DUP5
02c4PUSH0
02c5SWAP7
02c6ADD
02c7CALLDATACOPY
02c8ADD
02c9ADD
02caMSTORE
02cbJUMP
02ccJUMPDEST
02cdSWAP1
02ceCALLDATALOAD
02cfPUSH10x1e
02d1NOT
02d2DUP3
02d3CALLDATASIZE
02d4SUB
02d5ADD
02d6DUP2
02d7SLT
02d8ISZERO
02d9PUSH20x0089
02dcJUMPI
02ddADD
02dePUSH10x20
02e0DUP2
02e1CALLDATALOAD
02e2SWAP2
02e3ADD
02e4SWAP2
02e5PUSH80xffffffffffffffff
02eeDUP3
02efGT
02f0PUSH20x0089
02f3JUMPI
02f4DUP2
02f5CALLDATASIZE
02f6SUB
02f7DUP4
02f8SGT
02f9PUSH20x0089
02fcJUMPI
02fdJUMP
02feJUMPDEST
02ffSWAP1
0300DUP1
0301PUSH10x20
0303SWAP4
0304SWAP3
0305DUP2
0306DUP5
0307MSTORE
0308DUP5
0309DUP5
030aADD
030bCALLDATACOPY
030cPUSH0
030dDUP3
030eDUP3
030fADD
0310DUP5
0311ADD
0312MSTORE
0313PUSH10x1f
0315ADD
0316PUSH10x1f
0318NOT
0319AND
031aADD
031bADD
031cSWAP1
031dJUMP
031eJUMPDEST
031fSWAP4
0320SWAP2
0321SWAP1
0322PUSH80xffffffffffffffff
032bDUP4
032cSWAP3
032dPUSH10x80
032fDUP8
0330ADD
0331SWAP3
0332PUSH320x7f6cbd803755b2eff336a8194e29019d89d975cb558dd54f8582a673b8fc8772
0353DUP9
0354MSTORE
0355PUSH10x20
0357DUP9
0358ADD
0359MSTORE
035aAND
035bPUSH10x40
035dDUP7
035eADD
035fMSTORE
0360PUSH10x80
0362PUSH10x60
0364DUP7
0365ADD
0366MSTORE
0367MSTORE
0368PUSH10xa0
036aDUP4
036bADD
036cSWAP1
036dPUSH10xa0
036fDUP2
0370PUSH10x05
0372SHL
0373DUP6
0374ADD
0375ADD
0376SWAP4
0377DUP4
0378PUSH0
0379SWAP2
037aPUSH10x7e
037cNOT
037dDUP3
037eCALLDATASIZE
037fSUB
0380ADD
0381SWAP1
0382JUMPDEST
0383DUP5
0384DUP5
0385LT
0386PUSH20x0393
0389JUMPI
038aPOP
038bPOP
038cPOP
038dPOP
038ePOP
038fPOP
0390POP
0391SWAP1
0392JUMP
0393JUMPDEST
0394SWAP1
0395SWAP2
0396SWAP3
0397SWAP4
0398SWAP5
0399SWAP6
039aSWAP7
039bPUSH10x9f
039dNOT
039eDUP3
039fDUP3
03a0SUB
03a1ADD
03a2DUP8
03a3MSTORE
03a4DUP8
03a5CALLDATALOAD
03a6DUP4
03a7DUP2
03a8SLT
03a9ISZERO
03aaPUSH20x0089
03adJUMPI
03aeDUP5
03afADD
03b0DUP1
03b1CALLDATALOAD
03b2PUSH20x03ba
03b5DUP2
03b6PUSH20x0115
03b9JUMP
03baJUMPDEST
03bbPUSH10x01
03bdPUSH10x01
03bfPUSH10xa0
03c1SHL
03c2SUB
03c3AND
03c4DUP3
03c5MSTORE
03c6PUSH10x20
03c8DUP2
03c9ADD
03caCALLDATALOAD
03cbPUSH10xff
03cdDUP2
03ceAND
03cfSWAP3
03d0SWAP1
03d1DUP4
03d2SWAP1
03d3SUB
03d4PUSH20x0089
03d7JUMPI
03d8PUSH20x0423
03dbPUSH10x20
03ddSWAP3
03deDUP3
03dfPUSH10x01
03e1SWAP6
03e2DUP6
03e3DUP1
03e4SWAP6
03e5ADD
03e6MSTORE
03e7PUSH20x0415
03eaPUSH20x040a
03edPUSH20x03f9
03f0PUSH10x40
03f2DUP6
03f3ADD
03f4DUP6
03f5PUSH20x02cc
03f8JUMP
03f9JUMPDEST
03faPUSH10x80
03fcPUSH10x40
03feDUP7
03ffADD
0400MSTORE
0401PUSH10x80
0403DUP6
0404ADD
0405SWAP2
0406PUSH20x02fe
0409JUMP
040aJUMPDEST
040bSWAP3
040cPUSH10x60
040eDUP2
040fADD
0410SWAP1
0411PUSH20x02cc
0414JUMP
0415JUMPDEST
0416SWAP2
0417PUSH10x60
0419DUP2
041aDUP6
041bSUB
041cSWAP2
041dADD
041eMSTORE
041fPUSH20x02fe
0422JUMP
0423JUMPDEST
0424SWAP10
0425ADD
0426SWAP8
0427ADD
0428SWAP6
0429SWAP5
042aADD
042bSWAP3
042cSWAP2
042dSWAP1
042ePUSH20x0382
0431JUMP
0432JUMPDEST
0433PUSH10x40
0435MLOAD
0436PUSH40x28305db1
043bPUSH10xe2
043dSHL
043eDUP2
043fMSTORE
0440SWAP2
0441SWAP6
0442SWAP1
0443SWAP5
0444SWAP3
0445SWAP4
0446SWAP1
0447SWAP3
0448SWAP1
0449SWAP2
044aPUSH320x000000000000000000000000c19d888a2f7ba65a8dcf8d03ad8f1af5cdc66430
046bPUSH10x01
046dPUSH10x01
046fPUSH10xa0
0471SHL
0472SUB
0473AND
0474SWAP1
0475PUSH10x20
0477DUP2
0478PUSH10x04
047aDUP2
047bDUP6
047cGAS
047dSTATICCALL
047eSWAP1
047fDUP2
0480ISZERO
0481PUSH20x0621
0484JUMPI
0485PUSH0
0486SWAP2
0487PUSH20x068c
048aJUMPI
048bJUMPDEST
048cPOP
048dDUP1
048eISZERO
048fPUSH20x0626
0492JUMPI
0493JUMPDEST
0494PUSH20x0587
0497JUMPI
0498JUMPDEST
0499POP
049aPOP
049bPOP
049cPUSH10x01
049ePUSH10x01
04a0PUSH10xa0
04a2SHL
04a3SUB
04a4DUP4
04a5AND
04a6SWAP1
04a7POP
04a8DUP1
04a9ISZERO
04aaPUSH20x0578
04adJUMPI
04aeDUP3
04afEXTCODESIZE
04b0ISZERO
04b1PUSH20x055c
04b4JUMPI
04b5PUSH0
04b6MLOAD
04b7PUSH10x20
04b9PUSH20x0754
04bcPUSH0
04bdCODECOPY
04bePUSH0
04bfMLOAD
04c0SWAP1
04c1PUSH0
04c2MSTORE
04c3SLOAD
04c4PUSH10x01
04c6PUSH10x01
04c8PUSH10xa0
04caSHL
04cbSUB
04ccAND
04cdDUP2
04ceDUP2
04cfEQ
04d0PUSH20x0540
04d3JUMPI
04d4PUSH0
04d5MLOAD
04d6PUSH10x20
04d8PUSH20x0754
04dbPUSH0
04dcCODECOPY
04ddPUSH0
04deMLOAD
04dfSWAP1
04e0PUSH0
04e1MSTORE
04e2DUP1
04e3SLOAD
04e4PUSH10x01
04e6PUSH10x01
04e8PUSH10xa0
04eaSHL
04ebSUB
04ecNOT
04edAND
04eePUSH10x01
04f0PUSH10x01
04f2PUSH10xa0
04f4SHL
04f5SUB
04f6DUP7
04f7AND
04f8OR
04f9SWAP1
04faSSTORE
04fbPUSH320x5d611f318680d00598bb735d61bacf0c514c6b50e1e5ad30040a4df2b12791c7
051cPUSH0
051dDUP1
051eLOG3
051fDUP1
0520PUSH20x0528
0523JUMPI
0524POP
0525POP
0526POP
0527JUMP
0528JUMPDEST
0529PUSH20x053d
052cSWAP3
052dPUSH20x0537
0530SWAP2
0531CALLDATASIZE
0532SWAP2
0533PUSH20x0296
0536JUMP
0537JUMPDEST
0538SWAP1
0539PUSH20x06bb
053cJUMP
053dJUMPDEST
053ePOP
053fJUMP
0540JUMPDEST
0541PUSH40x01c12297
0546PUSH10xe1
0548SHL
0549PUSH0
054aMSTORE
054bPUSH10x01
054dPUSH10x01
054fPUSH10xa0
0551SHL
0552SUB
0553DUP5
0554AND
0555PUSH10x04
0557MSTORE
0558PUSH10x24
055aPUSH0
055bREVERT
055cJUMPDEST
055dPUSH40x9e172b1d
0562PUSH10xe0
0564SHL
0565PUSH0
0566MSTORE
0567PUSH10x01
0569PUSH10x01
056bPUSH10xa0
056dSHL
056eSUB
056fDUP4
0570AND
0571PUSH10x04
0573MSTORE
0574PUSH10x24
0576PUSH0
0577REVERT
0578JUMPDEST
0579PUSH40x3726979f
057ePUSH10xe2
0580SHL
0581PUSH0
0582MSTORE
0583PUSH10x04
0585PUSH0
0586REVERT
0587JUMPDEST
0588PUSH20x0592
058bCALLDATASIZE
058cDUP7
058dDUP10
058ePUSH20x0296
0591JUMP
0592JUMPDEST
0593DUP1
0594MLOAD
0595PUSH10x20
0597SWAP2
0598DUP3
0599ADD
059aKECCAK256
059bPUSH10x40
059dDUP1
059eMLOAD
059fPUSH10x01
05a1PUSH10x01
05a3PUSH10xa0
05a5SHL
05a6SUB
05a7DUP11
05a8AND
05a9SWAP4
05aaDUP2
05abADD
05acSWAP4
05adDUP5
05aeMSTORE
05afDUP1
05b0DUP3
05b1ADD
05b2SWAP3
05b3SWAP1
05b4SWAP3
05b5MSTORE
05b6DUP2
05b7MSTORE
05b8PUSH20x05c2
05bbPUSH10x60
05bdDUP3
05bePUSH20x0218
05c1JUMP
05c2JUMPDEST
05c3MLOAD
05c4SWAP1
05c5KECCAK256
05c6SWAP3
05c7DUP2
05c8EXTCODESIZE
05c9ISZERO
05caPUSH20x0089
05cdJUMPI
05cePUSH0
05cfDUP1
05d0SWAP5
05d1PUSH20x05f0
05d4PUSH10x40
05d6MLOAD
05d7SWAP8
05d8DUP9
05d9SWAP7
05daDUP8
05dbSWAP6
05dcDUP7
05ddSWAP5
05dePUSH40x22f3f447
05e3PUSH10xe1
05e5SHL
05e6DUP7
05e7MSTORE
05e8PUSH10x04
05eaDUP7
05ebADD
05ecPUSH20x031e
05efJUMP
05f0JUMPDEST
05f1SUB
05f2SWAP3
05f3GAS
05f4CALL
05f5DUP1
05f6ISZERO
05f7PUSH20x0621
05faJUMPI
05fbPUSH20x0607
05feJUMPI
05ffJUMPDEST
0600DUP1
0601DUP1
0602DUP1
0603PUSH20x0498
0606JUMP
0607JUMPDEST
0608DUP1
0609PUSH20x0615
060cPUSH0
060dPUSH20x061b
0610SWAP4
0611PUSH20x0218
0614JUMP
0615JUMPDEST
0616DUP1
0617PUSH20x008d
061aJUMP
061bJUMPDEST
061cPUSH0
061dPUSH20x05ff
0620JUMP
0621JUMPDEST
0622PUSH20x0257
0625JUMP
0626JUMPDEST
0627POP
0628PUSH10x40
062aMLOAD
062bPUSH40xf5778b03
0630PUSH10xe0
0632SHL
0633DUP2
0634MSTORE
0635PUSH10x20
0637DUP2
0638PUSH10x04
063aDUP2
063bDUP6
063cGAS
063dSTATICCALL
063eSWAP1
063fDUP2
0640ISZERO
0641PUSH20x0621
0644JUMPI
0645PUSH0
0646SWAP2
0647PUSH20x065d
064aJUMPI
064bJUMPDEST
064cPOP
064dPUSH10x01
064fPUSH10x01
0651PUSH10xa0
0653SHL
0654SUB
0655AND
0656CALLER
0657EQ
0658ISZERO
0659PUSH20x0493
065cJUMP
065dJUMPDEST
065ePUSH20x067f
0661SWAP2
0662POP
0663PUSH10x20
0665RETURNDATASIZE
0666PUSH10x20
0668GT
0669PUSH20x0685
066cJUMPI
066dJUMPDEST
066ePUSH20x0677
0671DUP2
0672DUP4
0673PUSH20x0218
0676JUMP
0677JUMPDEST
0678DUP2
0679ADD
067aSWAP1
067bPUSH20x0262
067eJUMP
067fJUMPDEST
0680PUSH0
0681PUSH20x064b
0684JUMP
0685JUMPDEST
0686POP
0687RETURNDATASIZE
0688PUSH20x066d
068bJUMP
068cJUMPDEST
068dPUSH20x06ae
0690SWAP2
0691POP
0692PUSH10x20
0694RETURNDATASIZE
0695PUSH10x20
0697GT
0698PUSH20x06b4
069bJUMPI
069cJUMPDEST
069dPUSH20x06a6
06a0DUP2
06a1DUP4
06a2PUSH20x0218
06a5JUMP
06a6JUMPDEST
06a7DUP2
06a8ADD
06a9SWAP1
06aaPUSH20x023f
06adJUMP
06aeJUMPDEST
06afPUSH0
06b0PUSH20x048b
06b3JUMP
06b4JUMPDEST
06b5POP
06b6RETURNDATASIZE
06b7PUSH20x069c
06baJUMP
06bbJUMPDEST
06bcPUSH0
06bdSWAP2
06beDUP3
06bfSWAP2
06c0PUSH10x20
06c2DUP3
06c3MLOAD
06c4SWAP3
06c5ADD
06c6SWAP1
06c7GAS
06c8DELEGATECALL
06c9RETURNDATASIZE
06caISZERO
06cbPUSH20x0701
06ceJUMPI
06cfRETURNDATASIZE
06d0SWAP1
06d1PUSH20x06d9
06d4DUP3
06d5PUSH20x027a
06d8JUMP
06d9JUMPDEST
06daSWAP2
06dbPUSH20x06e7
06dePUSH10x40
06e0MLOAD
06e1SWAP4
06e2DUP5
06e3PUSH20x0218
06e6JUMP
06e7JUMPDEST
06e8DUP3
06e9MSTORE
06eaRETURNDATASIZE
06ebPUSH0
06ecPUSH10x20
06eeDUP5
06efADD
06f0RETURNDATACOPY
06f1JUMPDEST
06f2ISZERO
06f3PUSH20x06f9
06f6JUMPI
06f7SWAP1
06f8JUMP
06f9JUMPDEST
06faPUSH10x20
06fcDUP2
06fdMLOAD
06feSWAP2
06ffADD
0700REVERT
0701JUMPDEST
0702PUSH10x60
0704SWAP1
0705PUSH20x06f1
0708JUMP
0709JUMPDEST
070aPUSH0
070bMLOAD
070cPUSH10x20
070ePUSH20x0754
0711PUSH0
0712CODECOPY
0713PUSH0
0714MLOAD
0715SWAP1
0716PUSH0
0717MSTORE
0718SLOAD
0719PUSH10x01
071bPUSH10x01
071dPUSH10xa0
071fSHL
0720SUB
0721AND
0722DUP1
0723ISZERO
0724PUSH20x0744
0727JUMPI
0728PUSH0
0729DUP1
072aSWAP2
072bCALLDATASIZE
072cDUP3
072dDUP1
072eCALLDATACOPY
072fDUP2
0730CALLDATASIZE
0731SWAP2
0732GAS
0733DELEGATECALL
0734RETURNDATASIZE
0735PUSH0
0736DUP1
0737RETURNDATACOPY
0738ISZERO
0739PUSH20x0740
073cJUMPI
073dRETURNDATASIZE
073ePUSH0
073fRETURN
0740JUMPDEST
0741RETURNDATASIZE
0742PUSH0
0743REVERT
0744JUMPDEST
0745PUSH40x40dde935
074aPUSH10xe0
074cSHL
074dPUSH0
074eMSTORE
074fPUSH10x04
0751PUSH0
0752REVERT
0753INVALID
0754CALLDATASIZE
0755ADDMOD
0756SWAP5
0757LOG1
0758EXTCODESIZE
0759LOG1
075aLOG3
075bUNKNOWN 0x21
075cMOD
075dPUSH80xc828492db98dca3e
0766KECCAK256
0767PUSH230xcc3735a920a3ca505d382bbc truncated