Kontrakt
0xd219f8fc34502e6a64482241f72c15da5e8883cb
- Adres
- 0xd219f8fc34502e6a64482241f72c15da5e8883cb
- Rodzaj
- zweryfikowany kontrakt FinalBundleLog
- Saldo
- 0 vETH
- Nonce
- 1
- Kod
- 9,427 bajtów codehash 0xb06ea386d59083e39debc38a53a2b18dd84588b52d51f1729a223131b77f76de
drzewo kont
- Drzewo
- 1 · konta
- Obecność
- brak liścia
- Klucz
- 0xda103f08f2e7596c4b7cbc4eaa8989170d9d47230b42fecaae22ab919a8a1d60
- Korzeń bieżący
- 0x18f30182962d8af79e7ab628ce200be69d28f54119890d737c7e736de62e703c
Ten adres nie ma liścia w drzewie kont. Każdy Final Wallet — łącznie z tożsamościami usługowymi — go ma, więc brak liścia oznacza zwykłe konto, a nie portfel.
źródło zweryfikowane
- Kontrakt
- FinalBundleLog dokładne dopasowanie · immutables zamaskowane
- Kompilator
- v0.8.33+commit.64118f21
- Optymalizator
- włączony · 200 przebiegów
- Wersja EVM
- prague
- Zweryfikowano
- 2026-09-06T09:30:32.513Z
- Pochodzenie
- preverify-final-chain (forge artifact, bytecode compared against live code)
contracts/finalchain/FinalBundleLog.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
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;
import {FinalMmr} from "../utils/FinalMmr.sol";
import {FinalChainPrecompiles} from "./FinalChainPrecompiles.sol";
import {FinalIntentLog} from "./FinalIntentLog.sol";
import {FinalBundleTree} from "../utils/FinalBundleTree.sol";
import {FinalIdentityRegistry} from "./FinalIdentityRegistry.sol";
import {FinalPqQuorum} from "./FinalPqQuorum.sol";
/**
* @title FinalBundleLog
* @notice The PQ bundle append-only log, on Final Chain.
*
* Deployed on **Final Chain**, alongside `FinalPriceOracle` — another tree in
* the set the backend chain publishes, and the one the PQ anchor depends on.
*
* ## Why this exists at all
*
* `PqAnchorModule` has always said where this log lives: the head
* (`masterRoot` @ `mmrSize`) is "advanced by the Final-chain validator set as
* it folds newly-finalized bundles into the MMR". The log itself was never on
* a chain. It lived in a Redis list maintained by a single `mmr-index` worker,
* because Final Chain did not exist and the backend was standing in for it.
*
* That stand-in is what this replaces. The MMR anchor is the **sole
* authorization for PQ execution** — a bundle executes because its root is
* proven under `masterRoot`, and nothing else vouches for it — so the question
* "what is leaf 41?" is a question about custody, and it was being answered by
* a list in a cache that one worker held a lock on.
*
* ## The log answers; nothing folds an MMR to ask it
*
* The obvious cheaper design is to emit leaves and let each reader fold them
* into an MMR itself. It is wrong for this log. Every co-signer would derive
* the root it is about to attest to, so a quorum of correct signers would be
* attesting to their own agreement about a computation rather than to a fact
* the chain states — and a subtly divergent folder in one implementation is
* indistinguishable from a dishonest one.
*
* So the chain states `(root, size)` — and it also states the PROOF. `proofAt`
* and `rootAt` answer for any historical size, so a consumer that needs "leaf
* 41 under the root the gateway anchored" makes one `eth_call` and holds no
* MMR of its own. That closes the last place two implementations of this
* construction had to agree: the backend used to replay `BundleAppended`,
* re-fold every leaf in JavaScript and build the audit path there, which is a
* second folder whose divergence from this one presents as a proof that
* verifies nowhere with both sides internally consistent.
*
* A historical proof is derivable because an MMR node is written ONCE, when
* its perfect subtree completes, and is never revised. Truncating the log to
* `atSize` therefore does not change any node the proof reads — it only
* changes which nodes the border bags — so `proofAt(i, atSize)` is exact for
* every `atSize <= size`, which is what `advanceMmrRoot` lagging behind the
* log requires.
*
* ## Storage: every node, not just the peaks
*
* `_node[level][index]` holds each completed perfect subtree, `_payload[i]`
* the pre-image at each position. That is ~2 slots per leaf and it is what
* makes the two views above possible; the peak bag is derived from `size`
* rather than stored, because a bag kept alongside the nodes would be a second
* representation of one fact.
*
* An append is `O(log n)` worst case and amortised `O(1)`: a leaf carries
* while the two highest peaks are equal-sized, which is binary increment on
* `size`, so the carry count is the number of trailing ones. It is the
* cheapest thing in the PQ path by a wide margin, and on Final Chain — one
* block every 100 ms at a 1 wei base fee — the extra `SSTORE`s cost nothing
* that matters.
*
* ## The layout is exactly `FinalMmr`'s
*
* The log is an RFC-6962 append-only log: perfect subtrees ("peaks") in
* strictly decreasing size, folded right-to-left into the root.
*
* Leaf hashing and the proof's shape come from `FinalMmr` rather than being
* restated — including `bitLength`/`popcount`, which are `internal` there
* precisely so the proof this contract BUILDS and the proof
* `PqAnchorModule.verifyInclusion` CHECKS decompose with one implementation.
* A proof built to one shape and verified against another reverts on the
* gateway naming neither side.
*
* ## Append-only is enforced, not documented
*
* There is no setter for a leaf, no way to shorten the log, and no owner path
* that rewrites one. The only mutation is `append`. A log whose operator can
* revise leaf 41 does not constrain a PQ dispatch at all — it only records
* what the operator was willing to admit, which is what an authorization root
* must not be.
*
* ## Who may append: a PQ quorum, not a role
*
* `FinalPqQuorum` over `ROLE_MMR_COSIGNER`, which is the same roster that
* anchors the head on the execution chains — deliberately, since splitting them
* would create an authority that can admit bundles nobody anchors, or anchor a
* head over leaves nobody admitted.
*
* It was a single `FINAL_MANAGER` role, and that was the largest hole left in
* the PQ lane. Admission IS the authorization: a bundle executes because its
* intent root proves under `masterRoot`, and nothing downstream re-checks it.
* So one compromised manager key admitted an arbitrary bundle and the chain
* then *stated* that root as fact, with every co-signer and every gateway
* correctly deferring to it. A K-of-N quorum of post-quantum signatures, each
* verified by this chain's own precompiles against keys read from the registry
* rather than from calldata, is what makes the log's contents cost more than
* one key.
*
* The whole authority plane moved with it. The log used to answer to a
* `FinalAccessManager`, which does not exist on Final Chain — the registry is
* the only role plane here, so `configure` is gated by it exactly as
* `FinalStateTrees.configureTree` is, and the access-manager pointer and its
* rotation setter are gone rather than left dangling.
*/
contract FinalBundleLog {
/// @notice Commitment space. Must equal `PqAnchorModule.DOMAIN_PQ_MMR`, so
/// a proof against this log's root is accepted by the gateway and a proof
/// from any other log is not.
///
/// Restated rather than imported because the gateway declares it
/// `internal` and lives on a different chain — there is no import that
/// would make this one value. `test_DomainMatchesTheGateway` pins the two
/// together; without it, a `_v3` bump on one side would produce a log whose
/// every proof is rejected, and the first symptom would be a PQ bundle that
/// will not dispatch.
bytes32 public constant DOMAIN = keccak256("FINAL_PQ_MMR_NODE_v01");
/// @dev What a co-signer's approval authorizes. Per-action, so an approval
/// to append cannot be replayed as one for any other quorum on this chain.
/// @dev `.v2`: an append is approved over each bundle's INTENT LEAVES now,
/// not over a claimed root. The shapes differ under `abi.encode`, so a v1
/// approval could not be replayed in any case — but the action is what a
/// signer reads to know what it is approving, and it is now approving a
/// different thing.
/// @dev `.v02`: each bundle is approved over its bundle-TERMS leaf as well
/// as its intent leaves (review C3). The terms leaf is folded at position 0
/// ahead of the intents, so the anchored root commits to the payout terms
/// the co-signers admitted — dispatch method, recipient, fee cap, flags,
/// includer tip/cap, committed submitter, searcher bids — and a submitter
/// who alters any of them on the execution chain fails `BundleNotAnchored`.
bytes32 private constant ACTION_APPEND = keccak256("FinalBundleLog.append.v02");
/// @dev Standalone leaves (reviews C1/C2/C5): staged-envelope and
/// settlement-credit leaves are MMR entries of their own, appended under
/// the same quorum but consuming nothing from `FinalIntentLog` — they are
/// not intents. A distinct action so an approval to append bundles can
/// never be replayed as one to append payloads, and vice versa.
bytes32 private constant ACTION_APPEND_PAYLOADS = keccak256("FinalBundleLog.appendPayloads.v01");
/// @dev Registrar-quorum action, verified by the registry with this log as
/// the verifying contract.
bytes32 public constant ACTION_CONFIGURE = keccak256("FINAL_BUNDLE_LOG_CONFIGURE_v01");
bytes32 public constant ACTION_SEED = keccak256("FINAL_BUNDLE_LOG_SEED_v01");
/// @dev Registrar-quorum action: a fresh log taking over the previous log's
/// LEAVES, payload by payload (the small-log form of {seed}).
bytes32 public constant ACTION_SEED_PAYLOADS = keccak256("FINAL_BUNDLE_LOG_SEED_PAYLOADS_v01");
/// @notice Where every signer, key and role is resolved. Immutable, so the
/// quorum can never be pointed at a registry that arrived in calldata.
FinalIdentityRegistry public immutable registry;
/// @notice The posting log every anchored intent must already appear in.
///
/// @dev Immutable for the same reason as `registry`, and for a sharper one:
/// this is the whole of Final-Chain-first admission. A log that could be
/// repointed could be repointed at one that says yes to everything, and the
/// gate would be gone with nothing on chain looking different.
FinalIntentLog public immutable intentLog;
/// @notice The role a member must hold to approve an append.
uint256 public writerRole;
/// @notice How many approvals one append needs. Zero means unconfigured,
/// and an unconfigured log refuses every write.
uint256 public threshold;
/// @notice Bound into every quorum digest. One per successful `append`
/// call, not per leaf — a batch is one authorization.
uint64 public nonce;
/// @dev Every completed perfect subtree, by level and by index at that
/// level. Written once and never revised — that immutability is the whole
/// reason `proofAt` can answer for a historical `size`.
mapping(uint256 level => mapping(uint256 index => bytes32)) private _node;
/// @dev The pre-image at each position: the bundle's intent-Merkle root,
/// untagged. Stored rather than left to the event log so a consumer needs
/// one `eth_call` and no `getLogs` range — the chain caps that range at
/// 100,000 blocks and mints one every 100 ms.
mapping(uint256 index => bytes32) private _payload;
/// @dev First position a payload took, plus one. Zero means never
/// appended. First-occurrence-wins: a duplicate bundle root is a duplicate
/// bundle, and either position proves the same root under the same head,
/// so the earlier one is the one a lagging anchor can already reach.
mapping(bytes32 payload => uint256) private _firstIndexPlusOne;
/// @notice Leaf count. The `size` half of the head the execution chains'
/// `advanceMmrRoot` is called with.
uint256 public size;
/// @notice Current log root over all `size` leaves — the `root` half of
/// that head. Zero exactly when `size == 0`: an empty log commits to no
/// leaves, matching `FinalMmr.MmrEmptyLog`.
bytes32 public root;
/// @notice A bundle root was folded in at `index`.
/// @dev Carries the payload AND the resulting head, so a reader that wants
/// the log replays this event and needs no other source. `payload` is the
/// pre-image (the bundle's intent-Merkle root), not the tagged leaf — a
/// consumer proving inclusion re-tags it via `FinalMmr.hashLeaf`, and
/// emitting the tagged form would invite proving against the wrong one.
event BundleAppended(uint256 indexed index, bytes32 indexed payload, bytes32 root, uint256 size);
/// @notice The writer role or the threshold moved.
event LogConfigured(uint256 writerRole, uint256 threshold);
/// @notice A fresh log took over the previous log's frontier.
event Seeded(uint256 size, uint256 peaks);
/// @notice A fresh log took over the previous log's leaves, whole.
event SeededPayloads(uint256 size);
/// @notice `payload` is zero. A zero leaf is almost always an uninitialised
/// read upstream, and it is unrecoverable here: the log cannot be shortened.
error ZeroBundleRoot();
/// @notice The frontier can be seeded only into an empty log.
error NotFresh();
error PeaksMismatch(uint256 expected, uint256 given);
/// @notice A view was asked about more leaves than the log holds.
error SizeAhead(uint256 asked, uint256 held);
/// @notice The audit-path walk and the `(index, size)` decomposition
/// disagreed about how many siblings a proof has.
error ProofShapeMismatch(uint256 expected, uint256 walked);
/// @notice `append` with no leaves. A quorum round that admits nothing is
/// always a caller bug, and burning a nonce for it would invalidate every
/// approval already collected for the real batch.
error EmptyBatch();
/// @notice `threshold` is zero: no quorum has been configured yet.
error LogNotConfigured();
/// @notice A threshold no live roster can meet. Register the members first.
error ThresholdUnreachable(uint256 live, uint256 required);
/// @notice Not the bootstrap admin and not a registrar.
error NotAuthorized(address caller);
/// @dev A bundle with no intents has no root to fold and nothing to consume.
error EmptyBundleLeaves(uint256 bundleIndex);
error ZeroIntentLog();
/// @notice `append` was handed a different number of terms leaves than
/// bundles — every bundle carries exactly one.
error TermsLeafCountMismatch(uint256 termsLeaves, uint256 bundles);
/// @notice A bundle's terms leaf was zero. The terms are what the
/// co-signers admitted; a bundle with none has no admitted payout terms.
error ZeroTermsLeaf(uint256 bundleIndex);
/**
* @param registry_ The identity registry. Every signer, key and role comes
* from it, and it is fixed at deployment for the same reason the
* keys are read from storage: a registry supplied per call is a
* registry the caller chooses.
*
* @dev A constructor, unlike the salt-only contracts in `contracts/`. This
* one is deployed by ordinary CREATE alongside `FinalIdentityRegistry` and
* `FinalStateTrees` — it lives on exactly one chain, so an address that is
* identical across chains buys nothing, and the vanity path costs a
* `FinalDeployer` bootstrap Final Chain has no other use for.
*
* The precompile probe is the point of having one at all: a log deployed
* where ML-DSA cannot be verified would accept no approval it was ever
* given, and the first symptom would be a PQ lane that silently never
* admits a bundle.
*/
constructor(FinalIdentityRegistry registry_, FinalIntentLog intentLog_) {
FinalChainPrecompiles.assertAvailable();
if (address(intentLog_) == address(0)) revert ZeroIntentLog();
registry = registry_;
intentLog = intentLog_;
}
/**
* @notice Set which role may approve an append, and how many approvals.
* @dev The registry's bootstrap admin alone while its window is open, the
* sealed `ROLE_REGISTRAR` quorum afterwards — the same window and the same
* quorum the registry and the trees use, because a threshold is membership
* by another name. `approvals` is empty during bootstrap.
*
* Deliberately re-callable. A co-signer set that grows or shrinks has to be
* able to move its threshold, and the alternative is a log that must be
* redeployed — which for an append-only log means abandoning its contents.
*/
function configure(
uint256 role,
uint256 k,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
if (registry.bootstrapSealed() || msg.sender != registry.bootstrapAdmin()) {
registry.requireRegistrarQuorum(
ACTION_CONFIGURE, keccak256(abi.encode(role, k)), anchorBlock, approvals
);
}
// Refuse a threshold nobody can meet. A 4-of-5 configured against three
// registered co-signers is a log that reverts on every append, and the
// revert would name the threshold rather than the roster.
if (k != 0) {
uint256 live = registry.liveMemberCount(role);
if (live < k) revert ThresholdUnreachable(live, k);
}
writerRole = role;
threshold = k;
emit LogConfigured(role, k);
}
/**
* @notice Take over the previous log's frontier — its `peaks()` and `size`
* — so appends continue at the old positions and the accumulator
* (roots, `masterRoot @ mmrSize` on every gateway) never runs
* backwards across a redeploy. NO-WIPE redeploy, ruled 2026-09-03.
* @dev The peaks are stored as the nodes they are (level = bit, index =
* leaves-before >> level), which is all a future append or `peaksAt`
* ever reads. Leaves BELOW the seed are the old log's: `payloadAt`
* and proofs for them answer from there, not here. Same authority as
* {configure}; only while this log holds nothing.
*/
function seed(
bytes32[] calldata peaks_,
uint256 size_,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
if (registry.bootstrapSealed() || msg.sender != registry.bootstrapAdmin()) {
registry.requireRegistrarQuorum(
ACTION_SEED, keccak256(abi.encode(peaks_, size_)), anchorBlock, approvals
);
}
if (size != 0) revert NotFresh();
uint256 expected;
for (uint256 x = size_; x != 0; x >>= 1) {
if (x & 1 == 1) expected++;
}
if (expected != peaks_.length) revert PeaksMismatch(expected, peaks_.length);
uint256 prefix;
uint256 p;
for (uint256 level = 255; ; level--) {
if ((size_ >> level) & 1 == 1) {
if (peaks_[p] == bytes32(0)) revert ZeroBundleRoot();
_node[level][prefix >> level] = peaks_[p++];
prefix += (uint256(1) << level);
}
if (level == 0) break;
}
size = size_;
// `head()` answers (root, size) as one pair; a seeded size beside a
// zero root is a torn head until the first append recomputes it.
root = size_ == 0 ? bytes32(0) : rootAt(size_);
emit Seeded(size_, peaks_.length);
}
/**
* @notice Take over the previous log's LEAVES — every payload, in order —
* so this log answers `payloadAt`, `indexOf`, `proofAt` and
* `peaksAt` for every position the old one did, with the same
* roots at every size. The small-log form of {seed}: a peak-seeded
* log holds no payload below its seed, and the first live one
* (2026-09-05) was seeded one leaf ABOVE what the gateways had
* anchored — the proposer could not read that leaf's payload here,
* could not build a verifiable proposal, and every anchor stalled
* behind it. Copying the leaves has no such edge; it costs one
* fold per leaf, which on this chain is cheap for a log of
* thousands. Same authority as {configure}; only while this log
* holds nothing.
*/
function seedPayloads(
bytes32[] calldata payloads,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
if (registry.bootstrapSealed() || msg.sender != registry.bootstrapAdmin()) {
registry.requireRegistrarQuorum(
ACTION_SEED_PAYLOADS, keccak256(abi.encode(payloads)), anchorBlock, approvals
);
}
if (size != 0) revert NotFresh();
if (payloads.length == 0) revert EmptyBatch();
for (uint256 i = 0; i < payloads.length; i++) {
_append(payloads[i]);
}
emit SeededPayloads(payloads.length);
}
/**
* @notice Fold one or more bundles in as the next leaves, under a
* post-quantum quorum.
* @param termsLeaves One bundle-terms leaf per bundle (review C3) —
* `keccak256(abi.encode(DOMAIN_BUNDLE_TERMS, chainId, method,
* recipient, maxFeeWei, bundleFlags, tipPerGas, capPerGas,
* submitter, bidsHash))`, exactly as the execution chain's gateway
* rebuilds it. Folded at position 0, ahead of the intents; never
* consumed from the intent log, because it is not an intent.
* @param bundles One inner array per bundle: that bundle's intent leaves,
* in bundle order. The root is RECOMPUTED from `[terms, leaves…]`.
* @param approvals At least `threshold` of them, ascending by signer.
* @return firstIndex The 0-based position the FIRST leaf occupies, forever.
*
* @dev **Leaves, not a root.** A claimed root is a claim about intents
* nobody on this chain checked. Taking the leaves lets the contract do two
* things it could not do before: recompute the payload with the same fold
* the execution chain's gateway uses, and require every leaf to be an open
* posting in `FinalIntentLog`.
*
* That is what makes Final-Chain-first admission a mechanism instead of a
* quorum policy. A bundle cannot be anchored unless every intent in it was
* posted here first, and since the execution chains' whole authorization is
* this log's head, an intent that was never posted can never execute. The
* co-signers already recomputed the root off-chain in
* `validateBundleForAnchor`; this moves the check to where a compromised
* quorum cannot skip it.
*
* Consumption is also the anti-replay gate for intents. The execution chain
* keeps a consumed-seqId set that stops a BUNDLE re-executing; it cannot see
* one intent being re-anchored inside a second bundle. This can.
*
* A batch rather than a leaf, because the quorum round — not the
* `O(log n)` carry — is what an append costs. One round per bundle would
* put a K-of-N collection on the critical path of every PQ dispatch; one
* round per run of bundles is the same authorization over more work. A
* batch of one is the degenerate case and is exactly as safe.
*
* The digest binds the nonce AND the pre-append `size`, so a co-signer
* approves the POSITIONS as well as the contents. `size` is redundant for
* replay — the nonce already covers that — and it is not redundant for what
* this log is: "what is leaf 41?" is the question the whole contract exists
* to answer, and an approval that named the leaves but not where they
* land would leave that answer to whoever assembled the transaction.
*
* ML-DSA-87 is required rather than accepted, AND every approval carries a
* seal. Admission is the authorization for execution — a root that lands
* here executes — so it takes both key classes: the transaction key's
* ML-DSA-87 signature and the seal key's SLH-DSA-SHAKE-256s over the same
* digest. A break in either family leaves the log unmovable rather than
* taken. `anchorBlock` is the tree-1 view the members decided the roster
* against; `FinalPqQuorum.require_` bounds how stale it may be.
*/
function append(
bytes32[] calldata termsLeaves,
bytes32[][] calldata bundles,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external returns (uint256 firstIndex) {
if (bundles.length == 0) revert EmptyBatch();
if (termsLeaves.length != bundles.length) revert TermsLeafCountMismatch(termsLeaves.length, bundles.length);
uint256 k = threshold;
if (k == 0) revert LogNotConfigured();
uint64 n = nonce;
firstIndex = size;
FinalPqQuorum.require_(
registry,
approvals,
FinalPqQuorum.digest(
address(this),
ACTION_APPEND,
anchorBlock,
keccak256(abi.encode(n, firstIndex, termsLeaves, bundles))
),
writerRole,
k,
FinalPqQuorum.ALG_ML_DSA_87,
anchorBlock,
true
);
nonce = n + 1;
for (uint256 i = 0; i < bundles.length; i++) {
bytes32[] calldata leaves = bundles[i];
if (leaves.length == 0) revert EmptyBundleLeaves(i);
if (termsLeaves[i] == bytes32(0)) revert ZeroTermsLeaf(i);
// Consume BEFORE folding. Either order works today, but consuming
// first means a bundle that names an unposted or already-spent leaf
// reverts on the leaf itself rather than after doing the fold — and
// the revert names which leaf, which is the thing an operator needs.
// The terms leaf sits at position 0 and is NOT consumed: it is the
// admitted payout terms, not a posting.
bytes32[] memory layer = new bytes32[](leaves.length + 1);
layer[0] = termsLeaves[i];
for (uint256 j = 0; j < leaves.length; j++) {
intentLog.consume(leaves[j]);
layer[j + 1] = leaves[j];
}
_append(FinalBundleTree.foldLeaves(layer));
}
}
/**
* @notice Append standalone leaves — staged-envelope and settlement-credit
* commitments (reviews C1/C2/C5) — under the same post-quantum
* quorum, consuming nothing.
* @param payloads The leaf preimages, already domain-separated by their
* own kind (`DOMAIN_STAGED_ENVELOPE`, `DOMAIN_SETTLEMENT_CREDIT` on
* the gateway side). Each becomes one position in the log and is
* provable to any gateway exactly as a bundle root is.
* @return firstIndex The 0-based position the FIRST payload occupies.
*
* @dev These are not intents and were never posted, so nothing is consumed
* from `FinalIntentLog`; what authorizes them is what authorizes a bundle
* root — K of N co-signers, each of whom recomputed the leaf from the facts
* it commits to (a settlement credit from the collecting chain's recorded
* charge and the terminal execution fact; a staged envelope from the
* admitted intent's anchored bound) before signing. A separate action id
* keeps a bundle approval from being replayed here and vice versa; the
* digest binds the nonce and the pre-append size, so positions are approved
* along with contents, as for `append`.
*/
function appendPayloads(
bytes32[] calldata payloads,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external returns (uint256 firstIndex) {
if (payloads.length == 0) revert EmptyBatch();
uint256 k = threshold;
if (k == 0) revert LogNotConfigured();
uint64 n = nonce;
firstIndex = size;
FinalPqQuorum.require_(
registry,
approvals,
FinalPqQuorum.digest(
address(this),
ACTION_APPEND_PAYLOADS,
anchorBlock,
keccak256(abi.encode(n, firstIndex, payloads))
),
writerRole,
k,
FinalPqQuorum.ALG_ML_DSA_87,
anchorBlock,
true
);
nonce = n + 1;
for (uint256 i = 0; i < payloads.length; i++) {
_append(payloads[i]);
}
}
/// @dev The fold itself. Separated from authorization so the batch loop is
/// one concern and the quorum is the other.
function _append(bytes32 payload) private {
if (payload == bytes32(0)) revert ZeroBundleRoot();
uint256 index = size;
_payload[index] = payload;
// First occurrence wins, so `indexOf` is stable for the life of the
// log. A later duplicate is still appended and still provable at its
// own position; it just is not the one this lookup names.
if (_firstIndexPlusOne[payload] == 0) _firstIndexPlusOne[payload] = index + 1;
// Push the new leaf as a size-1 peak, then carry while the top two
// peaks are equal-sized. Equal size is exactly "the low bits of the
// pre-increment count are set", so the carry count is the number of
// trailing ones — binary increment, and the reason this is amortised
// O(1) rather than O(log n) per append.
bytes32 carry = FinalMmr.hashLeaf(DOMAIN, payload);
_node[0][index] = carry;
uint256 completed = index; // leaves already in the log
uint256 level = 0;
while (completed & 1 == 1) {
// The left sibling is the perfect subtree that ends where this one
// begins, and it is already stored — `_append` is the only writer
// and it wrote that node on the append that completed it.
//
// Positional: the earlier subtree is always the LEFT child. Sorting
// the pair here would make two different logs hash alike and is the
// second-preimage hole `FinalMmr`'s tags exist to close.
bytes32 left = _node[level][completed - 1];
carry = _hashNode(left, carry);
unchecked {
completed >>= 1;
++level;
}
_node[level][completed] = carry;
}
unchecked {
size = index + 1;
}
root = rootAt(size);
emit BundleAppended(index, payload, root, size);
}
// ------------------------------------------------------------------ reads
/// @notice The pre-image at one position — the bundle's intent-Merkle root.
/// @dev Untagged, as the event carries it. A consumer proving inclusion
/// re-tags through `FinalMmr.hashLeaf`; returning the tagged form here
/// would invite proving against the wrong one.
function payloadAt(uint256 index) public view returns (bytes32) {
if (index >= size) revert FinalMmr.MmrIndexOutOfRange(index, size);
return _payload[index];
}
/// @notice The pre-images at `[from, to)`, in order.
/// @dev Batched because the proposer needs every unanchored leaf between
/// the gateway's head and this log's, and one `eth_call` per leaf turns a
/// pass into `size` round trips on a chain that mints a block every 100 ms.
function payloadsBetween(uint256 from, uint256 to) external view returns (bytes32[] memory out) {
if (to > size) revert SizeAhead(to, size);
if (from > to) revert FinalMmr.MmrIndexOutOfRange(from, to);
out = new bytes32[](to - from);
for (uint256 i = from; i < to; ) {
out[i - from] = _payload[i];
unchecked { ++i; }
}
}
/// @notice Where a bundle root first landed.
/// @return index Its position. @return found False when it never landed,
/// which is a state — a proposed bundle that has not been admitted yet —
/// rather than an error.
function indexOf(bytes32 payload) external view returns (uint256 index, bool found) {
uint256 plusOne = _firstIndexPlusOne[payload];
if (plusOne == 0) return (0, false);
return (plusOne - 1, true);
}
/// @notice The peak bag backing the current root.
/// @dev Derived from `size` rather than stored: a bag kept alongside the
/// nodes would be a second representation of one fact, and the interesting
/// property — `length == popcount(size)` — is then a consequence rather
/// than something to maintain.
function peaks() external view returns (bytes32[] memory) {
return peaksAt(size);
}
/// @notice The peak bag as of `atSize` leaves.
function peaksAt(uint256 atSize) public view returns (bytes32[] memory bag) {
if (atSize > size) revert SizeAhead(atSize, size);
bag = new bytes32[](FinalMmr.popcount(atSize));
uint256 n = 0;
uint256 offset = 0;
uint256 level = FinalMmr.bitLength(atSize);
// MSB first, so the peaks come out largest-first — the order
// `_rootFromPeaks` and every reader expect.
while (level > 0) {
unchecked { --level; }
if ((atSize >> level) & 1 == 1) {
bag[n] = _node[level][offset >> level];
unchecked {
++n;
offset += (1 << level);
}
}
}
}
/// @notice The head the execution chains' `advanceMmrRoot` should be called
/// with, read in one call so the pair can never be torn across two.
function head() external view returns (bytes32 root_, uint256 size_) {
return (root, size);
}
/**
* @notice The root over the FIRST `atSize` leaves.
* @dev Exact for every `atSize <= size`, not only the current one, because
* an MMR node is written once and never revised. That is what lets a
* consumer prove against the head the gateway actually anchored, which
* lags this log by design — a proof built against a larger size is
* well-formed and verifies nowhere.
*/
function rootAt(uint256 atSize) public view returns (bytes32) {
if (atSize > size) revert SizeAhead(atSize, size);
if (atSize == 0) return bytes32(0);
return _rangeRoot(0, atSize);
}
/**
* @notice The audit path proving leaf `leafIndex` under `rootAt(atSize)`.
* @param leafIndex 0-based position, `< atSize`.
* @param atSize The head to prove against — `gateway.mmrSize()`, not this
* log's size, whenever the two differ.
*
* @dev The walk is the Trillian formulation: rise level by level and record
* the sibling, which is a stored perfect subtree unless it is the ragged
* last node of its level, in which case it is the bag of the peaks that
* remain. Both cases resolve to nodes this contract wrote, so there is
* nothing to recompute from leaves and nothing for a caller to fold.
*
* The result feeds `FinalMmr.computeRoot` verbatim on the gateway. Its
* length is derived here from the same `(index, size)` decomposition that
* verifier uses, and the walk's own count is checked against it — a
* disagreement is a proof rejected on another chain naming neither side,
* so it is caught where both derivations are visible.
*/
function proofAt(uint256 leafIndex, uint256 atSize) public view returns (bytes32[] memory proof) {
if (atSize > size) revert SizeAhead(atSize, size);
if (atSize == 0) revert FinalMmr.MmrEmptyLog();
if (leafIndex >= atSize) revert FinalMmr.MmrIndexOutOfRange(leafIndex, atSize);
uint256 inner = FinalMmr.bitLength(leafIndex ^ (atSize - 1));
uint256 border = FinalMmr.popcount(leafIndex >> inner);
proof = new bytes32[](inner + border);
uint256 idx = leafIndex;
uint256 last = atSize - 1; // index of the last node at the current level
uint256 level = 0;
uint256 n = 0;
while (last != 0) {
uint256 sibling = idx ^ 1;
if (sibling < last) {
// A complete perfect subtree, stored when it completed.
proof[n] = _node[level][sibling];
unchecked { ++n; }
} else if (sibling == last) {
// The ragged right edge: this level's last node may cover fewer
// than `2 ** level` leaves, so it is the bag of the peaks in
// `[sibling << level, atSize)` rather than a node of its own.
proof[n] = _rangeRoot(sibling << level, atSize);
unchecked { ++n; }
}
// `sibling > last` — no sibling at this level. Rise without
// recording, which is what makes an imperfect tree's path shorter
// than its depth.
unchecked {
idx >>= 1;
last >>= 1;
++level;
}
}
if (n != proof.length) revert ProofShapeMismatch(proof.length, n);
}
/**
* @notice Everything needed to prove one bundle, in one call.
* @dev One call rather than three, because payload, proof and root are only
* meaningful together: fetched separately, an append landing between two of
* them yields a proof against a root the caller did not read, and the
* dispatch reverts on another chain with three individually-correct values.
*/
function bundleAt(uint256 leafIndex, uint256 atSize)
external
view
returns (bytes32 payload, bytes32[] memory proof, bytes32 root_)
{
proof = proofAt(leafIndex, atSize);
payload = payloadAt(leafIndex);
root_ = rootAt(atSize);
}
/**
* @dev The Merkle-Tree-Hash of leaves `[lo, hi)`, where `lo` is aligned to
* the largest perfect subtree the range can hold.
*
* Every part is a COMPLETE perfect subtree and therefore a stored node,
* which is the property that makes a historical proof derivable at all.
* The parts come out largest-first and fold right-to-left, which is the
* same peak bag `FinalMmr.computeRoot` walks as a proof's border — and is
* what makes a proof against this root verify on the gateway.
*/
function _rangeRoot(uint256 lo, uint256 hi) private view returns (bytes32) {
uint256 remaining = hi - lo;
bytes32[] memory bag = new bytes32[](FinalMmr.popcount(remaining));
uint256 n = 0;
uint256 offset = lo;
uint256 level = FinalMmr.bitLength(remaining);
while (level > 0) {
unchecked { --level; }
if ((remaining >> level) & 1 == 1) {
bag[n] = _node[level][offset >> level];
unchecked {
++n;
offset += (1 << level);
}
}
}
bytes32 acc = bag[n - 1];
for (uint256 i = n - 1; i > 0; ) {
unchecked { --i; }
acc = _hashNode(bag[i], acc);
}
return acc;
}
/// @dev `FinalMmr` keeps its node hash private (proof verification is its
/// only caller there), so the one construction that must not fork is
/// restated once, here, against its published spec:
/// `keccak256(0x01 ‖ domain ‖ left ‖ right)`. `test_LayoutMatchesFinalMmr`
/// pins it against a proof the library itself verifies, so a change to
/// either side fails rather than producing a log the gateway rejects.
function _hashNode(bytes32 left, bytes32 right) private pure returns (bytes32) {
return keccak256(abi.encodePacked(uint8(0x01), DOMAIN, left, right));
}
}
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
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;
import {FinalChainPrecompiles} from "./FinalChainPrecompiles.sol";
import {FinalChainTime} from "./FinalChainTime.sol";
/**
* @title FinalCertificate
* @notice Reads a Final Certificate (`.fcert`, schema v3) on chain.
*
* @dev Final Chain only — it needs the SHA3-256 precompile, because the schema
* hashes with FIPS-202 SHA3 and the EVM has `keccak256`, which is a different
* function.
*
* ## Why the chain parses this at all
*
* `FinalIdentityRegistry.registerWithCertificate` used to take the TBS bytes
* AND the public keys as separate arguments. It derived `certHash` from the
* bytes, which sounds like verification and is not: nothing compared the keys
* to the certificate, so a registrar could bind any certificate to any keypair.
* The registry would then 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 way for
* two arguments to disagree.
*
* ## The SubjectKeyId check
*
* The schema defines `SubjectKeyId` as SHA3-256 of the `PublicKeyBlock`. Having
* parsed the block, this recomputes that digest and compares. The field is
* inside the TBS, so it is covered by the CA's signatures — which makes the
* check a statement about what the CA attested, not merely about internal
* consistency of bytes the caller supplied.
*
* ## What this does NOT do
*
* It does not verify the CA's signatures over the TBS, and it does not walk the
* chain to the root. Both are possible here — the precompiles verify ML-DSA-87
* and SLH-DSA-SHAKE-256s — and both are deliberately out of scope for the
* registry's bootstrap path, where the registrar is the party that issued the
* certificate in the first place. `verifyIssuerSignatures` below is provided for
* callers that need it, and the identity registry uses it once a CA is itself
* registered.
*/
library FinalCertificate {
/// `"PQCF"`.
uint32 internal constant MAGIC = 0x50514346;
/// The current wire generation — v5's `Version = 2` (chain-attested
/// issuance; ruled 2026-09-01). The v4 wire (`Version = 1`) stays
/// PARSEABLE so pre-cutover artifacts still read; encoders write 2.
/// fails to parse rather than being reinterpreted: `pqKeysHash` and every
/// wallet address derive from this exact layout.
uint32 internal constant VERSION = 2;
/// The v4 generation, accepted on parse for pre-cutover artifacts.
uint32 internal constant VERSION_V4 = 1;
/// @notice The 0x0102 Institution identity extension (issuer profile).
uint16 internal constant EXT_INSTITUTION = 0x0102;
/// Algorithm ids ARE the FIPS numbers, in one space for signatures and KEMs
/// — the same ids the quorum wire format and the backend registry use, and
/// the numbers the precompile addresses end in.
/// ML-KEM-1024 (FIPS 203), the lattice half of the encapsulation pair.
uint16 internal constant ALG_ML_KEM_1024 = 0x0003;
/// ML-DSA-87 (FIPS 204). Transaction class.
uint16 internal constant ALG_ML_DSA_87 = 0x0004;
/// SLH-DSA-SHAKE-256s (FIPS 205). Access class, and the seal.
uint16 internal constant ALG_SLH_DSA_SHAKE_256S = 0x0005;
/// FN-DSA (FIPS 206). Reserved: no implementation, never accepted.
uint16 internal constant ALG_FN_DSA = 0x0006;
/// HQC-5 (FIPS 207), the code-based half of the encapsulation pair.
uint16 internal constant ALG_HQC_5 = 0x0007;
/// Certificate signing. Says which key to verify WITH; it grants nothing —
/// that comes from `Depth` and `MaxDelegationDepth`.
uint16 internal constant PURPOSE_CERT_SIGNING = 0x0004;
/// The wallet's four slots, in two stages of two.
///
/// A certificate carries ONE stage, never all four. The stage is what gets
/// 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.
///
/// 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;
uint16 internal constant PURPOSE_ACTIVE_ACCESS = 0x0011;
uint16 internal constant PURPOSE_RECOVERY_TX = 0x0012;
uint16 internal constant PURPOSE_RECOVERY_ACCESS = 0x0013;
/// @dev v4's encapsulation purposes. Parsed, and each stage's pair is
/// resolved alongside its signing pair — `FinalIdentityRegistry` then
/// stores them so a sender can encapsulate to a registered party
/// without a second lookup somewhere less authoritative.
///
/// They were declared and skipped for one release, which is how the
/// registry's four encapsulation-key mappings ended up read in three
/// places and written in none: `kemCommitments` hashed the empty
/// string for every account and `kemKeysOf` returned nothing.
uint16 internal constant PURPOSE_ACTIVE_KEM = 0x0014;
uint16 internal constant PURPOSE_RECOVERY_KEM = 0x0015;
/// @dev The seal: a second SLH-DSA-SHAKE-256s key, distinct from the access
/// key, that co-signs execution-class quorum decisions. Carried by
/// SERVICE certificates only — a user's wallet never seals — and
/// optional in the schema, so a certificate without it parses
/// unchanged. Outside `keysHash`: a seal is operational, rotated by
/// issuing a new live certificate, and it must not move a wallet
/// address it plays no part in.
uint16 internal constant PURPOSE_ACTIVE_SEAL = 0x0016;
/// @dev A purpose no certificate can carry, so `parse` can be told "this
/// stage has no encapsulation slot" without a second boolean. `0xffff`
/// is outside the registry and reserved by being used here.
uint16 internal constant NO_KEM_PURPOSE = 0xffff;
/// Nanoseconds per second. The schema's validity fields are nanoseconds and
/// `block.timestamp` is seconds; a comparison across the two units is a bug
/// waiting for the first certificate anybody actually checks.
/// @dev The schema stamps validity in NANOseconds and this chain's clock is
/// MILLIseconds, so a certificate converts down by 1e6 rather than by
/// 1e9. It was 1e9 — seconds — which made every `notBefore` look 1000x
/// too small against `block.timestamp` and every certificate
/// permanently "already valid", including one issued for the future.
uint64 internal constant NS_PER_MILLISECOND = FinalChainTime.NS_PER_MILLISECOND;
/// @notice What the chain keeps out of one certificate.
struct Parsed {
bytes32 certHash;
bytes32 serial;
/// keccak256 of the IssuerDN bytes, for the chain-issuer pin: a
/// chain-attested certificate carries the ruled constant DN and the
/// registry compares hashes rather than strings.
bytes32 issuerDnHash;
/// The SubjectDN bytes verbatim — the jurisdiction rule reads its
/// `C=` component at issuer registration.
bytes subjectDn;
/// The 0x0102 Institution extension VALUE, when present; empty
/// otherwise. Issuer registration parses jurisdiction out of it.
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;
uint8 depth;
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;
}
error BadMagic(uint32 got);
error BadVersion(uint32 got);
error Truncated(uint256 needed, uint256 got);
error SubjectKeyIdMismatch(bytes32 derived, bytes32 declared);
error MissingSlot(uint16 purpose);
error WrongAlgorithmForSlot(uint16 purpose, uint16 algorithm);
error DuplicateKey(uint16 purpose, uint16 algorithm);
error KeysNotSorted();
error BadKeyLength(uint16 algorithm, uint256 length);
error InvalidDepth(uint8 depth, uint8 maxDelegationDepth);
error ValidityInverted(uint64 notBefore, uint64 notAfter);
/**
* @notice Parse and self-check a `TBSCertificate`.
* @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.
*
* @dev Checking for a CAPABILITY rather than a type is the 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.
*/
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 generations. v4 artifacts predate chain-attested issuance
// and still parse — supersession is handled at admission (PoP and the
// chain-issuer pins), not by refusing to read history.
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: `activeTransaction` + `activeAccess`.
/// @dev `external`, like the other three entry points below: the registry
/// sits against the EIP-170 ceiling and the TBS parser is its single
/// largest inlined dependency, so the four doors it actually calls are
/// DEPLOY-LINKED — the library is one more contract in the plane's fixed
/// nonce-0 deploy order (doctrine §2 of `arch/final-chain-regenesis.md`),
/// its address baked immutably into the registry's bytecode. A linked
/// library is code, not a key: nothing can repoint it after deployment.
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, an ordinary action for that
/// account, uses the live access key. Keeping the two stages in separate
/// certificates is what makes that boundary something a verifier can see.
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 CA certificate, whose two keys are both cert-signing.
/// @dev No encapsulation purpose: a CA signs and is never sealed to, so
/// `PURPOSE_ACTIVE_KEM` is passed as a value the loop can never match. A
/// CA certificate carrying encapsulation keys would parse them into slots
/// `_write` then discards, which is a shape worth refusing to have.
function parseCa(bytes calldata tbs) external view returns (Parsed memory) {
return parse(tbs, PURPOSE_CERT_SIGNING, PURPOSE_CERT_SIGNING, NO_KEM_PURPOSE);
}
/**
* @notice Verify a CA's dual signature over `tbs`.
* @dev Both must verify, not either. Two signatures under two different
* hardness assumptions is the entire reason the schema carries two, and
* accepting one would collapse that to whichever family breaks first.
*/
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);
}
function _need(bytes calldata tbs, uint256 upto) private pure {
if (tbs.length < upto) revert Truncated(upto, tbs.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);
}
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/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
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;
/**
* @title FinalChainPrecompiles
* @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 `final-reth`, the node binary in `FinalBackend/vendor/reth/final/`, 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/FinalChainTime.sol
// SPDX-License-Identifier: BUSL-1.1
// Copyright (c) 2024-2026 Final DeFi
pragma solidity ^0.8.20;
/**
* @title FinalChainTime
* @notice **On Final Chain, `block.timestamp` is MILLISECONDS.**
*
* @dev Every other EVM chain stamps seconds. This one cannot: it mints a block
* every 100 ms and Ethereum requires block timestamps to strictly increase, so
* a second-denominated clock would run out of distinct values ten times over
* per second. Milliseconds is the deliberate choice, and it is a property of
* the CHAIN — `final-reth` — not of any contract here.
*
* Every duration on this chain is therefore in milliseconds, and this library
* exists so that is stated in one place instead of assumed in fifteen.
*
* ## How this was found, which is the reason for the naming rules below
*
* It was not found by the test suite. Foundry's `block.timestamp` is seconds,
* so all 1249 tests agreed with the contracts and every one of them was wrong
* about the chain they deploy to. It was found the first time anything
* exercised a deadline against the real chain — a posted intent, which reverted
* `DeadlinePassed` against a header whose deadline had been computed from wall
* time.
*
* What was actually broken was worse than a posting. `rotationInitiatedAt` is
* written from `block.timestamp` and compared against `rotationInitiatedAt +
* delaySeconds`: a millisecond clock plus a second-denominated delay. The
* 24-hour default recovery delay elapsed in **86 seconds**, and the two-year
* dormancy threshold in about seventeen hours. That delay is the thing standing
* between a stolen recovery key and an account.
*
* Nothing had noticed because nothing time-dependent had ever run: `walletCount`
* is 0, `FinalBundleLog.size` is 0, and no intent had been posted.
*
* ## The naming rule
*
* A field or constant carrying a duration or an instant on this chain ends in
* `Ms`. Not decoration — the bug was a field named `delaySeconds` that held
* milliseconds, and a name that lies is how the next reader reintroduces it.
* `SECONDS` names are gone from `contracts/finalchain/` and must not come back.
*
* Solidity's `hours` / `days` suffixes are still the clearest way to write a
* duration, so they are written as `24 hours * MS_PER_SECOND` rather than as a
* literal: the intent stays readable and the unit stays explicit.
*/
library FinalChainTime {
/// @notice Milliseconds per second. The whole conversion, named once.
uint64 internal constant MS_PER_SECOND = 1_000;
/// @notice Milliseconds per nanosecond divisor — the certificate schema
/// stamps validity in NANOseconds, so a certificate converts down to this
/// chain's clock rather than up.
uint64 internal constant NS_PER_MILLISECOND = 1_000_000;
/// @notice This chain's clock, stated as a function so a caller reads the
/// unit rather than remembering it.
/// @dev No arithmetic. It exists to make `FinalChainTime.nowMs()` the thing
/// people write, which is self-describing where `block.timestamp` is not.
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
//
// @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";
/**
* @title FinalIdentityRegistry
* @notice Who every party in the system IS, on chain, with its certificate.
*
* @dev Final Chain only. Every service, every co-signer, every certificate
* authority and every operator has one record here, and that record carries the
* party's actual public keys — not commitments to them.
*
* ## Why the full key and not a hash
*
* 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: 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` reads them from storage, and
* "which key is co-signer 3" has exactly one answer. That question previously
* had three: an environment variable, an on-chain roster, and a Secret Manager
* entry, with nothing comparing them. Every configuration failure in this
* program has been those three disagreeing.
*
* ## The certificate is the record, not a pointer to one
*
* `certHash` is `SHA3-256(TBSCertificate)` — the certificate's own identity per
* the v3 schema, and the handle revocation is keyed on. The schema says
* revocation exists "on Final Chain only"; this is that place.
*
`registerWithCertificate` takes the TBS bytes and **reads everything out of
* them**: the digest, the serial, the key identifiers, the depth pair, the
* validity window and both public keys. It takes no key arguments at all.
*
* That is a correction, and the version it replaces is worth naming because it
* looked right. It took the TBS *and* the keys, derived `certHash` from the
* TBS, and never compared the two — so a registrar could bind any certificate
* to any keypair, and the registry would hold a key the certificate does not
* contain. Every signature that key produced would then verify against a
* certificate that never authorised it.
*
* ## The root is the first record on this chain, not a file somewhere
*
* The schema says Final Chain is the only root CA and that "the root is pinned,
* not distributed" — chain validation terminates at Final Chain **by identity**,
* never by finding a self-signed certificate in a local store.
*
* `registerRoot` is that pin, and it is the only entry point that accepts a
* certificate without checking an issuer's signature. It takes a depth-0,
* self-issued certificate from the bootstrap admin, once. Everything after it
* is `registerWithCertificate`, which **verifies the issuer's ML-DSA and
* SLH-DSA signatures on chain, through the precompiles**, against the issuer's
* own registered keys, and checks that the child's `AuthorityKeyId` is the
* issuer's `SubjectKeyId` and that the issuer's depth admits it.
*
* So there is no path by which a key enters this registry unattested. Not
* "a registrar should only register certified keys" — a registrar *cannot*
* register anything else.
*
* ## Roles are a bitmask
*
* One party is legitimately several things — a co-signer that is also a
* publisher, an operator that is also a guardian. A single enum would force
* either duplicate records for one key (two sources of truth about one party)
* or a role hierarchy nobody agrees on. A mask has neither problem, and a
* quorum asks "does this account carry ROLE_X" rather than "is this account an
* X", which is the same distinction the certificate schema draws when it says
* verifiers check for capabilities and never for types.
*
* ## 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, an LMS key,
* the registrar threshold itself) and every state-plane configuration change
* that routes 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 exception, and it is the only one: while it is
* open the bootstrap admin writes alone, because every roster has to be
* installed by someone before it can install itself.
*
* ## The sender is not the account
*
* Final Chain transactions are type 0x46, signed by ML-DSA-87, and the node
* derives `msg.sender` from the key: `keccak256(0x04 ‖ publicKey)[12:]`. That
* address pays gas and holds no authority. {accountOfSender} binds it to the
* identity whose `activeTransaction` 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 rather than the roster.
*/
/// @dev Domain for a stage's encapsulation commitment. Byte-equal to
/// `FinalWalletFactory.DOMAIN_KEM_BUNDLE` and to `DOMAIN_KEM_BUNDLE_PREIMAGE` in
/// the issuer; three derivations of one word, and a mismatch in any of them is a
/// certificate that verifies nowhere.
bytes32 constant DOMAIN_KEM_BUNDLE = keccak256("FINAL_KEM_BUNDLE_v01");
/// @dev Tree 8's leaf domain — byte-equal to
/// `IdentityRootModule.DOMAIN_IDENTITY_LEAF` on every execution chain.
/// Restated rather than imported because the module lives on other chains and
/// there is no import that would make them one value; the cross-contract
/// parity test pins the pair. The `_PQ_` spelling is historical and FROZEN:
/// the premined vanity certificates were mined against this exact constant,
/// and the leaf it derives is the `certHash` inside every wallet's CREATE2
/// derivation.
bytes32 constant DOMAIN_IDENTITY_LEAF = keccak256("FINAL_IDENTITY_LEAF_PQ_v01");
/// @dev D7 (ruled 2026-09-01): ISSUER records project into tree 8 under their
/// own domain — `DOMAIN_ISSUER_LEAF ‖ certHash ‖ version ‖ issuerTreeRoot` —
/// so an issuer is stapleable for offline licence verification while the
/// distinct domain keeps its leaf out of wallet admission (the gateway folds
/// with the wallet domain, so an issuer leaf can never satisfy
/// `verifyIdentityCert`). `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 the fixed-depth insertion-ordered state
/// trees cannot prove non-inclusion.
bytes32 constant DOMAIN_ISSUER_LEAF = keccak256("FINAL_ISSUER_LEAF_v01");
/// @dev Chain-issuer constants (ruled 2026-09-01, amended same day: C-less).
/// The chain is the issuer but holds no keypair, so every chain-attested
/// certificate carries these two NAMED values in its issuer fields — required
/// by the wire format, verifying nothing, covered by `certHash`. The DN is
/// deliberately env-agnostic AND jurisdiction-silent: the issuer is the
/// worldwide network, not a legal entity, and an env-specific DN would fork
/// `certHash` per environment. Reference implementation:
/// `dashboard/public/fcert.js` (`CHAIN_ISSUER_DN`, `CHAIN_AUTHORITY_KEY_ID`);
/// `docs/developers/certificate-schema.md` § Chain-issuer constants.
bytes32 constant CHAIN_ISSUER_DN_HASH = keccak256("CN=Final Chain,O=Final DeFi");
/// @dev `SHA3-256(utf8("FINAL_CHAIN_AUTHORITY_v01"))` — a DOMAIN constant, not
/// a key digest (the chain has no PublicKeyBlock). Precomputed because the
/// mock SHA3 precompile under Foundry is deliberately not the real function;
/// pinned against `hashlib.sha3_256` and the dashboard's value by test.
/// Zero-length AuthorityKeyId stays reserved for the retired genesis root
/// alone and is admitted nowhere.
bytes32 constant CHAIN_AUTHORITY_KEY_ID =
0x9a6a5d8139ad2d28957698330aaa691017dba7dc80eb7cbec585239fb680bbab;
/// @notice The identity tree's projection door on `FinalStateTrees`. 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. Same pattern as `IChainSource` on the trees side.
interface IIdentityLeafSink {
function syncIdentityLeaves(address[] calldata accounts) external;
}
/// @notice `FinalRevocationLog`'s recording door, same narrow-interface
/// reasoning. `recorded` is read first so a fingerprint someone already
/// recorded permissionlessly cannot revert the registry mutation feeding it.
interface IRevocationRecorder {
function record(bytes32 signerId) external;
function recorded(bytes32 signerId) external view returns (bool);
}
contract FinalIdentityRegistry {
// ---------------------------------------------------------------- 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
/// @dev One per membership mutation, so an approval to grant a role can
/// never be replayed as one to revoke. The registry is its own verifying
/// contract for these.
bytes32 public constant DOMAIN_REGISTER_WALLET = keccak256("FINAL_REGISTRY_REGISTER_WALLET_v01");
bytes32 public constant DOMAIN_REGISTER_ISSUER = keccak256("FINAL_REGISTRY_REGISTER_ISSUER_v01");
/// @notice The admission proof-of-possession digest domain (schema §v5).
/// 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.
bytes32 public constant DOMAIN_IDENTITY_ADMISSION = keccak256("FINAL_IDENTITY_ADMISSION_v01");
/// @notice Root-plane global certificate revocation (D5).
bytes32 public constant DOMAIN_REVOKE_CERTIFICATE =
keccak256("FINAL_REGISTRY_REVOKE_CERTIFICATE_v01");
/// @notice The ISSUING identity's certificate-revocation digest domain.
bytes32 public constant DOMAIN_ISSUER_CERT_REVOCATION =
keccak256("FINAL_ISSUER_CERT_REVOCATION_v01");
bytes32 public constant DOMAIN_REGISTER_LMS_KEY = keccak256("FINAL_REGISTRY_REGISTER_LMS_KEY_v01");
bytes32 public constant DOMAIN_SET_ROLES = keccak256("FINAL_REGISTRY_SET_ROLES_v01");
bytes32 public constant DOMAIN_REVOKE = keccak256("FINAL_REGISTRY_REVOKE_v01");
bytes32 public constant DOMAIN_SET_REGISTRAR_THRESHOLD =
keccak256("FINAL_REGISTRY_SET_REGISTRAR_THRESHOLD_v01");
/// @dev The algorithm id the sender derivation is domain-separated by:
/// ML-DSA-87, FIPS 204, the only algorithm the transaction envelope admits.
uint8 private constant ENVELOPE_ALG_ML_DSA_87 = 4;
// ------------------------------------------------------------- storage
/**
* @notice One party's on-chain identity.
* @dev `version` increments on every mutation and is what a rotation is:
* the record is replaced, not appended to, and the version is how a reader
* on another chain knows which of two copies it saw 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;
/// Seconds since epoch. The schema's TBS is nanoseconds; the conversion
/// happens off chain because block timestamps are seconds and a
/// comparison across units is a bug waiting for a leap.
/// @dev MILLISECONDS — this chain's clock. See `FinalChainTime`.
uint64 notBefore;
/// Seconds since epoch, or 0 for "never expires" — which the schema
/// allows and personal identity certificates use.
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;
}
/**
* @notice A hash-based (LMS) signing key held by a registered account.
*
* The protocol plane's quorums verify LMS, not ML-DSA: an execution chain
* has no PQ precompiles, so `FinalRootAuthority` checks a keccak hash loop
* instead (`arch/hash-based-authority.md`). Those keys are the authority
* over `masterRoot`, and therefore over PQ execution — which makes "who
* holds signer 0x39bb…?" 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 LMS signing key for an account, if it holds one.
/// @dev One slot per (account, chain) — LMS-01. `nextLeaf` on an
/// authority is a complete single-use counter 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.
mapping(address account => mapping(uint64 chainId => LmsKey)) private _lmsKey;
/// @notice Which account a signer fingerprint belongs to. This is the
/// lookup the whole record exists for: a gateway roster names fingerprints
/// and nothing else, so without it the keys are unattributable.
/// @dev What a fingerprint is bound to: the account that holds it and the
/// chain it signs for — one slot, written once at registration and left in
/// place when superseded (attribution is history). The chain names the
/// (account, chain) slot `lmsSignerIsLive` resolves against.
// NOTE: this contract sits ~13 bytes under EIP-170 (24,563 of 24,576 at
// the pinned optimizer settings). The next feature here pays for itself
// in bytecode first — see the LMS-binding merge and the off-chain
// zero-chain check for what that looks like.
struct LmsBinding {
address account;
uint64 chainId;
}
mapping(bytes32 signerId => LmsBinding) private _lmsBinding;
/// @notice The identity record for an account.
mapping(address account => Identity) private _identity;
/// The four slots, verbatim. All four are stored in full because the
/// precompiles verify against a KEY, not a commitment — and a key that
/// arrived in calldata proves nothing about who signed.
///
/// A CA has two keys, not 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;
mapping(address account => bytes) private _activeAccessKey;
mapping(address account => bytes) private _recoveryTransactionKey;
mapping(address account => bytes) private _recoveryAccessKey;
/// @notice The seal key — a service's second SLH-DSA-SHAKE-256s key, which
/// co-signs execution-class quorum decisions. Empty for every identity
/// whose certificate carries no `PURPOSE_ACTIVE_SEAL` entry: users, CAs.
mapping(address account => bytes) private _activeSealKey;
/// @notice Encapsulation keys, per stage. Two algorithms each — ML-KEM-1024
/// (lattice) and HQC-5 (code-based) — so a break in either family leaves the
/// other standing, the same reasoning that pairs ML-DSA with SLH-DSA above.
/// @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;
mapping(address account => bytes) private _activeKemHqc;
mapping(address account => bytes) private _recoveryKemMlKem;
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
event IdentityRegistered(
address indexed account, bytes32 indexed certHash, uint256 roles, uint64 version
);
event IdentityRolesChanged(address indexed account, uint256 previousRoles, uint256 newRoles);
event LmsKeyRegistered(
address indexed account,
bytes32 indexed signerId,
uint64 indexed chainId,
bytes16 keyId,
uint8 height,
bytes32 root,
uint64 version
);
event IdentityRevoked(address indexed account, bytes32 indexed certHash);
/// @notice One revocation-lane entry: `revoker` is `address(0)` for the
/// root plane, the issuing identity otherwise.
event CertificateRevoked(bytes32 indexed certHash, address indexed revoker);
event BootstrapSealed(address indexed sealedBy);
/// @notice The one-shot state-plane wiring landed.
event StatePlaneWired(address stateTrees, address revocationLog);
event RegistrarThresholdSet(uint256 threshold);
/// @notice A registrar quorum authorized an action. `nonce` is the value
/// the approvals were made over; the next action needs the next one.
event RegistrarQuorumApproved(
address indexed verifyingContract, bytes32 indexed actionDomain, uint64 nonce, uint256 valid
);
// -------------------------------------------------------------- errors
error NotAuthorized(address caller);
error BootstrapAlreadySealed();
error UnknownAccount(address account);
/// @notice A certificate's encapsulation key failed the chain's own
/// well-formedness check. Names the algorithm, because the pair is stored
/// together and "one of these two" is not an actionable answer.
error MalformedEncapsulationKey(address account, uint16 algorithmId);
error CertificateAlreadyBound(bytes32 certHash, address boundTo);
error CertificateIsRevoked(bytes32 certHash);
error VersionNotNewer(uint64 current, uint64 offered);
error IssuerNotACertificateAuthority(address issuer);
error IssuerMayNotSign(address issuer, uint8 depth, uint8 maxDelegationDepth);
error WrongDepth(uint8 got, uint8 want);
error DelegationWidened(uint8 child, uint8 issuer);
error AuthorityKeyIdMismatch(bytes32 got, bytes32 want);
error StagesDisagree(bytes32 liveSerial, bytes32 recoverySerial);
/// @notice `height` outside 1..24. See `FinalLms.MAX_HEIGHT`.
error LmsHeightOutOfRange(uint8 height);
/// @notice A zero root commits to no tree.
error LmsRootIsZero();
/// @notice This fingerprint already belongs to a different account.
error LmsKeyAlreadyBound(bytes32 signerId, address boundTo);
/// @notice Two identities cannot share a transaction key: the sender it
/// derives would be attributable to both.
error SenderAlreadyBound(address sender, address boundTo);
/// @notice Fewer registrars able to seal than the threshold asks for.
error RegistrarThresholdUnreachable(uint256 sealable, uint256 threshold);
error RegistrarThresholdIsZero();
/// @notice {wireStatePlane} ran already, or was handed a zero address.
error StatePlaneAlreadyWired();
error ZeroStatePlane();
/// @notice The holder's admission proof of possession did not verify —
/// one family failed, or the digest was built over the wrong nonce.
error AdmissionProofInvalid(address account);
/// @notice The certificate does not carry the ruled chain-issuer
/// AuthorityKeyId — it is not a chain-attested certificate.
error NotChainAttested(bytes32 authorityKeyId);
/// @notice The certificate's IssuerDN is not the ruled constant.
error WrongIssuerDn(bytes32 issuerDnHash);
/// @notice A chain-attested end entity sits at depth 1 with
/// `maxDelegationDepth == depth`; anything else is not an end entity.
error NotAnEndEntity(uint8 depth, uint8 maxDelegationDepth);
/// @notice An issuer that cannot sign is an end entity wearing a profile.
error IssuerCannotSign(uint8 depth, uint8 maxDelegationDepth);
/// @notice Third-party issuers carry a real `NotAfter` (ruling 3) —
/// expiry is the passive half of their lifecycle.
error IssuerMustExpire();
/// @notice An issuer validity window past the ~2-year ceiling (ruling 3).
error IssuerValidityTooLong(uint64 notBefore, uint64 notAfter);
/// @notice An institution registration without a real ISO 3166 `C=` in
/// its subject DN, or with a jurisdiction that does not match its
/// Institution extension. Only the trust root is jurisdiction-silent.
error JurisdictionMissing();
error JurisdictionMismatch();
// --------------------------------------------------------- constructor
/**
* @param admin The bootstrap registrar. Genesis names the chain deployer.
* @dev The precompile probe is the point of the constructor. This contract
* is meaningless on a chain that cannot verify PQ signatures, and deploying
* it there would produce a registry full of keys nothing can check.
*/
constructor(address admin) {
FinalChainPrecompiles.assertAvailable();
bootstrapAdmin = admin;
}
// ----------------------------------------------------------- authority
/**
* @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
* pretending otherwise produced the one roster that could not be
* bootstrapped in `FinalRootAuthority`. It is closed by
* `sealBootstrap`, which is irreversible.
*
* While it 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.
*/
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 state-plane contracts.
* @dev `msg.sender` — the calling contract — is the verifying contract the
* digest binds and the counter it burns, so an approval collected for the
* trees' configuration cannot be spent on the bundle log'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 the quorum.
*/
function requireRegistrarQuorum(
bytes32 actionDomain,
bytes32 payloadDigest,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
_requireRegistrarQuorum(msg.sender, actionDomain, payloadDigest, anchorBlock, approvals);
}
/// @dev The digest is `FinalPqQuorum.digest(verifyingContract, actionDomain,
/// anchorBlock, keccak256(abi.encode(nonce, payloadDigest)))`; the seal is
/// required — membership is the hybrid class.
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 Bootstrap admin while the window is open; the current registrar
* quorum afterwards, so a registrar set that grows or shrinks can move it.
* Refuses a threshold the sealable registrars cannot meet, and refuses zero:
* both are a registry that can never be written to again.
*/
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.
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`. Restated rather
* than imported because the two live on different chains and there is no
* import that 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.
*/
function lmsSignerId(bytes16 keyId, uint8 height, bytes32 root) public pure returns (bytes32) {
return keccak256(abi.encode(keyId, height, root));
}
/**
* @notice Record the LMS signing key an already-registered account holds.
* @dev Membership-gated, same as 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-per-key-they-hold.
*
* @param account Must already be registered and not revoked.
* @param version Strictly increasing. 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; see
* `FinalPqQuorum`. 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, chain) — LMS-01 made the same
// operator a different signer on every chain, so chain B starting at
// version 1 says nothing about chain A 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.
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. Zeroes for a fingerprint never registered.
/// @dev The revocation log's permanence gate reads this to find the
/// (account, chain) SLOT a fingerprint belongs to — the slot's current key
/// is what separates a superseded fingerprint (permanent, recordable) from
/// a merely lapsed one (expiry, temporary, refused). Attribution is
/// history: the binding survives supersession, exactly as the mapping
/// behind {lmsSignerIsLive} does, because it IS that mapping.
function lmsBindingOf(bytes32 signerId) external view returns (address account, uint64 chainId) {
LmsBinding storage binding = _lmsBinding[signerId];
return (binding.account, binding.chainId);
}
/**
* @notice Is this signer fingerprint held by a live, unrevoked account?
* @dev The question a verifier actually has. A gateway roster names
* fingerprints and nothing else, so "is 0x39bb… still good?" is otherwise
* unanswerable from the state plane.
*/
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. The
/// count is of registrars that can SEAL — a certificate authority carrying
/// the role has no seal key and can never contribute an approval.
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 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 — both contracts take THIS registry as one — so the deploy
* tooling calls it in the same nonce-fixed block that deploys them, before
* any identity is registered.
*/
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_);
}
/// @dev Project `account`'s tree-8 leaf, same-tx. Skipped while the plane
/// is unwired — the 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 besides forgetting to call it.
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);
}
/// @dev Record a PERMANENTLY retired fingerprint, same-tx, unless the log
/// is unwired or someone already recorded it permissionlessly.
function _recordRevokedSigner(bytes32 signerId) private {
address log = revocationLog;
if (log == address(0)) return;
if (IRevocationRecorder(log).recorded(signerId)) return;
IRevocationRecorder(log).record(signerId);
}
// -------------------------------------------------------- registration
/// @notice The holder's admission proof of possession: both live-stage
/// families over the admission digest (schema §v5). There is no root
/// keypair and no CA signature any more — the chain admits, and the
/// "2 signatures at creation" are the HOLDER's, verified by the
/// precompiles inside this very transaction.
struct AdmissionProof {
bytes mlDsaSignature;
bytes slhDsaSignature;
}
/**
* @notice Register or rotate a Final Wallet identity from its two public
* certificates — CHAIN-ATTESTED (schema §v5, ruled 2026-09-01).
*
* @param account The wallet address the certificate set derives.
* @param liveTbs `live.pub.fcert` TBS — `activeTransaction` + `activeAccess`.
* @param recoveryTbs `recovery.pub.fcert` TBS — 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), verified in the precompiles inside this
* transaction. This replaced the CA signature: issuance authority
* is the registrar quorum, possession is this proof, and there is
* no root keypair anywhere.
* @param roles Capability bitmask. The one thing the certificates do not
* say, because capability is this system's decision.
* @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.
*
* @dev **Both stages, together.** A wallet has four keys in two stages and
* the recovery pair is PRE-COMMITTED — written at `initialize` from the same
* certificate set that determined the address, which is why PQ migration
* takes no key arguments. The two must share a `SerialNumber`: a serial is
* per certificate SET, so two stages disagreeing are two different wallets.
*
* **Chain-attested means pinned, per stage:** the ruled IssuerDN and
* AuthorityKeyId constants, depth exactly 1 (directly under the chain),
* and `maxDelegationDepth == depth` (an end entity signs nothing — the
* same immutable pair `identityTreeLeafOf` discriminates records 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 our own
* intermediate) that signs certificates OFF-chain with the keys
* registered here (D2: the superCA).
*
* @param account The issuer's account on this chain.
* @param tbs The single issuer certificate's TBS: two CERT_SIGNING keys
* (ML-DSA-87 + SLH-DSA-SHAKE-256s), 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;
* `address(0)` for an issuer hanging directly under the chain.
* @param proof The issuer's OWN two cert-signing keys over the admission
* digest (`recoveryCertHash` slot is zero — there is no recovery
* stage to bind).
*
* @dev Admission is chain-native like any identity: registrar quorum plus
* the holder's PoP. What the v4 delegation rules said survives verbatim as
* LINEAGE — a nested issuer's depth, delegation bound and AuthorityKeyId
* must chain to its registered parent — but no parent SIGNS anything; the
* chain's admission is the issuance.
*
* Ruling 3: a registered issuer always expires (`NotAfter` real, window
* bounded ~2 years) — the passive liveness touchpoint; renewal re-issues
* under the same registered keys with a version bump.
*
* The jurisdiction rule (ruled 2026-09-01, amended): only the trust root
* is jurisdiction-silent. An institution MUST carry its real ISO 3166
* `C=` in its subject DN, matching the `jurisdiction` field of its
* `0x0102` Institution extension — CA/Browser-Forum practice, enforced at
* the door because a verifier's legal recourse starts with knowing where
* an issuer answers for itself.
*/
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 Ruling 3's validity ceiling for registered issuers, in this
/// chain's milliseconds: two 366-day years.
uint64 public constant MAX_ISSUER_VALIDITY_MS = 2 * 366 days * 1000;
/// @dev The chain-attested end-entity pins, run once per 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);
}
}
/// @dev The v4 delegation rules, surviving as lineage: a nested issuer
/// chains to a registered, signing-capable parent one level up; a direct
/// issuer hangs under the chain at depth 1.
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);
}
}
/// @dev The jurisdiction rule: a real ISO 3166 alpha-2 `C=` in the subject
/// DN, equal to the Institution extension's `jurisdiction` field. The DN
/// is canonical comma-separated form, so `C=` matches at the start or
/// right after a comma; the component value is exactly two bytes.
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();
}
/// @dev Verify the holder's PoP: both live-stage families over the
/// admission digest, in the precompiles, inside this transaction. Burns
/// the gate nonce on the bootstrap path (the quorum path burned it in
/// `_requireRegistrarQuorum` already), so an admission is one-shot in
/// both regimes.
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;
}
}
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);
}
/**
* @dev Store one stage's encapsulation pair, or clear it.
*
* Empty is legitimate and is not the same as absent-and-wrong: a CA has no
* encapsulation stage, and a certificate issued before v4 carries none.
* `FinalCertificate.parse` 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 intent then never decrypts — the failure
* mode with no error attached, and the one this whole pairing exists to
* avoid.
*/
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.
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);
}
/// @dev Once sealed, no mutation may leave the registrar quorum unreachable
/// — that is the one change nothing could ever undo. Checked after the
/// write so the count reflects it.
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.
/// @param chainIds The chains whose LMS-key slots this account holds — the
/// registrars supply the list (the digest binds it) because a mapping
/// cannot enumerate its own keys. Each named slot's current fingerprint is
/// recorded into the revocation log same-tx; a chain with no slot is
/// skipped, and a fingerprint missed by an incomplete list stays
/// permanently recordable through the log's permissionless door, since a
/// revoked account never regains standing.
/// @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.
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` (D5).
*
* @dev The half of the one revocation lane that gates registration and
* covers break-glass: any certificate — registered, off-chain-issued, or
* never seen — can be killed by handle under the registrar quorum. When
* the handle is a registered identity's CURRENT certificate the identity
* falls with it (flag, roles, same-tx projection), so a break-glass by
* handle is never weaker than {revoke} — it only skips the LMS-slot
* enumeration, which stays permanently recordable through the revocation
* log's permissionless door.
*/
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 "Sub-issuer and us alike" (D5) — but SCOPED: this records WHO
* revoked, and a verifier honours the entry only when the revoker is the
* certificate's own issuer (which the verifier knows — it holds the
* cert). 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, the
* chain, the handle and the issuer's own gate nonce. 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 the lane models one.
*/
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. `registered` is the field to branch on.
function identityOf(address account) external view returns (Identity memory) {
return _identity[account];
}
/// @notice `activeTransaction` — ML-DSA-87. What a quorum verifies against.
function activeTransactionKeyOf(address account) external view returns (bytes memory) {
return _activeTransactionKey[account];
}
/// @notice `activeAccess` — SLH-DSA-SHAKE-256s. Identity, and guardianship.
function activeAccessKeyOf(address account) external view returns (bytes memory) {
return _activeAccessKey[account];
}
/// @notice `activeSeal` — SLH-DSA-SHAKE-256s. What `FinalPqQuorum` verifies
/// an execution-class approval's `seal` against. Empty when the identity
/// carries no seal, in which case it cannot take part in a sealed quorum.
function activeSealKeyOf(address account) external view returns (bytes memory) {
return _activeSealKey[account];
}
/// @notice `recoveryTransaction`. Authorizes rotating this account's own
/// credentials and nothing else. Empty for a CA.
function recoveryTransactionKeyOf(address account) external view returns (bytes memory) {
return _recoveryTransactionKey[account];
}
/// @notice `recoveryAccess`. Empty for a CA.
function recoveryAccessKeyOf(address account) external view returns (bytes memory) {
return _recoveryAccessKey[account];
}
/// @notice The four commitments, in the order tree 1's leaf wants them.
/// @dev keccak, not SHA3 — these feed `FinalWalletFactory.accountStateLeafHash`,
/// which every other chain verifies with, and that one hashes with keccak.
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 the wallet's CREATE2 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 (`minePqVanityCerts.cjs` is the reference encoder;
* the parity test pins this function against the premined fixtures).
*
* 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
* CA. The CA exclusion is structural, not a role read: an end entity has
* `depth == maxDelegationDepth` (it issues nothing), a CA never does, and
* the depth pair is immutable per version where roles are not.
*
* Lives HERE rather than on `FinalStateTrees` (whose tree 8 consumes it)
* because every input is this contract's storage and the trees contract
* sits against EIP-170.
*/
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) {
// D7 (ruled 2026-09-01): an ISSUER exists in tree 8 under its own
// domain, so its record is stapleable for offline licence
// verification. `certHash` suffices (it covers the whole TBS and
// the verifier holds the cert), `version` makes supersession move
// the leaf, and the third word RESERVES the issuer's own
// certificate-tree anchor — zero until wired. The distinct domain
// does the wallet-admission exclusion the zero projection used to
// do; zero-on-revoke above is now 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 `AccountStateLeaf` order.
/// @dev One word per STAGE, over both of that stage's KEM public keys. The
/// pair is the unit — an account holds both or neither — so committing 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 registered before the encapsulation slots existed hashes the
/// empty string here rather than reverting: `syncIdentities` must keep
/// projecting it, and a leaf that cannot be built is a party that cannot be
/// revoked.
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 encapsulation keys themselves, for a party composing a message.
function kemKeysOf(address account)
external
view
returns (bytes memory activeMlKem, bytes memory activeHqc)
{
return (_activeKemMlKem[account], _activeKemHqc[account]);
}
// ------------------------------------------------------------- senders
/**
* @notice The Final Chain sender a transaction key produces.
* @dev `keccak256(uint8(4) ‖ publicKey)[12:]` — byte-identical to what the
* node derives from a type-0x46 envelope and to the backend's
* `pqTransaction.senderOf`. Pure, so a client can compute it from a
* certificate before the identity is registered.
*/
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, 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. False for a sender no identity claims.
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.
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.
function accountCount() external view returns (uint256) {
return _accounts.length;
}
/// @notice Registered account by index, in registration order.
function accountAt(uint256 index) external view returns (address) {
return _accounts[index];
}
/// @notice Every account carrying every bit in `roleMask`.
/// @dev A view, so the O(n) scan costs nothing. Callers that need this in a
/// transaction should pass the member list explicitly instead — see
/// `FinalPqQuorum`, which takes signers rather than searching for them.
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 that reverts forever with nothing naming the
* roster as the cause.
*/
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 should not be a universal pass.
*/
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.
function isActive(address account) public view returns (bool) {
Identity storage id = _identity[account];
return id.registered && !id.revoked && _withinValidity(id);
}
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;
}
}
contracts/finalchain/FinalIntentLog.sol
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.20;
import {FinalChainPrecompiles} from "./FinalChainPrecompiles.sol";
import {FinalChainTime} from "./FinalChainTime.sol";
import {FinalIdentityRegistry} from "./FinalIdentityRegistry.sol";
import {FinalPqQuorum} from "./FinalPqQuorum.sol";
import {FinalStateTrees} from "./FinalStateTrees.sol";
/**
* @title FinalIntentLog
* @notice Every intent is posted here before it may be anchored. Ordering is
* block order.
*
* @dev ## Why this is a log and not a tree
*
* Final Chain carries six trees, and they publish in ROUNDS — every root
* together, one version bump each. That structure is for state that changes
* slowly and must be proven elsewhere. An intent is neither: it is high
* cadence, it lives 180 seconds, and putting it in a round-published tree would
* bump a version on every transaction and age every outstanding proof — the
* same objection that kept a liveness timestamp out of the tree-1 account leaf.
*
* It is not an MMR either. `FinalBundleLog`'s payload already IS the bundle's
* intent-Merkle root, so a second append-only log over the same leaves would
* prove, one level earlier, a fact the bundle anchor already proves.
*
* What an intent actually needs is ordering, public availability, and a
* presence check the anchoring quorum can run objectively. Block order gives
* the first, calldata gives the second, and a mapping gives the third. That is
* the whole contract.
*
* ## What it buys
*
* `FinalBundleLog.append` consumes from here, so a bundle cannot be anchored
* unless every intent in it was posted first — and the execution chains'
* authorization is that anchor. Final-Chain-first admission stops being a
* quorum policy and becomes bytecode. Replay protection falls out of the same
* mapping: a leaf is consumed exactly once, ever.
*
* For the user it closes substitution. The intent they composed is ordered
* publicly before anything executes, so a relayer or a frontend cannot swap it
* for another — the anchor will only carry what this log holds.
*
* ## The body is encrypted and this contract cannot read it
*
* `header` is routing metadata: a chain reference, a deadline, KEM ciphertexts,
* a commitment to the body, and the intent leaf. Account, destination, fee and
* calldata are all inside `ciphertext`. That is what closes content-based
* front-running — an operator watching this log sees a chain id and a deadline
* and cannot build a profitable order from either.
*
* There is deliberately no decryption path on chain and no AEAD precompile. The
* chain checks a hash and nothing else.
*
* ## The envelope is STORED, not left in the event log
*
* A two-KEM envelope is ~16 KB and keeping it in state is not free. It is kept
* anyway, for the reason `FinalBundleLog` keeps its payloads: this chain caps a
* `getLogs` range at 100,000 blocks and mints one every 100 ms, so the log is
* reachable for **2.8 hours**. A forced-path intent lives **48**.
*
* An event-only envelope would therefore be unreadable long before it expired —
* precisely for the guardians who are meant to review a forced intent, and
* precisely in the window the forced path exists to cover. That is not a
* degraded read, it is the escape hatch not working.
*
* So the event is for DISCOVERY and carries no bytes; state is for retrieval,
* over one `eth_call` and no range. Emitting the envelope as well would be
* ~128k gas of duplication for data the same transaction already stored.
*
* Measured in `FinalIntentLog.t.sol`: **11,517,049 gas** for a 16,426-byte
* envelope, which at a 60M block gas limit is **5 intents per block** — 50 a
* second at 100 ms. That is the throughput ceiling on this path, and it is
* dominated by HQC-5's 14,421-byte ciphertext.
*
* Two levers if it ever binds. SSTORE2-style data contracts are ~3x cheaper per
* byte, and would need chunking because a two-recipient envelope exceeds
* EIP-170's 24,576. Pruning consumed envelopes is the other, and it trades the
* record for the space — a decision, not a cleanup.
*
* **The bond becomes load-bearing when the RPC opens, not before.** Posting is
* permissionless in this contract and the base fee is 1 wei, so the only thing
* standing between an attacker and 60M gas a block of permanent state is that
* Final Chain's RPC is fronted by a default-deny Cloud Armor allowlist and
* reaches operators only. That is a perimeter, not a mechanism, and it is the
* one that has to be removed for users to post their own intents. Nothing here
* rate-limits; the bond is what is supposed to, and this is why it cannot ship
* after the RPC does.
*
* ## Approve and cancel, without naming an account
*
* An approval-gated intent executes only after a second on-chain act, made
* after the user decrypts the posted envelope and reads what the CHAIN holds —
* the read-back that closes execution of what the user has not seen. Who may
* approve is a **per-intent, wallet-generated ML-DSA-87 key**, committed in the
* plaintext header: the commitment names a key, never an account, which is what
* answers "a per-intent veto has to say who may veto, and the account is
* encrypted". A substituted commitment strands the intent — the execution
* signature bound into the leaf still authorizes every field — and cancel is
* the same key, any time before consumption.
*
* `approve` also DELIVERS the executor section: the content key and the
* account's KEM public keys, sealed to the executor set. Before approval the
* executor holds ciphertext it cannot open; that is cryptography, not policy.
* An AUTO_APPROVE posting (header flag, bit 0) skips the gate for automation
* and scheduling and carries its executor section at posting — flipping the
* flag skips the second look and can never change what executes.
*
* The account-wide veto stays `FinalAccountLedger`'s freeze; auto-approved
* intents have no per-intent cancel because they commit to no approval key.
*
* ## Tree 7 is the search structure; the MMR is the record
*
* Every transition — posted, approved, cancelled, consumed — is mirrored into
* `FinalStateTrees.TREE_INTENTS` through `treeWriter[7]`, keyed by a RING over
* the posting sequence (`seq mod 2^20`), so slots recycle and the tree is an
* index with a ~1M-posting retention window rather than a permanent record.
* Backends enumerate it instead of scanning logs; proofs against it are proofs
* about the current window.
*/
contract FinalIntentLog {
/// @notice Where roles are resolved. Immutable.
///
/// @dev The only thing this log needs a registry for is authorizing
/// `setConsumer`, and that could have been a deployer check. It is the
/// registry instead so the wiring falls under the SAME bootstrap window
/// as `FinalStateTrees.setTreeWriter` and `FinalBundleLog.configure` —
/// one rule for who may wire Final Chain together, rather than three.
FinalIdentityRegistry public immutable registry;
/// @notice Where every status transition is mirrored (tree 7). Immutable:
/// a movable index would be an index whose history can be swapped.
FinalStateTrees public immutable trees;
/// @dev Header layout, fixed offsets. Must match `encodeHeader` in
/// `FinalBackend/src/intents/intentEnvelope.js` — the cross-repo parity
/// suite pins it. Length-prefixed and canonical precisely so these
/// offsets exist: a JSON header would have no stable position to read.
///
/// 0 version uint8 = 2 | 3 (v3 salts recipientsHash; offsets identical)
/// 1 flags uint8 bit0 AUTO_APPROVE; rest zero
/// 2..33 targetChainRef bytes32
/// 34..41 executeNotBefore uint64 ms; 0 = immediate
/// 42..49 deadline uint64 ms
/// 50..81 bodyCommitment bytes32
/// 82..113 intentLeaf bytes32
/// 114..145 approvalKeyCommit bytes32 zero iff AUTO_APPROVE
/// 146..147 kemSet uint16
/// 148..149 kemKeyVersion uint16
/// 150..181 recipientsHash bytes32
/// 182.. recipients, executorSection, bond
uint256 private constant OFF_FLAGS = 1;
uint256 private constant OFF_TARGET_CHAIN_REF = 2;
uint256 private constant OFF_EXECUTE_NOT_BEFORE = 34;
uint256 private constant OFF_DEADLINE = 42;
uint256 private constant OFF_BODY_COMMITMENT = 50;
uint256 private constant OFF_INTENT_LEAF = 82;
uint256 private constant OFF_APPROVAL_KEY_COMMIT = 114;
/// @dev The whole fixed prefix, including `recipientsHash` — which this
/// contract never reads. Requiring it anyway is the cheap half of
/// "well-formed": a header truncated after the leaf would parse
/// perfectly here and then fail to decrypt for its recipient. The
/// contract reads nothing past offset 145.
uint256 private constant HEADER_MIN_BYTES = 182;
/// @notice The oldest envelope wire version this contract reads.
uint8 public constant HEADER_VERSION = 2;
/// @notice The newest. v3 (B6) keeps every offset and salts `recipientsHash`
/// — a field this contract never reads — with a holder-derived value,
/// so a wallet's postings stop sharing a fingerprint in the clear.
/// Both versions parse identically here; the header's own AAD is what
/// tells them apart for the recipients.
uint8 public constant HEADER_VERSION_MAX = 3;
/// @notice Header flag bit 0: skip the approval gate.
/// @dev Poster-controlled routing, honestly scoped: flipping it skips the
/// user's second look and can never change what executes — the user's own
/// signature over the intent fields, bound into the leaf, remains the only
/// execution authorization.
uint8 public constant FLAG_AUTO_APPROVE = 0x01;
// ---------------------------------------------------- approval domains
/// @dev Distinct per action, so an approval can never be replayed as a
/// cancellation or vice versa. Both digests bind this chain and this log.
bytes32 public constant DOMAIN_INTENT_APPROVE = keccak256("FINAL_INTENT_APPROVE_v01");
bytes32 public constant DOMAIN_INTENT_CANCEL = keccak256("FINAL_INTENT_CANCEL_v01");
// ------------------------------------------------------ tree-7 mirror
/// @dev The ring key's domain. Named in `FinalStateTrees`' key.* family
/// because that is the space it lives in; computed HERE because the log is
/// the writer and the backend mirrors this function, not the tree.
bytes32 public constant DOMAIN_INTENT_KEY = keccak256("FinalStateTrees.key.intent.v01");
/// @dev The status leaf's domain.
bytes32 public constant DOMAIN_INTENT_STATUS_LEAF = keccak256("FINAL_INTENT_STATUS_LEAF_v01");
/// @notice Ring size: keys recycle at `CAPACITY`, so the tree can never
/// fill however long the chain runs. Pinned equal to
/// `FinalStateTrees.CAPACITY` by test.
uint64 public constant INTENT_SLOT_RING = uint64(1) << 20;
/// @notice The tree this log writes. Restated from `FinalStateTrees`
/// because a contract-type constant is not reachable here; pinned equal to
/// `trees.TREE_INTENTS()` by test.
uint8 public constant TREE_INTENTS_ID = 7;
/// @notice The branch the intent ring lives in — `FinalStateTrees.BRANCH_MAIN`,
/// pinned by test. Branch 0 of tree 7 is the log's configuration.
uint8 public constant BRANCH_MAIN_ID = 1;
/// @notice Tree-7 status values.
uint8 public constant STATUS_POSTED = 1;
uint8 public constant STATUS_APPROVED = 2;
uint8 public constant STATUS_CONSUMED = 3;
uint8 public constant STATUS_CANCELLED = 4;
/// @dev Registrar-quorum actions, verified by the registry with this log as
/// the verifying contract.
bytes32 public constant ACTION_SET_CONSUMER = keccak256("FINAL_INTENT_LOG_SET_CONSUMER_v01");
bytes32 public constant ACTION_SEED_SEQUENCE = keccak256("FINAL_INTENT_LOG_SEED_SEQUENCE_v01");
bytes32 public constant ACTION_SET_BOND_POLICY = keccak256("FINAL_INTENT_LOG_SET_BOND_POLICY_v01");
/**
* @notice The longest an intent may stay consumable past its earliest
* execution moment.
*
* @dev A cap rather than an exact value because every lane shares this log,
* and an uncapped deadline would let a posting sit consumable forever —
* which is a replay window dressed as a long-lived intent. An immediate
* intent (`executeNotBefore == 0`) gets exactly this from `now`; a
* scheduled one gets it from its own start.
*/
/// @dev MILLISECONDS, like every duration on this chain. Its predecessor
/// was once 48 hours read against a millisecond clock — 48 SECONDS —
/// so a header whose deadline a client computed from wall time was
/// refused before it could ever be posted.
uint64 public constant EXECUTION_WINDOW = 48 hours * FinalChainTime.MS_PER_SECOND;
/// @notice How far ahead `executeNotBefore` may sit. The scheduling
/// horizon: chosen at signing, because the deadline is inside the signed
/// intent and cannot be extended afterwards.
uint64 public constant MAX_SCHEDULE_HORIZON = 30 days * FinalChainTime.MS_PER_SECOND;
/// @notice How far ahead of `executeNotBefore` a scheduled posting may be
/// CONSUMED when its target chain has no lead of its own (W1, ruled
/// 2026-09-03: 90 seconds, per target chain). Admission — the
/// co-signer round and the append that consumes the leaf — runs
/// before T, so the fleet can compose and broadcast the wrapper to
/// land in the first target-chain block at or after T rather than
/// minutes late. The holder's veto (`cancel`) closes at consume,
/// i.e. no earlier than T − lead: the lead is the last call for a
/// cancel. An immediate posting (`executeNotBefore == 0`) has no
/// lead. Per chain: a row in tree 7's CONFIGURATION branch
/// (`FinalStateTrees.setConfig`, key `configKey(CONFIG_SCHEDULE_LEAD_MS,
/// targetChainRef)`) overrides this default (`scheduleLeadMsOf`),
/// explicit zero included — the first tenant of the config branch.
uint64 public constant DEFAULT_SCHEDULE_LEAD_MS = 90 * FinalChainTime.MS_PER_SECOND;
/// @notice The config-row NAME of a target chain's schedule lead, in
/// milliseconds: `trees.configKey(CONFIG_SCHEDULE_LEAD_MS, targetChainRef)`
/// → one word holding the lead. Written under the configuration
/// authority (bootstrap admin, then the registrar quorum) — an
/// operational parameter, not a trust boundary — and PROVABLE like
/// every other row of the plane, which a table here was not.
bytes32 public constant CONFIG_SCHEDULE_LEAD_MS = keccak256("FinalIntentLog.config.scheduleLeadMs.v01");
struct Posted {
/// @dev Zero means never posted. Non-zero and `consumedAt == 0` means open.
uint64 postedAt;
/// @dev From the header, in MILLISECONDS. Before this, `consume` refuses.
uint64 executeNotBefore;
/// @dev From the header, in MILLISECONDS. After this, `consume` refuses.
uint64 deadline;
/// @dev Block timestamp of the anchoring append. Non-zero means spent.
uint64 consumedAt;
/// @dev `keccak256(body)` — the leaf↔body binding. With the reveal
/// schema gone it is checked OFF-chain: the execution chain's
/// public calldata against this word.
bytes32 bodyCommitment;
/// @dev `keccak256` of the per-intent approval public key; zero iff
/// AUTO_APPROVE. Names a key, never an account.
bytes32 approvalKeyCommit;
/// @dev From the header; kept for the tree-7 status leaf.
bytes32 targetChainRef;
/// @dev When `approve` verified. Non-zero means approved.
uint64 approvedAt;
/// @dev When `cancel` verified. Non-zero means dead: `consume` refuses.
uint64 cancelledAt;
/// @dev Posting sequence — the tree-7 ring position (`seq mod 2^20`).
uint64 seq;
/// @dev Header flags, verbatim.
uint8 flags;
}
/// @dev The bytes, kept apart from `Posted` on purpose. `consume` runs on
/// the anchoring path and reads only the packed record; it must never
/// pay to walk 16 KB it does not look at.
struct Envelope {
/// @dev Canonical header, exactly as posted.
bytes header;
/// @dev The sealed body. This contract cannot read it and never tries.
bytes ciphertext;
/// @dev The content key and the account's KEM public keys, sealed to
/// the executor set. Delivered by `approve` — so before approval
/// the executor holds ciphertext it cannot open — or present from
/// posting on an AUTO_APPROVE intent. Opaque here: this contract
/// stores it and never parses it.
bytes executorSection;
}
/// @notice Keyed by the intent LEAF, not by an envelope id.
///
/// @dev The leaf is what `FinalBundleLog` folds and what the execution
/// chain's gateway recomputes, so keying on it makes the presence check
/// exact. It also makes duplicate protection land on the right thing: a
/// second header carrying a leaf already posted is the same intent
/// offered twice, and is refused.
mapping(bytes32 leaf => Posted) public postedOf;
/// @notice The full envelope, retrievable for as long as the chain exists.
mapping(bytes32 leaf => Envelope) private _envelopeOf;
/// @notice Postings ever made. The next intent takes this as its `seq`.
uint64 public postSeq;
/// @notice The leaf posted at sequence `seq` — the enumeration the backend
/// walks (`postSeq` is the count) instead of a `getLogs` range.
mapping(uint64 => bytes32) public leafAt;
// ─────────────────────────── the per-vertex bond ───────────────────────────
//
// **Encrypting the account removes attribution, and attribution is what
// rate-limiting runs on.** Posting is permissionless at a 1 wei base fee and
// state spam does not age out the way calldata spam does, so the thing
// holding it off today is the default-deny allowlist on this chain's RPC —
// a perimeter, and precisely the one that has to come down for users to post
// their own intents. A bond charges the spammer regardless of identity,
// needs no new cryptography, and is the natural unit for operators paid per
// unit of work.
//
// Refunded on valid execution, forfeited on a vertex that was never
// anchored. Both are PULL: `consume` runs inside the anchoring append and
// must not be able to fail, or be delayed, because of where a refund was
// going.
//
// **Not in the header.** The header's `<authenticator>` field stays a
// free-form reference for off-chain accounting and is deliberately not the
// bond itself. Moving the bond into the header would be an encoding change
// rippling through every reader and the cross-repo offset parity, and a
// bond is not worth one when native value keyed by leaf says the same
// thing.
//
// **Zero is the shipped state**, which is today's behaviour exactly. Arming
// it is the step that must precede opening the RPC, not follow it.
struct Bond {
/// @dev Who paid, and who a refund goes back to. Not the intent's
/// account — that is encrypted and this contract cannot know it — which
/// is the whole reason a bond works here and a per-account quota does
/// not.
address payer;
/// @dev Wei paid. Stored rather than re-read from `bondWei`, because the
/// rate can move between posting and settlement and a refund of
/// something other than what was paid is a fee nobody agreed to.
/// `uint88` covers 309 million ether and packs the struct into one slot.
uint88 amount;
/// @dev Paid out or forfeited. Set before the transfer.
bool settled;
}
/// @notice The bond posted with each intent, if any.
mapping(bytes32 leaf => Bond) public bondOf;
/// @notice What `post` requires. Zero disables the bond entirely.
uint256 public bondWei;
/// @notice Where a forfeited bond goes.
/// @dev Zero means BURN — the value stays in this contract and nothing can
/// move it. That is the safe default rather than an oversight: a forfeit
/// destination is a revenue stream, and one set by accident is worse than
/// one that does not exist. Arming a bond without naming a destination is
/// refused, so this can never be reached by forgetting.
address public bondForfeitTo;
/// @notice The bond rate, or its destination, changed.
event BondPolicySet(uint256 bondWei, address forfeitTo);
/// @notice A bond was posted alongside an intent.
event BondPosted(bytes32 indexed leaf, address indexed payer, uint256 amount);
/// @notice A bond was returned after the intent was anchored.
event BondRefunded(bytes32 indexed leaf, address indexed payer, uint256 amount);
/// @notice A bond was forfeited: the intent expired without being anchored.
event BondForfeited(bytes32 indexed leaf, address indexed payer, uint256 amount);
error BondMismatch(uint256 required, uint256 supplied);
error NoBond(bytes32 leaf);
error BondAlreadySettled(bytes32 leaf);
error BondNotRefundable(bytes32 leaf);
error BondNotForfeitable(bytes32 leaf, uint64 deadline);
error BondTransferFailed(address to, uint256 amount);
error BondNeedsAForfeitDestination();
/// @notice The one contract allowed to consume. Set once.
address public consumer;
/// @notice Discovery only. The envelope itself is in state — read it with
/// `envelopeOf`, which needs no `getLogs` range.
event IntentPosted(
bytes32 indexed leaf,
bytes32 indexed targetChainRef,
uint64 executeNotBefore,
uint64 deadline,
uint8 flags,
uint256 headerBytes,
uint256 ciphertextBytes
);
/// @notice The user read the posted intent back and approved it. Carries
/// the executor section's size — the bytes that let the executor
/// decrypt from here on.
event IntentApproved(bytes32 indexed leaf, uint256 executorSectionBytes);
/// @notice The approval key revoked the intent. Terminal.
event IntentCancelled(bytes32 indexed leaf);
/// @notice Consumed by an anchoring append.
event IntentConsumed(bytes32 indexed leaf, address indexed by);
event ConsumerSet(address indexed consumer);
/// @notice A target chain's schedule lead was configured (W1).
error HeaderTooShort(uint256 length);
error UnsupportedHeaderVersion(uint8 version);
error DeadlinePassed(uint64 deadline, uint64 nowMs);
error DeadlineTooFar(uint64 deadline, uint64 limit);
error ZeroLeaf();
error AlreadyPosted(bytes32 leaf);
error NotPosted(bytes32 leaf);
error AlreadyConsumed(bytes32 leaf, uint64 at);
error Expired(bytes32 leaf, uint64 deadline);
/// @notice Header flag bits beyond the ones this version defines.
error UnsupportedFlags(uint8 flags);
/// @notice An AUTO_APPROVE header must commit to no approval key.
error AutoApproveTakesNoCommitment(bytes32 approvalKeyCommit);
/// @notice An approval-gated header must commit to one.
error ApprovalKeyCommitRequired();
/// @notice `executeNotBefore` sits beyond the scheduling horizon.
error ScheduleTooFar(uint64 executeNotBefore, uint64 limit);
/// @notice The presented key does not hash to the header's commitment.
error WrongApprovalKey(bytes32 expected, bytes32 got);
/// @notice The signature did not verify under the committed key.
error ApprovalSignatureInvalid(bytes32 leaf);
error AlreadyApproved(bytes32 leaf, uint64 at);
/// @notice AUTO_APPROVE intents commit to no key: nothing to approve or
/// cancel per-intent. Their veto is the account-wide freeze.
error NoApprovalKey(bytes32 leaf);
error IntentIsCancelled(bytes32 leaf, uint64 at);
/// @notice `executeNotBefore` has not arrived. Scheduling is a chain rule.
error TooEarly(bytes32 leaf, uint64 executeNotBefore, uint64 nowMs);
/// @notice Neither approved nor AUTO_APPROVE: the user has not read it back.
error NotApproved(bytes32 leaf);
error NotConsumer(address caller);
error ConsumerUnset();
error ConsumerAlreadySet(address current);
error ZeroConsumer();
error NotAuthorized(address caller);
/**
* @dev No precompile of its own to call — this contract verifies no
* signature and reads no PQ key, it hashes and compares. The probe is
* still here because the claim "these deploy to Final Chain and nowhere
* else" should be enforced rather than documented, and a posting log on
* an execution chain would be a place intents could be posted that no
* anchor will ever read.
*/
constructor(FinalIdentityRegistry registry_, FinalStateTrees trees_) {
FinalChainPrecompiles.assertAvailable();
registry = registry_;
trees = trees_;
}
/// @notice A fresh log took over the previous log's sequence.
event SequenceSeeded(uint64 postSeq);
/// @notice The sequence can be seeded only into a log that holds nothing.
error NotFresh();
/**
* @notice Take over the previous log's `postSeq`, so every walker's cursor
* (`postSeq` is the count; the fleet walks it, never a `getLogs`
* range) stays ahead of nothing and behind everything new. Slots
* below the seed are the old log's. NO-WIPE redeploy, ruled
* 2026-09-03. Configuration authority; only while empty.
*/
function seedSequence(
uint64 postSeq_,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
_requireConfigurationAuthority(
ACTION_SEED_SEQUENCE, keccak256(abi.encode(postSeq_)), anchorBlock, approvals
);
if (postSeq != 0) revert NotFresh();
postSeq = postSeq_;
emit SequenceSeeded(postSeq_);
}
/**
* @dev The configuration gate: the registry's bootstrap admin alone while
* its window is open, the sealed `ROLE_REGISTRAR` quorum afterwards — the
* same window and quorum the registry and the trees use. `approvals` is
* empty during bootstrap.
*/
function _requireConfigurationAuthority(
bytes32 actionDomain,
bytes32 payloadDigest,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) private {
if (!registry.bootstrapSealed() && msg.sender == registry.bootstrapAdmin()) return;
registry.requireRegistrarQuorum(actionDomain, payloadDigest, anchorBlock, approvals);
}
/**
* @notice Name the contract allowed to consume postings.
*
* @dev One-way and one-shot, like `FinalStateTrees.treeWriter`. Deployment
* is circular — the bundle log needs this address in its constructor —
* so this is set afterwards, and it can never be moved: a consumer that
* could be repointed would be a consumer that could be replaced with
* one that does not check.
*
* Authorized, and it has to be. Left open it would be a front-run away
* from permanent: an attacker naming their own contract first would
* brick the PQ lane with no recovery, because one-shot cuts both ways.
*/
function setConsumer(
address newConsumer,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
_requireConfigurationAuthority(
ACTION_SET_CONSUMER, keccak256(abi.encode(newConsumer)), anchorBlock, approvals
);
if (consumer != address(0)) revert ConsumerAlreadySet(consumer);
if (newConsumer == address(0)) revert ZeroConsumer();
consumer = newConsumer;
emit ConsumerSet(newConsumer);
}
/// @notice The lead a scheduled posting for `targetChainRef` gets: the
/// config-branch row when one exists (explicit zero included),
/// else `DEFAULT_SCHEDULE_LEAD_MS`. Capped at the schedule horizon:
/// a longer lead would make every scheduled posting consumable at
/// post time. The fleet reads this so the watch and the contract
/// agree on when an attempt may begin.
function scheduleLeadMsOf(bytes32 targetChainRef) public view returns (uint64) {
(bytes32 value, bool present) =
trees.configValue(TREE_INTENTS_ID, trees.configKey(CONFIG_SCHEDULE_LEAD_MS, targetChainRef));
if (!present) return DEFAULT_SCHEDULE_LEAD_MS;
uint256 lead = uint256(value);
return lead > MAX_SCHEDULE_HORIZON ? MAX_SCHEDULE_HORIZON : uint64(lead);
}
/**
* @notice Post an intent. Permissionless.
*
* @dev Permissionless on purpose. Gating posting on an identity would
* reintroduce exactly the attribution the encryption removes, so what
* bounds a spammer is the BOND rather than a quota: `msg.value` must
* equal `bondWei`, and it comes back only if the intent is anchored.
*
* While `bondWei` is zero — the shipped state — this is what it has
* always been, and `msg.value` must then be zero too. Accepting a
* payment the contract has no rule for would be accepting value it
* cannot refund.
*
* @param header Canonical envelope header. Parsed, never stored.
* @param ciphertext The sealed body. Emitted, never stored, never read.
* @return leaf The intent leaf this posting is keyed on.
*/
function post(bytes calldata header, bytes calldata ciphertext)
external
payable
returns (bytes32 leaf)
{
if (header.length < HEADER_MIN_BYTES) revert HeaderTooShort(header.length);
uint8 version = uint8(header[0]);
if (version < HEADER_VERSION || version > HEADER_VERSION_MAX) {
revert UnsupportedHeaderVersion(version);
}
uint8 flags = uint8(header[OFF_FLAGS]);
if (flags & ~FLAG_AUTO_APPROVE != 0) revert UnsupportedFlags(flags);
bytes32 targetChainRef = bytes32(header[OFF_TARGET_CHAIN_REF:OFF_TARGET_CHAIN_REF + 32]);
uint64 executeNotBefore =
uint64(bytes8(header[OFF_EXECUTE_NOT_BEFORE:OFF_EXECUTE_NOT_BEFORE + 8]));
uint64 deadline = uint64(bytes8(header[OFF_DEADLINE:OFF_DEADLINE + 8]));
bytes32 bodyCommitment = bytes32(header[OFF_BODY_COMMITMENT:OFF_BODY_COMMITMENT + 32]);
leaf = bytes32(header[OFF_INTENT_LEAF:OFF_INTENT_LEAF + 32]);
bytes32 approvalKeyCommit =
bytes32(header[OFF_APPROVAL_KEY_COMMIT:OFF_APPROVAL_KEY_COMMIT + 32]);
// The commitment and the flag are one decision stated twice, so the two
// must agree: an auto intent with a commitment would look cancellable
// and not be gated, and a gated one without a commitment could never be
// approved by anyone — dead on arrival, wearing a live intent's shape.
bool auto_ = flags & FLAG_AUTO_APPROVE != 0;
if (auto_ && approvalKeyCommit != bytes32(0)) {
revert AutoApproveTakesNoCommitment(approvalKeyCommit);
}
if (!auto_ && approvalKeyCommit == bytes32(0)) revert ApprovalKeyCommitRequired();
if (leaf == bytes32(0)) revert ZeroLeaf();
if (postedOf[leaf].postedAt != 0) revert AlreadyPosted(leaf);
// Scheduling is a contract rule. An immediate intent gets the window
// from now; a scheduled one gets it from its own start, and the start
// itself is bounded by the horizon — chosen at signing, because the
// deadline is inside the signed intent and cannot be extended after.
uint64 nowMs = FinalChainTime.nowMs();
if (deadline <= nowMs) revert DeadlinePassed(deadline, nowMs);
if (executeNotBefore == 0) {
uint64 limit = nowMs + EXECUTION_WINDOW;
if (deadline > limit) revert DeadlineTooFar(deadline, limit);
} else {
uint64 horizon = nowMs + MAX_SCHEDULE_HORIZON;
if (executeNotBefore > horizon) revert ScheduleTooFar(executeNotBefore, horizon);
uint64 limit = executeNotBefore + EXECUTION_WINDOW;
if (deadline > limit) revert DeadlineTooFar(deadline, limit);
}
// The bond scales with lifetime. A month-long posting occupies state
// and attention a 48-hour one does not, and a flat bond makes
// long-lived spam the cheap kind. Ceiling division, so a single extra
// millisecond of a new window costs a whole unit.
uint256 required = bondWei;
if (required != 0) {
required = required
* ((uint256(deadline) - nowMs + EXECUTION_WINDOW - 1) / EXECUTION_WINDOW);
}
if (required > type(uint88).max) revert BondMismatch(type(uint88).max, required);
if (msg.value != required) revert BondMismatch(required, msg.value);
uint64 seq = postSeq;
postSeq = seq + 1;
leafAt[seq] = leaf;
postedOf[leaf] = Posted({
postedAt: nowMs,
executeNotBefore: executeNotBefore,
deadline: deadline,
consumedAt: 0,
bodyCommitment: bodyCommitment,
approvalKeyCommit: approvalKeyCommit,
targetChainRef: targetChainRef,
approvedAt: 0,
cancelledAt: 0,
seq: seq,
flags: flags
});
_envelopeOf[leaf] = Envelope({header: header, ciphertext: ciphertext, executorSection: ""});
if (required != 0) {
bondOf[leaf] = Bond({payer: msg.sender, amount: uint88(required), settled: false});
emit BondPosted(leaf, msg.sender, required);
}
_writeStatus(leaf, postedOf[leaf], STATUS_POSTED);
emit IntentPosted(
leaf, targetChainRef, executeNotBefore, deadline, flags, header.length, ciphertext.length
);
}
/**
* @notice Set the bond rate and where a forfeit goes.
*
* @dev Same gate as `setConsumer`, and unlike it this is NOT one-shot: a
* bond is a price and a price that could never move would be a parameter
* chosen once, before the traffic it is meant to bound existed.
*
* Arming a non-zero bond without a forfeit destination is refused. Zero
* means burn, which is a real choice — but it has to be made rather than
* arrived at by leaving a field unset, because the difference is an
* accumulating balance nobody can ever move.
*
* Changing the rate does not touch bonds already posted. Each stores what
* was actually paid, so a rate rise cannot retroactively underpay a refund
* and a cut cannot leave one over-funded.
*/
function setBondPolicy(
uint256 newBondWei,
address forfeitTo,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
_requireConfigurationAuthority(
ACTION_SET_BOND_POLICY, keccak256(abi.encode(newBondWei, forfeitTo)), anchorBlock, approvals
);
if (newBondWei != 0 && forfeitTo == address(0)) {
// Only when ARMING. Setting the rate back to zero with no
// destination is disarming, which needs no destination.
revert BondNeedsAForfeitDestination();
}
if (newBondWei > type(uint88).max) revert BondMismatch(type(uint88).max, newBondWei);
bondWei = newBondWei;
bondForfeitTo = forfeitTo;
emit BondPolicySet(newBondWei, forfeitTo);
}
/**
* @notice Return a bond whose intent was anchored.
*
* @dev Permissionless to CALL and fixed in destination: the money goes to
* the recorded payer whoever asks. That is what lets a relayer sweep on a
* user's behalf without being able to redirect anything.
*
* A pull rather than a push inside `consume`. `consume` runs in the
* anchoring append, and a refund there would put an arbitrary payer's
* `receive` on the path of every bundle — one contract that reverts, or
* burns the gas, and the anchor fails for reasons that have nothing to do
* with the bundle.
*
* `consumedAt != 0` is the whole test. Consumption means `FinalBundleLog`
* folded this leaf into an anchored bundle, which is exactly "valid
* execution" — there is no later outcome for the chain to wait on, because
* from here the execution chains authorize against the anchor.
*/
function claimBond(bytes32 leaf) external {
Bond storage b = bondOf[leaf];
if (b.payer == address(0)) revert NoBond(leaf);
if (b.settled) revert BondAlreadySettled(leaf);
if (postedOf[leaf].consumedAt == 0) revert BondNotRefundable(leaf);
b.settled = true;
uint256 amount = b.amount;
address payer = b.payer;
emit BondRefunded(leaf, payer, amount);
_send(payer, amount);
}
/**
* @notice Forfeit the bond on an intent that expired without being anchored.
*
* @dev Permissionless, and gated on the DEADLINE rather than on a judgement.
* An intent past its deadline can never be consumed — `consume` refuses one
* — so "expired and unconsumed" is a terminal, objective state, and anyone
* may say so.
*
* That is the half of the bond that actually bounds spam. A refund on
* success only makes an honest posting free; the cost of a vertex that goes
* nowhere is what a spammer pays.
*/
function forfeitBond(bytes32 leaf) external {
Bond storage b = bondOf[leaf];
if (b.payer == address(0)) revert NoBond(leaf);
if (b.settled) revert BondAlreadySettled(leaf);
Posted storage p = postedOf[leaf];
if (p.consumedAt != 0) revert BondNotForfeitable(leaf, p.deadline);
if (FinalChainTime.nowMs() <= p.deadline) revert BondNotForfeitable(leaf, p.deadline);
b.settled = true;
uint256 amount = b.amount;
address to = bondForfeitTo;
emit BondForfeited(leaf, b.payer, amount);
// A zero destination BURNS: the value stays here and nothing can move
// it. Deliberate, and the reason `setBondPolicy` refuses to arm a bond
// without a destination unless somebody chose this one.
if (to != address(0)) _send(to, amount);
}
/// @dev Checks-effects-interactions is done by the caller — `settled` is set
/// before this runs — so a reentrant call finds the bond already spent.
///
/// Full gas rather than a stipend, because a payer may legitimately be a
/// contract. The consequence to accept: a payer whose `receive` reverts
/// cannot be refunded and the bond is stuck. That is their own contract's
/// behaviour, and the alternative — swallowing the failure — would mark the
/// bond settled while the money stayed here.
function _send(address to, uint256 amount) private {
(bool ok,) = to.call{value: amount}("");
if (!ok) revert BondTransferFailed(to, amount);
}
/**
* @notice Approve a posted intent: the read-back that authorizes execution.
*
* @dev Anyone may SUBMIT this — the authority is the signature and the
* sender only pays gas, the same standing the guardian lanes give
* their relay. The key arrives in calldata and proves something only
* because it is checked against the commitment the header made at
* posting: storage-anchored evidence, exactly the rule `FinalPqQuorum`
* states for quorum keys.
*
* The digest binds this chain, this log, the leaf AND the executor
* section, so an approval cannot be replayed here or elsewhere, and
* the section it delivered cannot be swapped for another under the
* same signature.
*
* @param publicKey The per-intent ML-DSA-87 public key (2592 B).
* @param signature Over the 32-byte approval digest, verbatim (4627 B).
* @param executorSection The content key and the account's KEM public
* keys, sealed to the executor set. Stored beside the envelope;
* this contract never parses it.
*/
function approve(
bytes32 leaf,
bytes calldata publicKey,
bytes calldata signature,
bytes calldata executorSection
) external {
Posted storage p = _requireActionable(leaf);
if (p.approvedAt != 0) revert AlreadyApproved(leaf, p.approvedAt);
uint64 nowMs = FinalChainTime.nowMs();
if (nowMs > p.deadline) revert Expired(leaf, p.deadline);
bytes32 digest = keccak256(
abi.encode(
DOMAIN_INTENT_APPROVE, block.chainid, address(this), leaf, keccak256(executorSection)
)
);
_requireApprovalKey(p, publicKey, signature, digest, leaf);
p.approvedAt = nowMs;
_envelopeOf[leaf].executorSection = executorSection;
_writeStatus(leaf, p, STATUS_APPROVED);
emit IntentApproved(leaf, executorSection.length);
}
/**
* @notice Revoke a posted intent. Terminal, and allowed AFTER approval:
* a user may change their mind at any point before execution.
*
* @dev No deadline check — cancelling an expired intent is harmless and
* refusing it would fail a retry for nothing. Only consumption closes
* the door, because consumption is execution.
*/
function cancel(bytes32 leaf, bytes calldata publicKey, bytes calldata signature) external {
Posted storage p = _requireActionable(leaf);
bytes32 digest =
keccak256(abi.encode(DOMAIN_INTENT_CANCEL, block.chainid, address(this), leaf));
_requireApprovalKey(p, publicKey, signature, digest, leaf);
p.cancelledAt = FinalChainTime.nowMs();
_writeStatus(leaf, p, STATUS_CANCELLED);
emit IntentCancelled(leaf);
}
/// @dev Posted, not consumed, not cancelled — the states in which the
/// approval key still has anything to say.
function _requireActionable(bytes32 leaf) private view returns (Posted storage p) {
p = postedOf[leaf];
if (p.postedAt == 0) revert NotPosted(leaf);
if (p.consumedAt != 0) revert AlreadyConsumed(leaf, p.consumedAt);
if (p.cancelledAt != 0) revert IntentIsCancelled(leaf, p.cancelledAt);
}
/// @dev The committed key, and a valid ML-DSA-87 signature under it over
/// the 32-byte digest verbatim — the same convention every quorum approval
/// follows. An AUTO_APPROVE posting committed to nothing and has nothing
/// to approve or cancel; its veto is the account-wide freeze.
function _requireApprovalKey(
Posted storage p,
bytes calldata publicKey,
bytes calldata signature,
bytes32 digest,
bytes32 leaf
) private view {
bytes32 commit = p.approvalKeyCommit;
if (commit == bytes32(0)) revert NoApprovalKey(leaf);
bytes32 got = keccak256(publicKey);
if (got != commit) revert WrongApprovalKey(commit, got);
if (!FinalChainPrecompiles.verifyMlDsa87(publicKey, abi.encodePacked(digest), signature)) {
revert ApprovalSignatureInvalid(leaf);
}
}
// ------------------------------------------------------ tree-7 mirror
/// @notice The tree-7 key for posting sequence `seq` — a RING, so slots
/// recycle at `INTENT_SLOT_RING` and the tree can never fill.
/// @dev Mirrored by the backend byte for byte; pinned cross-repo.
function intentSlotKey(uint64 seq) public pure returns (bytes32) {
return keccak256(
abi.encodePacked(DOMAIN_INTENT_KEY, bytes32(uint256(seq % INTENT_SLOT_RING)))
);
}
/// @notice The tree-7 status leaf. Everything in it is public log state —
/// nothing account-linked, so the tree adds searchability without
/// touching unlinkability. Expiry is derived from `deadline` by the
/// reader, never written.
function intentStatusLeaf(
bytes32 intentLeaf,
uint8 status,
uint64 postedAt,
uint64 executeNotBefore,
uint64 deadline,
bytes32 targetChainRef,
bytes32 bodyCommitment,
uint64 seq
) public pure returns (bytes32) {
return keccak256(
bytes.concat(
DOMAIN_INTENT_STATUS_LEAF,
abi.encode(
intentLeaf,
status,
postedAt,
executeNotBefore,
deadline,
targetChainRef,
bodyCommitment,
seq
)
)
);
}
/// @dev One transition, one write. The log is `treeWriter[7]`, and the
/// tree-1 argument applies verbatim: everything this mirrors was already
/// verified here, so no quorum belongs on top.
function _writeStatus(bytes32 leaf, Posted storage p, uint8 status) private {
bytes32[] memory keys = new bytes32[](1);
bytes32[] memory statusLeaves = new bytes32[](1);
keys[0] = intentSlotKey(p.seq);
statusLeaves[0] = intentStatusLeaf(
leaf,
status,
p.postedAt,
p.executeNotBefore,
p.deadline,
p.targetChainRef,
p.bodyCommitment,
p.seq
);
trees.setLeavesAsWriter(TREE_INTENTS_ID, BRANCH_MAIN_ID, keys, statusLeaves);
}
/**
* @notice Consume a posted intent as part of an anchoring append.
*
* @dev Called by `FinalBundleLog` in the same transaction as the append, so
* there is no window in which a bundle is anchored and its intents are
* not yet spent.
*
* Consumption is permanent and is the replay gate. The execution chain
* keeps its own consumed-seqId set, which covers a bundle being
* re-executed; this covers an intent being re-anchored into a second
* bundle, which that set does not see.
*/
function consume(bytes32 leaf) external {
// Checked before the comparison, not folded into it. An unset consumer
// is `address(0)`, and `msg.sender != consumer` would then be FALSE for
// a caller of `address(0)` — so an unwired log would consume for the one
// caller nobody can be, which is the kind of "unreachable" that stops
// being unreachable the moment something else changes.
address c = consumer;
if (c == address(0)) revert ConsumerUnset();
if (msg.sender != c) revert NotConsumer(msg.sender);
Posted storage p = postedOf[leaf];
if (p.postedAt == 0) revert NotPosted(leaf);
if (p.consumedAt != 0) revert AlreadyConsumed(leaf, p.consumedAt);
if (p.cancelledAt != 0) revert IntentIsCancelled(leaf, p.cancelledAt);
uint64 nowMs = FinalChainTime.nowMs();
if (nowMs > p.deadline) revert Expired(leaf, p.deadline);
// Scheduling is enforced HERE, which is what makes the funding and fee
// re-checks execution-time checks for free: admission cannot happen
// before the moment the intent named — less the lead, so the wrapper
// that follows admission can land AT that moment rather than minutes
// after it (`SCHEDULE_LEAD_MS`).
if (nowMs + _scheduleLeadMs(p) < p.executeNotBefore) {
revert TooEarly(leaf, p.executeNotBefore, nowMs);
}
// The read-back gate. Neither approved nor auto means the user has not
// seen on chain what is about to execute.
if (p.approvedAt == 0 && p.flags & FLAG_AUTO_APPROVE == 0) revert NotApproved(leaf);
p.consumedAt = nowMs;
_writeStatus(leaf, p, STATUS_CONSUMED);
emit IntentConsumed(leaf, msg.sender);
}
/// @notice The stored envelope. One `eth_call`, no range, any age.
///
/// @dev A getter rather than a public mapping so the three parts come back
/// in one call — a caller pairing a header from one read with a
/// ciphertext from another would be pairing across a posting that
/// landed between them.
function envelopeOf(bytes32 leaf)
external
view
returns (bytes memory header, bytes memory ciphertext, bytes memory executorSection)
{
Envelope storage e = _envelopeOf[leaf];
return (e.header, e.ciphertext, e.executorSection);
}
/// @notice Is `leaf` posted, unspent, uncancelled and unexpired right now?
function isOpen(bytes32 leaf) external view returns (bool) {
Posted storage p = postedOf[leaf];
return p.postedAt != 0 && p.consumedAt == 0 && p.cancelledAt == 0
&& FinalChainTime.nowMs() <= p.deadline;
}
/// @notice Would `consume` accept `leaf` right now? Open, due, and either
/// approved or AUTO_APPROVE — the work-queue predicate, so a solver
/// asks the same question the contract answers.
function isConsumable(bytes32 leaf) external view returns (bool) {
Posted storage p = postedOf[leaf];
uint64 nowMs = FinalChainTime.nowMs();
return p.postedAt != 0 && p.consumedAt == 0 && p.cancelledAt == 0 && nowMs <= p.deadline
&& nowMs + _scheduleLeadMs(p) >= p.executeNotBefore
&& (p.approvedAt != 0 || p.flags & FLAG_AUTO_APPROVE != 0);
}
/// @dev The lead `consume` grants a posting: its target chain's
/// (`scheduleLeadMsOf`) for a scheduled one, nothing for an immediate
/// one (there is no moment to lead).
function _scheduleLeadMs(Posted storage p) private view returns (uint64) {
return p.executeNotBefore == 0 ? 0 : scheduleLeadMsOf(p.targetChainRef);
}
}
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
//
// @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.
*
* An action that authorizes EXECUTION 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. Neither family alone can then move funds, and 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 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 access-class quorum is ~119 KB of calldata. That is affordable here
* only because this is our own chain. 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 where the action authorizes execution; empty otherwise.
bytes seal;
}
error ThresholdNotMet(uint256 valid, uint256 required);
error SignersNotAscending(address previous, address next);
error SignerLacksRole(address signer, uint256 roleMask);
error WrongAlgorithm(address signer, uint8 got, uint8 required);
error BadSignature(address signer, uint8 algorithm);
error BadSeal(address signer);
error AnchorAhead(uint64 anchorBlock, uint256 blockNumber);
error AnchorStale(uint64 anchorBlock, uint256 blockNumber);
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 execution class.
* @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);
}
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/finalchain/FinalStateTrees.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
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;
import {FinalIdentityRegistry} from "./FinalIdentityRegistry.sol";
import {FinalChainTime} from "./FinalChainTime.sol";
import {FinalPqQuorum} from "./FinalPqQuorum.sol";
/// @notice The one question `syncIdentities` asks the asset registry.
/// @dev An interface rather than an import of `FinalAssetRegistry`, which
/// imports this file: the registry is tree 6's writer and holds the trees
/// as an immutable, so the dependency runs that way and this is the one
/// read that runs the other.
interface IChainSource {
function enabledChainRefs() external view returns (bytes32[] memory);
}
/// @notice The one question {FinalStateTrees.syncSlotKeyLeaves} asks the
/// slot-key registry: the leaf value for one member's slot — the
/// registry's own verdict, zero when the slot holds nothing usable.
interface ISlotKeySource {
function slotKeyLeafOf(address member, uint64 slotIndex) external view returns (bytes32);
}
/// @notice The one question {FinalStateTrees.syncEndpointLeaves} asks the
/// endpoint registry: the leaf value for one tunnel endpoint — the
/// registry's own verdict (certificate hash, status, expiry, region),
/// zero when nothing is registered under the id.
interface IEndpointSource {
function endpointLeafOf(bytes32 endpointId) external view returns (bytes32);
}
/**
* @title FinalStateTrees
* @notice The eight trees. Final Chain's state plane, and the source of truth
* every other chain projects from.
*
* @dev One tree per domain, because they change at unrelated cadences and a
* combined tree invalidates every outstanding proof on every tick:
*
* | # | tree | holds | cadence |
* |---|---|---|---|
* | 1 | accounts | every Final Wallet's public state | per rotation / creation |
* | 2 | phi | the PHI record: per (wallet, chain) balances, the lock, exposures | per publisher round |
* | 3 | vasset | issued vAsset supply and backing, per (asset, chain) | per settlement |
* | 4 | oracle | published prices and their inputs | ~10 s; 1 s for morph and fee assets |
* | 5 | settlement | chain and asset registry roots | rarely |
* | 6 | allowlist | assets, chains, policy, price sources, DEX deployments | rarely |
* | 7 | intents | intent status, ring-keyed over the posting sequence | per posting |
* | 8 | identity | the wallet-creation admission set, projected from the registry | per identity mutation |
*
* ## The hash shape is not a choice
*
* Leaves hash as `keccak256(0x00 ‖ leaf)` and internal nodes as
* `keccak256(0x01 ‖ lo ‖ hi)` with the pair sorted. That is
* `FinalMerkle.verifyTaggedSortedProof`, verbatim, which is what
* `FinalWalletFactory.syncAccountState` and `FinalSettlement` already run on
* every supported chain. A proof produced here is consumed there with no
* translation and no contract change, and tree 1's leaf preimage is exactly
* `FinalWalletFactory.accountStateLeafHash` — same fields, same order, the
* `deployedChains` table `abi.encode`d like every other field.
*
* Getting this wrong is not a compile error anywhere. It is a root every chain
* silently rejects, with nothing pointing at the cause.
*
* ## Positional slots under a sorted-pair tree
*
* Sorted pairs make a proof position-agnostic, which is why it carries no
* direction bits. That does not stop the TREE from being positional, and here
* it is: every key gets a permanent slot, so a single leaf update is `DEPTH`
* hashes instead of a rebuild over every leaf. The verifier neither knows nor
* needs to know that a slot exists.
*
* ## Branches (2026-09-04)
*
* The slot space of every tree is cut into `BRANCH_COUNT` branches by the top
* `BRANCH_BITS` of the slot: a branch is a subtree with a permanent place, its
* root is one internal node, and a leaf's path to the tree root passes through
* it. Branches hold what belongs to the same domain but not to the same rows
* — branch 0 is the owning service's CONFIGURATION on every tree, tree 8 adds
* the owner → wallets index and the co-signers' slot keys beside the admission
* set — and they are chosen over more trees because a branch shares its
* tree's authority doors and writer, while a tree would need its own. A leaf
* proves against its branch root with `BRANCH_DEPTH` siblings, against the
* tree root with `DEPTH`, against the round root with `ROUND_DEPTH`: one path,
* cut at three heights, one verifier.
*
* ## Rounds, and why the live roots are not the product
*
* `setLeaves` moves a tree. It does not publish one. A consumer that fetched
* eight roots one at a time would get a price proof from one moment and a
* roster proof from another, and something delisted in between would still
* verify.
*
* `publishRound` snapshots all eight together, and folds them into ONE round
* root — the tree roots as the level-`DEPTH` nodes of a depth-`ROUND_DEPTH`
* tree, tree `t` at position `t` — so a single word commits to the whole
* plane and any leaf in it proves against that word with four more siblings.
* A round is the unit a consumer pins, and it is the only thing this contract
* promises is contemporaneous. The execution chains keep anchoring per-tree
* roots (identity, account state, registry roots): those must move at their
* own cadence, not at the oracle's.
*/
contract FinalStateTrees {
// ---------------------------------------------------------------- trees
/// @notice Every Final Wallet's public state. The source of truth other
/// chains copy through `syncAccountState`.
uint8 public constant TREE_ACCOUNTS = 1;
/// @notice The PHI record, per `(wallet, chain)`: balances, the lock, its
/// terms, the exposures carved from it and the accrual between reconciliations.
uint8 public constant TREE_PHI = 2;
/// @notice vAsset supply and backing.
uint8 public constant TREE_VASSET = 3;
/// @notice Oracle prices and their inputs.
uint8 public constant TREE_ORACLE = 4;
/// @notice Settlement chain and asset registry roots.
uint8 public constant TREE_SETTLEMENT = 5;
/// @notice Which assets and chains are supported.
uint8 public constant TREE_ALLOWLIST = 6;
/// @notice Intent status, keyed by a RING over the posting sequence.
/// @dev The search structure beside `FinalBundleLog`'s permanent record.
/// Written only by `FinalIntentLog` through `treeWriter[7]` — the tree-1
/// argument verbatim: the log verified the bond, the commitment, the
/// approval and the consume itself, and a service quorum on top would be a
/// censorship point over posting. Slots are permanent and intents are
/// unbounded flow, so the log recycles keys modulo `CAPACITY`: the tree is
/// an index with a ~1M-posting retention window, never the record.
uint8 public constant TREE_INTENTS = 7;
/// @notice The wallet-creation admission set — the identity leaves
/// (`keccak256(DOMAIN_IDENTITY_LEAF ‖ serial ‖ keysHash)`) every execution
/// chain's gateway verifies certificates against.
/// @dev The root the gateways anchor as `currentIdentityRoot` — a
/// CONTINUOUS root over this tree replaces the cold-set snapshot the
/// retired `publishIdentityRoot.cjs` ceremony folded off-chain. Two
/// feeders, one per identity plane, and NO quorum door for either:
///
/// - SERVICE identities: {syncIdentityLeaves}, the permissionless
/// projection of `FinalIdentityRegistry`'s own verdict — the registry
/// calls it same-tx on every identity mutation, and anyone may call it
/// to retire a leaf whose standing lapsed by TIME (expiry moves no
/// registry storage, so only a projection pass can zero it).
/// - USER identities: `treeWriter[8]` — `FinalAccountLedger`, which
/// computes the leaf from the genesis certificate fields it verified
/// under its opener quorum and writes it once at `openAccount`. A user
/// admission leaf is permanent by construction: the certificate IS the
/// address, rotation never changes it, and a post-rotation creation on
/// a new chain reads PUBLISHED account state (WAL-02), never the
/// certificate's genesis keys.
///
/// A quorum of service signatures must not be able to state an identity
/// neither ruler decided, so `setLeaves` refuses this tree outright.
uint8 public constant TREE_IDENTITY = 8;
/// @notice Count, for iteration. Trees are 1-indexed; 0 is not a tree.
uint8 public constant TREE_COUNT = 8;
/// @notice 2^24 slots per tree, laid out as 16 BRANCHES of 2^20: the top
/// `BRANCH_BITS` of a slot name the branch, the rest its position in it.
/// A million rows per branch is far past where this design gets replaced
/// by Final Chain proper. Raising any of this later is a migration, not a
/// parameter change: the depth is in every root.
uint256 public constant DEPTH = 24;
uint256 public constant BRANCH_BITS = 4;
uint8 public constant BRANCH_COUNT = 16;
/// @notice Height of a branch: a leaf proves against its branch root with
/// this many siblings.
uint256 public constant BRANCH_DEPTH = DEPTH - BRANCH_BITS;
/// @notice Slots per branch.
uint256 public constant BRANCH_CAPACITY = 1 << BRANCH_DEPTH;
/// @notice Slots per tree, all branches together.
uint256 public constant CAPACITY = 1 << DEPTH;
/// @notice The round root is a tree over the tree roots — position `t`
/// holds tree `t`'s root, positions 0 and 9..15 the empty tree — folded
/// with the same node hash. It is literally the root of a depth-28 tree
/// whose level-24 nodes are the eight tree roots, which is what lets one
/// path prove a leaf against it.
uint256 public constant FOREST_BITS = 4;
uint256 public constant ROUND_DEPTH = DEPTH + FOREST_BITS;
/// @notice Branch 0 of EVERY tree: the configuration of the service that
/// owns the tree — key → one word, the VALUE stored so a contract on this
/// chain reads it directly (`configValue`), the hash in the tree so it is
/// provable wherever a round root is. Written only by {setConfig} under
/// the configuration authority; every other door refuses the branch.
uint8 public constant BRANCH_CONFIG = 0;
/// @notice Branch 1 of every tree: the domain's own rows — accounts, PHI
/// records, vAssets, prices, registry roots, the allowlist, the intent
/// ring, the identity admission set.
uint8 public constant BRANCH_MAIN = 1;
/// @notice Tree 8, branch 2: the owner → wallets index. Key = the owner
/// (`ownerIndexKeyFor`), leaf = {ownerIndexLeafHash} over the ledger's
/// `walletsByOwner(owner)`. Written by tree 8's writer, the ledger, beside
/// every open and every owner transfer — the tree is the search structure,
/// the ledger holds the readable array it proves.
uint8 public constant BRANCH_OWNER_INDEX = 2;
/// @notice Tree 8, branch 3: the co-signers' per-slot KEM publics — a RING
/// of `SLOT_KEY_RING` positions per member, projected from
/// `slotKeySource` by {syncSlotKeyLeaves} exactly as identities are.
uint8 public constant BRANCH_SLOT_KEYS = 3;
/// @notice Tree 8, branch 4: the tunnel endpoints — the Final Node
/// identities a wallet's FNP session terminates at. Key = the endpoint id
/// (`endpointKeyFor`, the certificate's subject key id), leaf = the
/// endpoint registry's verdict, projected from `endpointSource` by
/// {syncEndpointLeaves} exactly as slot keys are. An execution chain never
/// parses an endpoint certificate; it anchors this tree's root and a client
/// proves the leaf against it (user ruling 2026-09-05).
uint8 public constant BRANCH_ENDPOINTS = 4;
/// @notice Slot-key positions per member. A slot index wraps modulo this,
/// so the branch is an index over the recent slots and never fills; 1024
/// members × 1024 positions is the branch exactly.
uint64 public constant SLOT_KEY_RING = 1024;
/// @dev Must equal `FinalWalletFactory.DOMAIN_ACCOUNT_STATE_LEAF`. Pinned
/// by the cross-repo parity test; a field reordered on one side and not the
/// other is a root every chain rejects.
///
/// `v02`: `deployedChains` became the `(chainRef, account)` table. A v01
/// leaf and a v02 leaf never share a domain, so a proof built against the
/// retired plane cannot verify against this one by accident.
bytes32 public constant DOMAIN_ACCOUNT_STATE_LEAF =
keccak256("FINAL_ACCOUNT_STATE_LEAF_v02");
bytes32 private constant ACTION_SET_LEAVES = keccak256("FinalStateTrees.setLeaves.v01");
/// @dev Registrar-quorum actions, verified by the registry with this
/// contract as the verifying contract. See `FinalIdentityRegistry.requireRegistrarQuorum`.
bytes32 public constant ACTION_CONFIGURE_TREE = keccak256("FINAL_STATE_TREES_CONFIGURE_TREE_v01");
bytes32 public constant ACTION_SET_TREE_WRITER = keccak256("FINAL_STATE_TREES_SET_TREE_WRITER_v01");
bytes32 public constant ACTION_SET_CHAIN_SOURCE = keccak256("FINAL_STATE_TREES_SET_CHAIN_SOURCE_v01");
bytes32 public constant ACTION_SET_SLOT_KEY_SOURCE = keccak256("FINAL_STATE_TREES_SET_SLOT_KEY_SOURCE_v01");
bytes32 public constant ACTION_SET_ENDPOINT_SOURCE = keccak256("FINAL_STATE_TREES_SET_ENDPOINT_SOURCE_v01");
bytes32 public constant ACTION_SEED_COUNTERS = keccak256("FINAL_STATE_TREES_SEED_COUNTERS_v01");
bytes32 public constant ACTION_SET_TYPED_WRITER = keccak256("FINAL_STATE_TREES_SET_TYPED_WRITER_v01");
bytes32 public constant ACTION_SET_CONFIG = keccak256("FINAL_STATE_TREES_SET_CONFIG_v01");
/// @dev Key domains. Both are full-width hashes rather than the packed
/// address they came from, which matters: an address key occupies only the
/// low 160 bits, so a hashed key colliding with one needs ~2^96 work rather
/// than a full collision. That is expensive but not comfortable, and the
/// consequence would be a service identity landing in a wallet's slot.
bytes32 private constant DOMAIN_ACCOUNT_KEY = keccak256("FinalStateTrees.key.account.v01");
bytes32 private constant DOMAIN_IDENTITY_TREE_KEY = keccak256("FinalStateTrees.key.identity.v01");
/// @dev Tree 8, branches 2 and 3, and branch 0 of every tree. Each is its
/// own domain so a key can never land in another branch's slot by
/// construction — `_set` refuses a key whose slot sits in a different
/// branch, and the domain is what makes that refusal unreachable.
bytes32 private constant DOMAIN_OWNER_INDEX_KEY = keccak256("FinalStateTrees.key.ownerIndex.v01");
bytes32 private constant DOMAIN_SLOT_KEY = keccak256("FinalStateTrees.key.slotKey.v01");
bytes32 private constant DOMAIN_ENDPOINT_KEY = keccak256("FinalStateTrees.key.endpoint.v01");
bytes32 private constant DOMAIN_CONFIG_KEY = keccak256("FinalStateTrees.key.config.v01");
/// @notice Leaf domains for the owner index (tree 8, branch 2) and for
/// configuration rows (branch 0 of every tree). The config leaf binds the
/// tree too, so the same row in two trees is two different leaves.
bytes32 public constant DOMAIN_OWNER_INDEX_LEAF = keccak256("FINAL_OWNER_INDEX_LEAF_v01");
bytes32 public constant DOMAIN_CONFIG_LEAF = keccak256("FINAL_CONFIG_LEAF_v01");
// -------------------------------------------------------------- storage
/// @notice The registry every signer is resolved through. Immutable so the
/// quorum can never be pointed at a registry supplied in calldata.
FinalIdentityRegistry public immutable registry;
/// @notice Approvals required per tree.
///
/// @dev Per-tree and not a scalar, because each tree is gated by a
/// DIFFERENT role — account co-signers, PHI, vAsset and oracle
/// publishers, registry publishers — so K is a property of that
/// tree's roster, not of the contract. All six read 2 today; that is
/// a deploy-time default, not an invariant, and collapsing them would
/// put the oracle roster's quorum on the account co-signers'.
///
/// The VALUE is a full word: it is a quantity compared against a live
/// member count, and every other threshold in the system is `uint256`.
/// The KEY is `uint8` because that is what a tree id is here — six
/// `uint8` constants, every parameter, every event, every error,
/// `_assertTree`, and the ten sibling mappings below. Widening it
/// would buy nothing (a narrow key is padded to 32 bytes before
/// hashing, so the slot is identical) and cost the getter's selector
/// on a contract that is live on both Final Chains.
mapping(uint8 treeId => uint256) public threshold;
/// @notice Role a signer must hold to write to a tree.
mapping(uint8 treeId => uint256) public writerRole;
/// Raw (untagged) leaf value by tree and slot.
mapping(uint8 => mapping(uint256 => bytes32)) private _leaf;
/// Internal nodes, levels 1..DEPTH. Level 0 is derived from `_leaf`.
mapping(uint8 => mapping(uint256 => mapping(uint256 => bytes32))) private _node;
/// Empty-subtree hash per level, computed once at construction — up to
/// the round root's height, since the forest's empty positions are empty
/// trees.
bytes32[ROUND_DEPTH + 1] private _zero;
/// Permanent slot for a key, 1-based so 0 means unassigned. The slot's top
/// `BRANCH_BITS` are the branch the key lives in.
mapping(uint8 => mapping(bytes32 => uint256)) private _slotPlusOne;
/// The key a slot was handed to — the reverse of `_slotPlusOne`, so any
/// branch enumerates on chain (`keyAt` over `0 .. branchSlotsUsed`) with
/// no log window. One extra word per NEW key, never per update.
mapping(uint8 => mapping(uint256 => bytes32)) private _keyAt;
/// @notice Slots handed out per tree, all branches together.
mapping(uint8 => uint256) public slotsUsed;
/// Slots handed out per branch — the next position in it.
mapping(uint8 => mapping(uint8 => uint256)) private _branchSlotsUsed;
/// The VALUE behind a configuration row (branch 0), by tree and key.
mapping(uint8 => mapping(bytes32 => bytes32)) private _configValue;
/// @notice Live root per tree. Moves on every `setLeaves`.
mapping(uint8 treeId => bytes32) public liveRoot;
/// @notice Writes applied per tree, for change detection between rounds.
mapping(uint8 treeId => uint64) public treeVersion;
/// @notice A contemporaneous snapshot of all eight roots, and the one
/// round root that folds them.
struct Round {
bytes32[TREE_COUNT + 1] roots;
bytes32 roundRoot;
uint64 blockNumber;
uint64 timestamp;
}
/// @notice Published rounds, 1-indexed. Round 0 is "nothing published".
mapping(uint64 => Round) private _rounds;
/// @notice Highest published round.
uint64 public round;
/// Tree versions as of the last published round.
mapping(uint8 => uint64) private _publishedVersion;
/// @notice Per-tree nonce, bound into every quorum digest.
mapping(uint8 treeId => uint64) public nonce;
/**
* @notice A CONTRACT allowed to write one tree without a quorum.
*
* @dev Exactly one per tree, and today exactly one exists: tree 1's is
* `FinalAccountLedger`.
*
* This looks like a hole and is the opposite. The quorum on `setLeaves`
* exists because a tree's writer is otherwise one key deciding what the
* chain states. A writer contract is not a key — its rules are its
* bytecode, it has no owner and no proxy, and tree 1's writer authorizes
* every change by verifying the ACCOUNT HOLDER'S own post-quantum signature
* in this chain's precompiles. That is strictly stronger evidence than a
* K-of-N of our own services attesting to what they read.
*
* Keeping the quorum on top of it would be actively worse: our fleet could
* then withhold approval from a user rotating a stolen key, which is a
* censorship power over the exact operation the account plane exists to
* make possible.
*
* The writer is set on the same bootstrap window as `configureTree` and can
* be moved by a registrar afterwards — an immutable pointer would mean a
* ledger upgrade abandons the tree it writes.
*/
mapping(uint8 treeId => address) public treeWriter;
/**
* @notice Where `syncIdentities` reads the chain set from — the asset
* registry, which is also tree 6's writer.
*
* @dev A service identity is a Final Wallet whose address is the same on
* every EVM chain, so its tree-1 `deployedChains` table is derivable: one
* `(chainRef, itself)` row per chain the registry has enabled. The table
* is DERIVED from state rather than supplied by the caller precisely so
* that `syncIdentities` can stay permissionless — a caller-chosen table
* would let anyone grant a service identity on a chain of their choosing.
*
* Unset (zero) means services carry an empty table and exist on Final
* Chain alone, which is what a plane looks like before its registry is
* seeded. Same configuration gate as `setTreeWriter`, because pointing this
* at a different contract changes what every service leaf says.
*/
address public chainSource;
/// @notice Where {syncSlotKeyLeaves} reads the co-signers' slot keys from
/// — the slot-key registry, whose verdict tree 8's branch 3
/// projects. Same configuration gate as `chainSource`; unset means
/// the branch cannot be written.
address public slotKeySource;
/// @notice The endpoint registry whose verdict tree 8's branch 4 projects.
address public endpointSource;
/// @notice The one contract admitted to {writeTyped}: `FinalStateRecords`,
/// which holds the preimages behind trees 2, 3 and 4 and computes
/// their keys and hashes. Same configuration gate as `treeWriter`.
address public typedWriter;
// --------------------------------------------------------------- events
event LeavesSet(uint8 indexed treeId, uint256 count, bytes32 newRoot, uint64 treeVersion);
event RoundPublished(uint64 indexed round, uint64 blockNumber, uint64 timestamp);
event TreeConfigured(uint8 indexed treeId, uint256 writerRole, uint256 threshold);
event TreeWriterSet(uint8 indexed treeId, address writer);
event ChainSourceSet(address source);
event SlotKeySourceSet(address source);
event EndpointSourceSet(address source);
/// @notice A fresh plane took over the previous plane's counters.
event CountersSeeded(uint64 round, uint64[] versions);
event TypedWriterSet(address writer);
event ConfigSet(uint8 indexed treeId, bytes32 indexed key, bytes32 value);
// --------------------------------------------------------------- errors
error UnknownTree(uint8 treeId);
error LengthMismatch(uint256 keys, uint256 leaves);
error BranchFull(uint8 treeId, uint8 branch);
error UnknownBranch(uint8 branch);
/// @notice A key already holds a slot in another branch of this tree.
error BranchMismatch(uint8 treeId, bytes32 key, uint8 have, uint8 want);
/// @notice Branch 0 is written by `setConfig` alone.
error ConfigBranchReserved(uint8 treeId);
error SlotKeySourceUnset();
error EndpointSourceUnset();
/// @notice Counters can be seeded only into a plane that has published nothing.
error NotFresh();
error VersionCountMismatch(uint256 given);
error TreeNotConfigured(uint8 treeId);
error NothingToPublish();
error UnknownKey(uint8 treeId, bytes32 key);
error NotAuthorized(address caller);
error NoRounds();
error ThresholdUnreachable(uint8 treeId, uint256 live, uint256 required);
/// @notice Trees 7 and 8 take no quorum writes — only their writer
/// contract (and, for tree 8, the registry projection).
error WriterOnlyTree(uint8 treeId);
/// @notice `setLeaves` was called on a tree that has a typed writer.
/// @dev Trees 2, 3 and 4 keep the leaf's preimage beside its hash so a
/// consumer can read the VALUE. An untyped write sets the hash and
/// cannot set the preimage — the pair would disagree, and the stored
/// value would look authoritative while committing to nothing. The
/// typed entrypoint is not a convenience over this one; it is the
/// only door.
error TypedTreeOnly(uint8 treeId);
/// @notice A `deployedChains` row names the zero chain or the zero account,
/// or repeats a chain. A table with either proves nothing about
/// where the account exists.
error InvalidChainAccount(bytes32 chainRef, bytes32 account);
// ---------------------------------------------------------- constructor
/**
* @param registry_ The identity registry. Every signer, key and role is
* resolved through it.
* @dev The empty-subtree table is built here rather than as constants
* because it depends on the tagging, and a constant table that drifted from
* the tagging would produce roots nothing can verify — silently, since both
* sides would still be self-consistent.
*/
constructor(FinalIdentityRegistry registry_) {
registry = registry_;
// Level 0: the tagged hash of an empty (zero) leaf.
_zero[0] = keccak256(abi.encodePacked(bytes1(0x00), bytes32(0)));
for (uint256 l = 0; l < ROUND_DEPTH; l++) {
// Both children equal, so the sort is a no-op and the order is
// irrelevant — which is the only reason this table is one value per
// level rather than one per position.
_zero[l + 1] = keccak256(abi.encodePacked(bytes1(0x01), _zero[l], _zero[l]));
}
for (uint8 t = 1; t <= TREE_COUNT; t++) {
liveRoot[t] = _zero[DEPTH];
}
}
// ------------------------------------------------------- configuration
/**
* @dev The configuration gate: the registry's bootstrap admin alone while
* its window is open, the sealed `ROLE_REGISTRAR` quorum afterwards. The
* same window the registry uses, for the same reason — every roster has to
* be installed by someone before it can install itself — and the same
* quorum, because a threshold is membership by another name: whoever can
* set K to one owns the tree.
*/
function _requireConfigurationAuthority(
bytes32 actionDomain,
bytes32 payloadDigest,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) private {
if (!registry.bootstrapSealed() && msg.sender == registry.bootstrapAdmin()) return;
registry.requireRegistrarQuorum(actionDomain, payloadDigest, anchorBlock, approvals);
}
/**
* @notice Set which role may write a tree and how many approvals it needs.
* @param k Approvals a write needs; `0` leaves the tree unconfigured.
* @param anchorBlock The registrars' roster anchor. Ignored during bootstrap.
* @param approvals The sealed registrar quorum. Empty during bootstrap.
*/
function configureTree(
uint8 treeId,
uint256 role,
uint256 k,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
_assertTree(treeId);
_requireConfigurationAuthority(
ACTION_CONFIGURE_TREE, keccak256(abi.encode(treeId, role, k)), anchorBlock, approvals
);
// Refuse a threshold nobody can meet. Register the members first; that
// ordering is the point, not an inconvenience. A 4-of-5 configured
// against three registered co-signers is a tree that reverts on every
// write, and the revert names the threshold rather than the roster.
if (k != 0) {
uint256 live = registry.liveMemberCount(role);
if (live < k) revert ThresholdUnreachable(treeId, live, k);
}
writerRole[treeId] = role;
threshold[treeId] = k;
emit TreeConfigured(treeId, role, k);
}
/**
* @notice Point a tree at the contract allowed to write it directly.
* @dev Same gate as `configureTree`, for the same reason. Setting it to the
* zero address removes the path entirely and leaves the tree quorum-only.
*/
function setTreeWriter(
uint8 treeId,
address writer,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
_assertTree(treeId);
_requireConfigurationAuthority(
ACTION_SET_TREE_WRITER, keccak256(abi.encode(treeId, writer)), anchorBlock, approvals
);
treeWriter[treeId] = writer;
emit TreeWriterSet(treeId, writer);
}
/**
* @notice Point `syncIdentities` at the contract that knows the chain set.
* @dev Same gate as `setTreeWriter`. Zero removes the source, after which
* service leaves carry an empty `deployedChains` table.
*/
function setChainSource(
address source,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
_requireConfigurationAuthority(
ACTION_SET_CHAIN_SOURCE, keccak256(abi.encode(source)), anchorBlock, approvals
);
chainSource = source;
emit ChainSourceSet(source);
}
/// @notice Point tree 8's branch 3 at the slot-key registry it projects.
function setSlotKeySource(
address source,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
_requireConfigurationAuthority(
ACTION_SET_SLOT_KEY_SOURCE, keccak256(abi.encode(source)), anchorBlock, approvals
);
slotKeySource = source;
emit SlotKeySourceSet(source);
}
/// @notice Point tree 8's branch 4 at the endpoint registry it projects.
function setEndpointSource(
address source,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
_requireConfigurationAuthority(
ACTION_SET_ENDPOINT_SOURCE, keccak256(abi.encode(source)), anchorBlock, approvals
);
endpointSource = source;
emit EndpointSourceSet(source);
}
/**
* @notice Take over the previous plane's counters — one `treeVersion` per
* tree (index = treeId, 0 unused) and the published `round` — so a
* redeploy is monotonic for every consumer that compares them
* (rings, explorers, the round feed). NO-WIPE redeploy, ruled
* 2026-09-03. Past rounds' roots stay on the old plane:
* `roundRootAt` below the seed answers zero.
* @dev Configuration authority (bootstrap admin before the seal, registrar
* quorum after), and only while this plane has published nothing.
*/
function seedCounters(
uint64[] calldata versions,
uint64 round_,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
_requireConfigurationAuthority(
ACTION_SEED_COUNTERS, keccak256(abi.encode(versions, round_)), anchorBlock, approvals
);
if (versions.length != TREE_COUNT + 1) revert VersionCountMismatch(versions.length);
if (round != 0) revert NotFresh();
for (uint8 t = 1; t <= TREE_COUNT; t++) {
if (treeVersion[t] != 0) revert NotFresh();
}
for (uint8 t = 1; t <= TREE_COUNT; t++) {
treeVersion[t] = versions[t];
}
round = round_;
emit CountersSeeded(round_, versions);
}
/// @notice Install the records contract that writes the typed trees.
function setTypedWriter(
address writer,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
_requireConfigurationAuthority(
ACTION_SET_TYPED_WRITER, keccak256(abi.encode(writer)), anchorBlock, approvals
);
typedWriter = writer;
emit TypedWriterSet(writer);
}
/**
* @notice Write configuration rows into a tree's branch 0.
* @param treeId The tree whose owning service the rows configure.
* @param keys `configKey(name, sub)` per row.
* @param values One word per row — a duration, a count, an address, a
* flag; the reader knows the shape from the name.
*
* @dev The configuration authority, not the tree's writer or quorum: a
* tree's writer states what its domain verified, its quorum attests to
* what it read, and neither is the authority over how the service that
* feeds it is configured. The value is stored beside the hash so a
* contract on this chain reads it in one call (`configValue`) while the
* same row is provable off chain against the round root — one source for
* the fleet, the contracts and the explorer, where the fleet's environment
* used to be a second one.
*/
function setConfig(
uint8 treeId,
bytes32[] calldata keys,
bytes32[] calldata values,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
_assertTree(treeId);
if (keys.length != values.length || keys.length == 0) revert LengthMismatch(keys.length, values.length);
_requireConfigurationAuthority(
ACTION_SET_CONFIG, keccak256(abi.encode(treeId, keys, values)), anchorBlock, approvals
);
for (uint256 i = 0; i < keys.length; i++) {
_configValue[treeId][keys[i]] = values[i];
_set(treeId, BRANCH_CONFIG, keys[i], configLeafHash(treeId, keys[i], values[i]));
emit ConfigSet(treeId, keys[i], values[i]);
}
_bump(treeId, keys.length);
}
// ------------------------------------------------------------- writing
/**
* @notice Write leaves into one branch of one tree under a PQ quorum.
* @param treeId Which tree.
* @param branch Which branch — never 0, which `setConfig` alone writes.
* @param keys Domain keys — a wallet address for accounts, an asset id for
* the allowlist, whatever identifies a row in that domain. Each gets
* a permanent slot in the branch on first write.
* @param leaves The raw (untagged) leaf values.
* @param approvals At least `threshold[treeId]` of them, ascending by signer.
*
* @dev The digest binds the tree, its nonce, and the full batch. Binding the
* nonce is what stops the same approved batch being replayed: without it,
* an approval to set a price is an approval to set that price again at any
* later block, which for an oracle is the whole attack.
*
* ML-DSA-87 is required rather than accepted. These are operational,
* high-cadence writes — the transaction class — and leaving the choice open
* would mean a break in either scheme takes the tree.
*/
function setLeaves(
uint8 treeId,
uint8 branch,
bytes32[] calldata keys,
bytes32[] calldata leaves,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
_assertTree(treeId);
_assertDataBranch(treeId, branch);
if (treeId == TREE_PHI || treeId == TREE_VASSET || treeId == TREE_ORACLE) {
revert TypedTreeOnly(treeId);
}
// Trees 7 and 8 have their own rulers and NO quorum path at all: an
// intent's status is what `FinalIntentLog` verified, an identity is
// what the registry or the ledger verified, and no set of service
// signatures can make a different answer true.
if (treeId == TREE_INTENTS || treeId == TREE_IDENTITY) revert WriterOnlyTree(treeId);
if (keys.length != leaves.length) revert LengthMismatch(keys.length, leaves.length);
uint256 k = threshold[treeId];
if (k == 0) revert TreeNotConfigured(treeId);
uint64 n = nonce[treeId];
FinalPqQuorum.require_(
registry,
approvals,
FinalPqQuorum.digest(
address(this),
ACTION_SET_LEAVES,
anchorBlock,
keccak256(abi.encode(treeId, branch, n, keys, leaves))
),
writerRole[treeId],
k,
FinalPqQuorum.ALG_ML_DSA_87,
anchorBlock,
false
);
nonce[treeId] = n + 1;
for (uint256 i = 0; i < keys.length; i++) {
_set(treeId, branch, keys[i], leaves[i]);
}
_bump(treeId, keys.length);
}
/// @notice One chain an account exists on, and as what.
/// @dev `chainRef` is the registry's CAIP-derived chain reference — the one
/// identifier that names an EVM chain and a non-EVM one alike — and
/// `account` is the wallet's account there, in that chain's own account
/// space (an EVM address right-aligned, a 32-byte key filling the
/// width). Field-for-field with `IWalletTypes.ChainAccount`.
struct ChainAccount {
bytes32 chainRef;
bytes32 account;
}
/// @notice `FinalWalletFactory.AccountStateLeaf`, field for field.
struct AccountStateLeaf {
address wallet;
bytes32 liveAccess;
bytes32 liveTransaction;
bytes32 recoveryAccess;
bytes32 recoveryTransaction;
/// @dev Active-stage encapsulation commitment and its pre-committed
/// successor. Field-for-field with `FinalWalletFactory.AccountStateLeaf`;
/// a field added on one side and not the other is a root every execution
/// chain rejects, with nothing pointing at the cause.
bytes32 liveKem;
bytes32 recoveryKem;
address owner;
bool pqEnabled;
bool frozen;
/// @dev The chains this account exists on, and its account on each —
/// including chains whose accounts are not EVM addresses. Decided HERE
/// (set by the holder through the ledger) and enforced there: an
/// execution chain refuses to create the account unless the table has a
/// row for it, and a settlement toward a chain with no row is refused at
/// the source. This is what a zero beneficiary resolves through; it
/// replaced a bitmask over registry slots that could only say "may
/// exist", never "as what".
ChainAccount[] deployedChains;
/// @dev Per-chain dormancy verdict, one bit per asset-registry chain
/// slot. Keeps the slot space the bitmask had.
uint32 dormantChains;
uint64 version;
}
/**
* @notice Write account state into tree 1 from the typed leaf.
* @dev The typed form exists so the leaf preimage is built HERE rather than
* by whoever assembles the calldata. Tree 1 is the source of truth for every
* other chain, and `syncAccountState` will accept any 32 bytes that carry a
* valid proof — so if the publisher chose the preimage, the publisher could
* write an account state that no wallet record on this chain agrees with,
* and the proof would still verify everywhere.
*
* **Sealed.** Tree 1 is membership: a leaf here is who an account is, on
* every chain. So the round takes the hybrid class — each approval carries
* the ML-DSA-87 vote AND the member's SLH-DSA seal — where the other trees
* take the transaction class alone. A lattice break rewrites a price; it
* does not rewrite an account.
*/
function setAccountStates(
AccountStateLeaf[] calldata leaves,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
uint256 k = threshold[TREE_ACCOUNTS];
if (k == 0) revert TreeNotConfigured(TREE_ACCOUNTS);
bytes32[] memory keys = new bytes32[](leaves.length);
bytes32[] memory hashes = new bytes32[](leaves.length);
for (uint256 i = 0; i < leaves.length; i++) {
keys[i] = accountKeyFor(leaves[i].wallet);
hashes[i] = accountStateLeafHash(leaves[i]);
}
uint64 n = nonce[TREE_ACCOUNTS];
FinalPqQuorum.require_(
registry,
approvals,
FinalPqQuorum.digest(
address(this),
ACTION_SET_LEAVES,
anchorBlock,
keccak256(abi.encode(TREE_ACCOUNTS, n, keys, hashes))
),
writerRole[TREE_ACCOUNTS],
k,
FinalPqQuorum.ALG_ML_DSA_87,
anchorBlock,
true
);
nonce[TREE_ACCOUNTS] = n + 1;
for (uint256 i = 0; i < leaves.length; i++) {
_set(TREE_ACCOUNTS, BRANCH_MAIN, keys[i], hashes[i]);
}
_bump(TREE_ACCOUNTS, leaves.length);
}
/**
* @notice Write account state into tree 1 from the contract that owns it.
* @dev No quorum, and no nonce burned: `treeWriter[1]` is the ledger, and
* the ledger already verified the holder's own signature before it called
* here. See {treeWriter} for why adding a service quorum on top would be a
* censorship power rather than a safeguard.
*
* Typed, exactly as `setAccountStates` is: the preimage is built HERE, so
* even the writer contract cannot publish a leaf whose meaning no record on
* this chain agrees with.
*/
function setAccountStatesAsWriter(AccountStateLeaf[] calldata leaves) external {
if (msg.sender != treeWriter[TREE_ACCOUNTS]) revert NotAuthorized(msg.sender);
for (uint256 i = 0; i < leaves.length; i++) {
_set(TREE_ACCOUNTS, BRANCH_MAIN, accountKeyFor(leaves[i].wallet), accountStateLeafHash(leaves[i]));
}
_bump(TREE_ACCOUNTS, leaves.length);
}
/**
* @notice Write raw leaves into any tree from the contract that owns it.
* @dev The generic sibling of {setAccountStatesAsWriter}, for a tree whose
* writer is a contract rather than a service quorum. Same authorization —
* `treeWriter[treeId]` and nothing else — and the same reasoning: the
* writer has already verified whatever its domain requires, and layering a
* quorum on top of a contract's own rules is a censorship power rather
* than a safeguard.
*
* UNTYPED, unlike the account path, and that is the trade. Tree 1's
* preimage is built here so even the ledger cannot publish a leaf whose
* meaning no record agrees with; a generic writer supplies its own hash,
* so the leaf means whatever that contract says it means. Acceptable only
* because the writer is a specific contract this chain's operators
* installed — its rules are its bytecode, it has no owner and no proxy —
* and NOT acceptable for a role-gated key. Point `treeWriter` at a
* contract, never at an EOA.
*/
function setLeavesAsWriter(uint8 treeId, uint8 branch, bytes32[] calldata keys, bytes32[] calldata leaves)
external
{
if (msg.sender != treeWriter[treeId]) revert NotAuthorized(msg.sender);
_assertDataBranch(treeId, branch);
if (keys.length != leaves.length) revert LengthMismatch(keys.length, leaves.length);
for (uint256 i = 0; i < keys.length; i++) {
_set(treeId, branch, keys[i], leaves[i]);
}
_bump(treeId, keys.length);
}
/// @notice The leaf hash `FinalWalletFactory.accountStateLeafHash` computes.
/// @dev Identical `abi.encode`, identical field order, identical domain.
/// Pinned against the factory by test. `deployedChains` rides through
/// `abi.encode` like every other field — head offset, then length and
/// rows — so the table is committed whole and in order.
function accountStateLeafHash(AccountStateLeaf memory leaf) public pure returns (bytes32) {
_assertChainAccounts(leaf.deployedChains);
return keccak256(
abi.encode(
DOMAIN_ACCOUNT_STATE_LEAF,
leaf.wallet,
leaf.liveAccess,
leaf.liveTransaction,
leaf.recoveryAccess,
leaf.recoveryTransaction,
leaf.liveKem,
leaf.recoveryKem,
leaf.owner,
leaf.pqEnabled,
leaf.frozen,
leaf.deployedChains,
leaf.dormantChains,
leaf.version
)
);
}
/// @dev A well-formed table: no zero chain, no zero account, no chain twice.
/// Checked where the leaf is hashed so no door — quorum, writer
/// contract, identity projection — can publish a table a resolver
/// would read two ways.
function _assertChainAccounts(ChainAccount[] memory rows) private pure {
for (uint256 i = 0; i < rows.length; i++) {
if (rows[i].chainRef == bytes32(0) || rows[i].account == bytes32(0)) {
revert InvalidChainAccount(rows[i].chainRef, rows[i].account);
}
for (uint256 j = 0; j < i; j++) {
if (rows[j].chainRef == rows[i].chainRef) {
revert InvalidChainAccount(rows[i].chainRef, rows[i].account);
}
}
}
}
/// @notice The account `wallet`'s published table names on `chainRef`, or
/// zero if it has no row there.
/// @dev A convenience over `accountStateLeafHash`'s input for readers on
/// this chain; execution chains answer the same question from their synced
/// record (`FinalWalletFactory.addressOn`).
function accountOn(AccountStateLeaf memory leaf, bytes32 chainRef) public pure returns (bytes32) {
for (uint256 i = 0; i < leaf.deployedChains.length; i++) {
if (leaf.deployedChains[i].chainRef == chainRef) return leaf.deployedChains[i].account;
}
return bytes32(0);
}
/**
* @notice The typed trees' write door — `FinalStateRecords` alone.
* @dev The quorum, the nonce and the write, shared by every typed record.
* The records contract computed the keys and hashes from the structs it
* stores; this contract admits nobody else to trees 2, 3 and 4
* (`setLeaves` refuses them), so the value there can never drift from
* the commitment here.
*
* The digest is byte-identical to `setLeaves`' over the same keys and
* hashes, deliberately: the typed entrypoints choose the PREIMAGE, not the
* authorization. A member recomputes one digest whichever door the batch
* came through, and there is no second approval shape to get wrong.
*/
function writeTyped(
uint8 treeId,
bytes32[] memory keys,
bytes32[] memory hashes,
uint64 anchorBlock,
FinalPqQuorum.Approval[] calldata approvals
) external {
if (msg.sender != typedWriter) revert NotAuthorized(msg.sender);
uint256 k = threshold[treeId];
if (k == 0) revert TreeNotConfigured(treeId);
uint64 n = nonce[treeId];
FinalPqQuorum.require_(
registry,
approvals,
FinalPqQuorum.digest(
address(this),
ACTION_SET_LEAVES,
anchorBlock,
keccak256(abi.encode(treeId, n, keys, hashes))
),
writerRole[treeId],
k,
FinalPqQuorum.ALG_ML_DSA_87,
anchorBlock,
false
);
nonce[treeId] = n + 1;
for (uint256 i = 0; i < keys.length; i++) {
_set(treeId, BRANCH_MAIN, keys[i], hashes[i]);
}
_bump(treeId, keys.length);
}
/**
* @notice Snapshot every tree's root into a new round.
* @dev Permissionless, deliberately. Every root being snapshotted was
* already authorized by its tree's quorum, so this adds no authority — it
* only fixes a moment. Requiring a signature would put a liveness
* dependency in front of publication for no security gain.
*
* A round that would change nothing is refused, so the round number cannot
* be advanced by anyone with gas to spend.
*/
function publishRound() external returns (uint64 published) {
bool changed;
for (uint8 t = 1; t <= TREE_COUNT; t++) {
if (treeVersion[t] != _publishedVersion[t]) {
changed = true;
break;
}
}
if (!changed) revert NothingToPublish();
published = round + 1;
Round storage r = _rounds[published];
for (uint8 t = 1; t <= TREE_COUNT; t++) {
r.roots[t] = liveRoot[t];
_publishedVersion[t] = treeVersion[t];
}
r.roundRoot = _foldForest(_forestLeaves(r.roots));
r.blockNumber = uint64(block.number);
// MILLISECONDS, like every instant on this chain.
r.timestamp = FinalChainTime.nowMs();
round = published;
emit RoundPublished(published, r.blockNumber, r.timestamp);
}
// ---------------------------------------------------------------- views
/// @notice Every root from one round. Index by the `TREE_*` constants;
/// index 0 is unused.
function rootsAt(uint64 which) external view returns (bytes32[TREE_COUNT + 1] memory) {
return _rounds[which].roots;
}
/// @notice One tree's root at one round.
function rootAt(uint64 which, uint8 treeId) external view returns (bytes32) {
_assertTree(treeId);
return _rounds[which].roots[treeId];
}
/// @notice The one word that commits to every tree at one round.
function roundRootAt(uint64 which) external view returns (bytes32) {
return _rounds[which].roundRoot;
}
/**
* @notice The `FOREST_BITS` siblings that take a tree's root at one round
* up to that round's root — appended to `proofFor`, they make a
* leaf provable against `roundRootAt(which)` by the same verifier.
*/
function roundProofFor(uint64 which, uint8 treeId) external view returns (bytes32[] memory path) {
_assertTree(treeId);
if (which == 0 || which > round) revert NoRounds();
bytes32[] memory level = _forestLeaves(_rounds[which].roots);
path = new bytes32[](FOREST_BITS);
uint256 idx = treeId;
uint256 n = level.length;
for (uint256 l = 0; l < FOREST_BITS; l++) {
path[l] = level[idx ^ 1];
n >>= 1;
for (uint256 i = 0; i < n; i++) {
level[i] = _pair(level[2 * i], level[2 * i + 1]);
}
idx >>= 1;
}
}
/// @notice The latest round's roots, with the block it was taken at.
function latestRound()
external
view
returns (uint64 which, bytes32[TREE_COUNT + 1] memory roots, uint64 blockNumber, uint64 timestamp)
{
which = round;
if (which == 0) revert NoRounds();
Round storage r = _rounds[which];
return (which, r.roots, r.blockNumber, r.timestamp);
}
/// @notice The raw leaf stored for a key, and whether it has a slot.
function leafOf(uint8 treeId, bytes32 key) external view returns (bytes32 leaf, bool present) {
uint256 s = _slotPlusOne[treeId][key];
if (s == 0) return (bytes32(0), false);
return (_leaf[treeId][s - 1], true);
}
/// @notice The permanent slot for a key. Reverts if it has none. The
/// slot's top `BRANCH_BITS` are its branch.
function slotOf(uint8 treeId, bytes32 key) public view returns (uint256) {
uint256 s = _slotPlusOne[treeId][key];
if (s == 0) revert UnknownKey(treeId, key);
return s - 1;
}
/// @notice The key a slot was handed to, or zero if it is still free —
/// the enumeration every branch offers: slots `branch << BRANCH_DEPTH`
/// through `+ branchSlotsUsed(treeId, branch) - 1`.
function keyAt(uint8 treeId, uint256 slot) external view returns (bytes32) {
return _keyAt[treeId][slot];
}
/// @notice Slots handed out in one branch.
function branchSlotsUsed(uint8 treeId, uint8 branch) external view returns (uint256) {
return _branchSlotsUsed[treeId][branch];
}
/// @notice One branch's root: the level-`BRANCH_DEPTH` node at its position.
function branchRoot(uint8 treeId, uint8 branch) external view returns (bytes32) {
_assertTree(treeId);
_assertBranch(branch);
return _nodeAt(treeId, BRANCH_DEPTH, branch);
}
/// @notice The first `BRANCH_DEPTH` siblings of `proofFor` — a proof
/// against the leaf's branch root rather than the tree root.
function branchProofFor(uint8 treeId, bytes32 key) external view returns (bytes32[] memory) {
_assertTree(treeId);
return _path(treeId, slotOf(treeId, key), BRANCH_DEPTH);
}
/// @notice A configuration row's value, and whether the row exists.
function configValue(uint8 treeId, bytes32 key) external view returns (bytes32 value, bool present) {
present = _slotPlusOne[treeId][key] != 0;
value = _configValue[treeId][key];
}
/// @notice The branch-0 key of a configuration row: a name the owning
/// service defines, and a sub-key (a chain reference, an asset, zero).
function configKey(bytes32 name, bytes32 sub) public pure returns (bytes32) {
return keccak256(abi.encode(DOMAIN_CONFIG_KEY, name, sub));
}
/// @notice The leaf a configuration row hashes to.
function configLeafHash(uint8 treeId, bytes32 key, bytes32 value) public pure returns (bytes32) {
return keccak256(abi.encode(DOMAIN_CONFIG_LEAF, treeId, key, value));
}
/// @notice The tree-8 branch-2 key an owner occupies.
function ownerIndexKeyFor(address owner) public pure returns (bytes32) {
return keccak256(abi.encode(DOMAIN_OWNER_INDEX_KEY, owner));
}
/// @notice The owner-index leaf: a commitment to the ledger's ordered
/// `walletsByOwner(owner)`.
function ownerIndexLeafHash(address owner, address[] memory wallets) public pure returns (bytes32) {
return keccak256(abi.encode(DOMAIN_OWNER_INDEX_LEAF, owner, wallets));
}
/// @notice The tree-8 branch-3 key of one member's slot — a ring position.
function slotKeyFor(address member, uint64 slotIndex) public pure returns (bytes32) {
return keccak256(abi.encode(DOMAIN_SLOT_KEY, member, slotIndex % SLOT_KEY_RING));
}
/**
* @notice Project slot keys into tree 8's branch 3 — the co-signers'
* per-slot KEM publics the private option seals to.
* @dev Permissionless, for {syncIdentityLeaves}' reason: the leaf VALUE
* is `slotKeySource`'s own verdict (the registry verified the member's
* signature when the key was published, and answers zero once the slot's
* window has passed), so this adds no authority and only projects. The
* registry calls it same-tx on publication; anyone may call it to retire a
* slot that lapsed by time.
*/
function syncSlotKeyLeaves(address member, uint64[] calldata slotIndexes) external {
address source = slotKeySource;
if (source == address(0)) revert SlotKeySourceUnset();
for (uint256 i = 0; i < slotIndexes.length; i++) {
_set(
TREE_IDENTITY,
BRANCH_SLOT_KEYS,
slotKeyFor(member, slotIndexes[i]),
ISlotKeySource(source).slotKeyLeafOf(member, slotIndexes[i])
);
}
_bump(TREE_IDENTITY, slotIndexes.length);
}
/// @notice The tree-8 branch-4 key of one tunnel endpoint.
function endpointKeyFor(bytes32 endpointId) public pure returns (bytes32) {
return keccak256(abi.encode(DOMAIN_ENDPOINT_KEY, endpointId));
}
/**
* @notice Project tunnel endpoints into tree 8's branch 4.
* @dev Permissionless, for {syncSlotKeyLeaves}' reason: the leaf VALUE is
* `endpointSource`'s own verdict — the registry admitted the certificate
* under the registrar quorum with the holder's proof of possession, and
* answers the revoked status once it is revoked — so this adds no authority
* and only projects. The registry calls it same-tx on registration and
* revocation; anyone may call it to re-project.
*/
function syncEndpointLeaves(bytes32[] calldata endpointIds) external {
address source = endpointSource;
if (source == address(0)) revert EndpointSourceUnset();
for (uint256 i = 0; i < endpointIds.length; i++) {
_set(
TREE_IDENTITY,
BRANCH_ENDPOINTS,
endpointKeyFor(endpointIds[i]),
IEndpointSource(source).endpointLeafOf(endpointIds[i])
);
}
_bump(TREE_IDENTITY, endpointIds.length);
}
/**
* @notice The sibling path for a key, ready for
* `FinalMerkle.verifyTaggedSortedProof` on any chain.
* @dev A view, so the backend fetches a proof with one `eth_call` instead of
* rebuilding the tree off chain. Rebuilding is where a divergence between
* what the chain holds and what a service believes it holds would come
* from, and this removes the second implementation entirely.
*/
function proofFor(uint8 treeId, bytes32 key) external view returns (bytes32[] memory) {
_assertTree(treeId);
return _path(treeId, slotOf(treeId, key), DEPTH);
}
/// @notice The empty-subtree hash at a level. Level `DEPTH` is the root of
/// a tree with nothing in it.
function emptyRoot(uint256 level) external view returns (bytes32) {
return _zero[level];
}
/// @notice The tree-1 key a wallet occupies.
function accountKeyFor(address wallet) public pure returns (bytes32) {
return keccak256(abi.encode(DOMAIN_ACCOUNT_KEY, wallet));
}
/**
* @notice Copy a registered identity into tree 1 as an account-state leaf.
* @dev Services are Final Wallets, so a service's leaf is the SAME leaf a
* user's wallet gets — `FinalWalletFactory.AccountStateLeaf`, four key
* commitments and all. There is no second shape and no second domain,
* which is what lets every chain that already consumes account state
* consume a co-signer's identity with no contract change.
*
* `owner` is the account itself: a service wallet is its own owner, having
* no separate holder to speak for it.
*
* Permissionless, and for the same reason `publishRound` is: every fact it
* writes was already authorized when it entered the registry, so this adds
* no authority and only projects. Gating it would put a liveness dependency
* in front of publishing a revocation, which is the one thing that must
* never wait.
*/
function syncIdentities(address[] calldata accounts) external {
// One table for the batch: a service is its own canonical address on
// every enabled chain, so the rows differ only in `account`.
bytes32[] memory chainRefs = _enabledChainRefs();
for (uint256 i = 0; i < accounts.length; i++) {
address who = accounts[i];
FinalIdentityRegistry.Identity memory id = registry.identityOf(who);
if (!id.registered) revert UnknownKey(TREE_ACCOUNTS, accountKeyFor(who));
(bytes32 la, bytes32 lt, bytes32 ra, bytes32 rt) = registry.keyCommitments(who);
(bytes32 lk, bytes32 rk) = registry.kemCommitments(who);
ChainAccount[] memory table = new ChainAccount[](chainRefs.length);
for (uint256 c = 0; c < chainRefs.length; c++) {
table[c] = ChainAccount({chainRef: chainRefs[c], account: bytes32(uint256(uint160(who)))});
}
AccountStateLeaf memory leaf = AccountStateLeaf({
wallet: who,
liveAccess: la,
liveTransaction: lt,
recoveryAccess: ra,
recoveryTransaction: rt,
liveKem: lk,
recoveryKem: rk,
// A service reaches every chain the registry has enabled, at
// its own address, and is never dormant: dormancy measures an
// ABSENT holder, and these identities have no holder to be
// absent.
deployedChains: table,
dormantChains: 0,
owner: who,
// Every identity here is PQ by construction — there is no other
// kind of key in this registry.
pqEnabled: true,
// Revocation is a leaf that CHANGES, not one that disappears.
// A consumer holding an old proof gets a stale `false`, which is
// why the round is the thing to pin.
frozen: id.revoked,
version: id.version
});
_set(TREE_ACCOUNTS, BRANCH_MAIN, accountKeyFor(who), accountStateLeafHash(leaf));
}
_bump(TREE_ACCOUNTS, accounts.length);
}
/// @notice The tree-8 slot key an identity occupies.
function identityKeyFor(address account) public pure returns (bytes32) {
return keccak256(abi.encode(DOMAIN_IDENTITY_TREE_KEY, account));
}
/**
* @notice Project identities into tree 8 — the wallet-creation admission
* set whose live root every execution chain anchors as its
* `currentIdentityRoot`.
*
* @dev The leaf VALUE is the registry's own verdict —
* `FinalIdentityRegistry.identityTreeLeafOf`: the execution chains'
* identity leaf while the identity stands, zero once it does not. Derived
* there rather than here because every input (serial, the six key
* commitments, standing, the CA depth pair) is registry storage, and this
* contract sits against EIP-170 while the registry does not.
*
* Permissionless, for exactly {syncIdentities}' reason: every fact
* written here was authorized when it entered the registry, so this adds
* no authority and only projects. The registry itself calls it same-tx on
* every identity mutation (register, rotate, roles, revoke, LMS-key ops),
* which is what makes the root CONTINUOUS; the open door additionally lets
* anyone retire a leaf whose standing lapsed by TIME — expiry moves no
* registry storage, so no mutation hook can ever fire for it.
*
* There is no quorum door and no writer seat (both raw doors refuse this
* tree), so the strongest thing any caller can do here is copy the
* registry's own verdict.
*/
function syncIdentityLeaves(address[] calldata accounts) external {
for (uint256 i = 0; i < accounts.length; i++) {
_set(TREE_IDENTITY, BRANCH_MAIN, identityKeyFor(accounts[i]), registry.identityTreeLeafOf(accounts[i]));
}
_bump(TREE_IDENTITY, accounts.length);
}
/**
* @notice Per-tree quorum health: can each configured tree still be written?
* @dev A threshold above the live member count is not a strict quorum, it is
* a tree that reverts forever with nothing naming the roster as the cause.
* `configureTree` refuses to create that state, but revocation can arrive at
* it later — revocation must never be blocked on quorum arithmetic, so the
* check has to be something monitoring reads rather than something the
* contract enforces after the fact.
*/
function quorumHealth()
external
view
returns (uint256[] memory live, uint256[] memory required, bool[] memory ok)
{
live = new uint256[](TREE_COUNT + 1);
required = new uint256[](TREE_COUNT + 1);
ok = new bool[](TREE_COUNT + 1);
for (uint8 t = 1; t <= TREE_COUNT; t++) {
required[t] = threshold[t];
live[t] = required[t] == 0 ? 0 : registry.liveMemberCount(writerRole[t]);
ok[t] = required[t] == 0 || live[t] >= required[t];
}
}
// -------------------------------------------------------------- internal
/// @dev The enabled chain references `chainSource` knows, or none if it is
/// unset. Read through the narrow interface so this contract need not
/// import the registry that imports it.
function _enabledChainRefs() private view returns (bytes32[] memory) {
address source = chainSource;
if (source == address(0)) return new bytes32[](0);
return IChainSource(source).enabledChainRefs();
}
function _assertTree(uint8 treeId) private pure {
if (treeId == 0 || treeId > TREE_COUNT) revert UnknownTree(treeId);
}
function _assertBranch(uint8 branch) private pure {
if (branch >= BRANCH_COUNT) revert UnknownBranch(branch);
}
/// @dev A branch a quorum or a writer may write: any but the config branch.
function _assertDataBranch(uint8 treeId, uint8 branch) private pure {
_assertBranch(branch);
if (branch == BRANCH_CONFIG) revert ConfigBranchReserved(treeId);
}
/// @dev Version + event, the tail of every write door.
function _bump(uint8 treeId, uint256 count) private {
uint64 v = treeVersion[treeId] + 1;
treeVersion[treeId] = v;
emit LeavesSet(treeId, count, liveRoot[treeId], v);
}
/// @dev `keccak256(0x01 ‖ lo ‖ hi)`, the pair sorted — the one node hash.
function _pair(bytes32 a, bytes32 b) private pure returns (bytes32) {
(bytes32 lo, bytes32 hi) = a < b ? (a, b) : (b, a);
return keccak256(abi.encodePacked(bytes1(0x01), lo, hi));
}
/// @dev The sibling path from a slot up `height` levels.
function _path(uint8 treeId, uint256 idx, uint256 height) private view returns (bytes32[] memory path) {
path = new bytes32[](height);
for (uint256 l = 0; l < height; l++) {
path[l] = _nodeAt(treeId, l, idx ^ 1);
idx >>= 1;
}
}
/// @dev The forest's leaves: the tree roots at their positions, the
/// empty tree at the rest.
function _forestLeaves(bytes32[TREE_COUNT + 1] memory roots) private view returns (bytes32[] memory level) {
level = new bytes32[](1 << FOREST_BITS);
for (uint256 p = 0; p < level.length; p++) {
level[p] = (p >= 1 && p <= TREE_COUNT) ? roots[p] : _zero[DEPTH];
}
}
/// @dev Fold a power-of-two level to its root, in place.
function _foldForest(bytes32[] memory level) private pure returns (bytes32) {
for (uint256 n = level.length; n > 1; n >>= 1) {
for (uint256 i = 0; i < n / 2; i++) {
level[i] = _pair(level[2 * i], level[2 * i + 1]);
}
}
return level[0];
}
function _set(uint8 treeId, uint8 branch, bytes32 key, bytes32 leaf) private {
uint256 s = _slotPlusOne[treeId][key];
uint256 idx;
if (s == 0) {
uint256 used = _branchSlotsUsed[treeId][branch];
if (used >= BRANCH_CAPACITY) revert BranchFull(treeId, branch);
idx = (uint256(branch) << BRANCH_DEPTH) | used;
_branchSlotsUsed[treeId][branch] = used + 1;
slotsUsed[treeId] += 1;
_slotPlusOne[treeId][key] = idx + 1;
_keyAt[treeId][idx] = key;
} else {
idx = s - 1;
uint8 have = uint8(idx >> BRANCH_DEPTH);
if (have != branch) revert BranchMismatch(treeId, key, have, branch);
}
_leaf[treeId][idx] = leaf;
bytes32 cursor = keccak256(abi.encodePacked(bytes1(0x00), leaf));
for (uint256 l = 0; l < DEPTH; l++) {
cursor = _pair(cursor, _nodeAt(treeId, l, idx ^ 1));
idx >>= 1;
_node[treeId][l + 1][idx] = cursor;
}
liveRoot[treeId] = cursor;
}
/// @dev Level 0 is derived from the leaf store rather than duplicated into
/// `_node`, so there is one place a leaf lives and no way for the two to
/// disagree. Unset positions fall through to the empty-subtree hash.
function _nodeAt(uint8 treeId, uint256 level, uint256 index) private view returns (bytes32) {
if (level == 0) {
return keccak256(abi.encodePacked(bytes1(0x00), _leaf[treeId][index]));
}
bytes32 v = _node[treeId][level][index];
return v == bytes32(0) ? _zero[level] : v;
}
}
contracts/utils/FinalBundleTree.sol
// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.20;
/// @notice Empty-bundle guard. File-level so every caller reverts with the same
/// selector — `PqAnchorModule` and `PaymasterModule` on the execution
/// chain, `FinalBundleLog` on Final Chain.
error EmptyBundleTree();
/**
* @title FinalBundleTree
* @notice The fold from a bundle's intent leaves to its root, in one place.
*
* @dev Two chains compute this root and they must agree exactly. The gateway
* folds it from the intents it is about to dispatch (`PqAnchorModule`);
* `FinalBundleLog` folds it from the leaves being anchored, so that the
* payload it stores is RECOMPUTED rather than asserted by whoever
* assembled the transaction. A divergence between the two would not be a
* failed verification — it would be a bundle that anchors on Final Chain
* and can never execute, or worse.
*
* So the fold lives here rather than being written twice. There is a third
* copy in `FinalBackend/src/mmr/pqBundle.js`, kept honest by the cross-repo
* parity suite.
*
* The padding discipline is the whole point of the odd-tail branch: the
* `POS_LIFT` tag is what makes `Root([a,b,c]) != Root([a,b,c,c])`. A
* self-paired tail would let a bundle be re-presented with its last intent
* duplicated under the same root.
*/
library FinalBundleTree {
/// @dev Position tag for an interior node. Distinct from the leaf tag so a
/// node hash can never be reinterpreted as a leaf.
uint8 internal constant POS_NODE = 0x01;
/// @dev Position tag for the lift of an odd trailing element.
uint8 internal constant POS_LIFT = 0x02;
/// @dev Domain separator for both interior forms.
bytes32 internal constant DOMAIN_PQ_NODE = keccak256("FINAL_PQ_BUNDLE_NODE_v01");
/**
* @notice Fold a leaf layer to its root.
*
* @dev Takes a layer rather than a bundle so a mixed bundle's PQ **subset**
* can be folded — Final Chain never saw the pre-PQ rows and cannot
* commit to leaves it did not build.
*
* Mutates nothing the caller can observe: `leaves` is read on the first
* pass and every later layer is freshly allocated.
*/
function foldLeaves(bytes32[] memory leaves) internal pure returns (bytes32 root) {
if (leaves.length == 0) revert EmptyBundleTree();
bytes32[] memory layer = leaves;
while (layer.length > 1) {
uint256 srcLen = layer.length;
uint256 pairCount = srcLen >> 1;
uint256 oddTail = srcLen & 1;
bytes32[] memory next = new bytes32[](pairCount + oddTail);
for (uint256 i = 0; i < pairCount; i++) {
next[i] = keccak256(
abi.encodePacked(POS_NODE, DOMAIN_PQ_NODE, layer[i << 1], layer[(i << 1) | 1])
);
}
if (oddTail == 1) {
next[pairCount] = keccak256(
abi.encodePacked(POS_LIFT, DOMAIN_PQ_NODE, layer[srcLen - 1])
);
}
layer = next;
}
return layer[0];
}
}
contracts/utils/FinalMmr.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
//
// @author Final DeFi
// @version 1.0.0
pragma solidity ^0.8.20;
/**
* @title FinalMmr
* @notice Production append-only-log (Merkle-Mountain-Range) **inclusion**
* verification — positional and domain-separated, replacing the legacy
* sorted-pair fold whose loss of position re-opened leaf/node second-preimage
* ambiguity (FinalWallet audit F-02 follow-up). The algorithm is the RFC 6962 /
* Trillian inclusion decomposition, which verifies a leaf against the
* Merkle-Tree-Hash of an arbitrarily-sized (not just power-of-two) append-only
* log — i.e. an MMR over the log's perfect-subtree "peaks".
*
* ## Canonical layout (the off-chain Final-Chain MMR MUST match this exactly)
*
* - **leaf**: `keccak256(0x00 ‖ domain ‖ payload)`
* - **node**: `keccak256(0x01 ‖ domain ‖ left ‖ right)` (positional: `left`
* is always the lower-index child)
*
* The 1-byte `0x00`/`0x01` tags are the load-bearing safety property: a 64-byte
* internal node can never be reinterpreted as a leaf preimage (and vice-versa),
* closing the second-preimage class that an untagged or sorted construction
* leaves open. `domain` is the caller's commitment-space separator (e.g.
* `DOMAIN_PQ_MMR`) so two distinct logs that share this library cannot have a
* proof from one accepted by the other.
*
* ## Proof shape: `(index, size, proof[])`
*
* - `index` — 0-based position of the leaf in the log.
* - `size` — total leaf count the `root` commits to.
* - `proof` — the audit path. Its structure is **fully determined** by
* `(index, size)`:
* - `inner = bitLength(index ^ (size - 1))` positional siblings (combined
* left/right per the corresponding bit of `index`), followed by
* - `border = popcount(index >> inner)` perfect-subtree peak hashes, each
* folded in as a left sibling (the right-to-left peak bag).
* Because the length and the left/right schedule are derived from
* `(index, size)` — never from the prover — a malformed, padded, or reshaped
* proof cannot silently validate: `computeRoot` reverts on a length mismatch
* and an out-of-range index.
*
* Pure library, no storage, no external calls. Hashing uses
* `abi.encodePacked` (same construction shape as
* `PqAnchorModule._computeBundleMerkleRoot`) for auditability over inline-asm
* micro-optimization — the cost is one inclusion proof per bundle (~log₂N
* hashes), not a per-intent hot path.
*/
library FinalMmr {
/// @dev Leaf domain tag. Prepended before `domain` + payload.
uint8 internal constant LEAF_TAG = 0x00;
/// @dev Internal-node domain tag. Prepended before `domain` + (left,right).
uint8 internal constant NODE_TAG = 0x01;
/// @notice Leaf index is not strictly less than the committed `size`.
error MmrIndexOutOfRange(uint256 index, uint256 size);
/// @notice `proof.length` does not equal the `(index, size)` decomposition.
error MmrProofLengthMismatch(uint256 expected, uint256 actual);
/// @notice `size` is zero — an empty log commits to no leaves.
error MmrEmptyLog();
/// @notice Domain-separated MMR leaf hash: `keccak256(0x00 ‖ domain ‖ payload)`.
/// @param domain Commitment-space separator (e.g. `DOMAIN_PQ_MMR`).
/// @param payload The value being committed as a leaf (e.g. a bundle root).
/// @return leaf The tagged leaf hash.
function hashLeaf(bytes32 domain, bytes32 payload) internal pure returns (bytes32 leaf) {
return keccak256(abi.encodePacked(LEAF_TAG, domain, payload));
}
/// @dev Domain-separated, positional internal node: `keccak256(0x01 ‖ domain ‖ left ‖ right)`.
function _hashNode(bytes32 domain, bytes32 left, bytes32 right) private pure returns (bytes32 node) {
return keccak256(abi.encodePacked(NODE_TAG, domain, left, right));
}
/// @notice Recompute the append-only-log root implied by a leaf's inclusion proof.
/// @dev Reverts (never silently returns a wrong root) on an out-of-range index,
/// an empty log, or a proof whose length does not match the canonical
/// `(index, size)` decomposition. The caller compares the result to the
/// trusted anchor.
/// @param leaf The already-`hashLeaf`-tagged leaf.
/// @param index 0-based leaf position.
/// @param size Total committed leaf count.
/// @param domain Commitment-space separator (must match the leaf's domain).
/// @param proof Audit path (`inner` positional siblings then `border` peaks).
/// @return root The recomputed log root.
function computeRoot(
bytes32 leaf,
uint256 index,
uint256 size,
bytes32 domain,
bytes32[] memory proof
) internal pure returns (bytes32 root) {
if (size == 0) revert MmrEmptyLog();
if (index >= size) revert MmrIndexOutOfRange(index, size);
// RFC 6962 / Trillian decomposition. `inner` is the number of levels
// at which the node still has a right subtree within the (possibly
// imperfect) tree; `border` is the number of perfect-subtree peaks to
// the left that must be bagged in afterwards.
uint256 inner = bitLength(index ^ (size - 1));
uint256 border = popcount(index >> inner);
uint256 expectedLen = inner + border;
if (proof.length != expectedLen) revert MmrProofLengthMismatch(expectedLen, proof.length);
bytes32 res = leaf;
// Phase 1 — `inner` positional combines. The i-th bit of `index`
// decides whether the running hash is the left (bit 0) or right (bit 1)
// child at that level.
for (uint256 i = 0; i < inner; ) {
bytes32 sibling = proof[i];
if ((index >> i) & 1 == 0) {
res = _hashNode(domain, res, sibling); // running hash is the left child
} else {
res = _hashNode(domain, sibling, res); // running hash is the right child
}
unchecked { ++i; }
}
// Phase 2 — `border` peak bag. Each remaining sibling is a completed
// perfect-subtree peak to the LEFT, folded right-to-left.
for (uint256 i = inner; i < expectedLen; ) {
res = _hashNode(domain, proof[i], res);
unchecked { ++i; }
}
return res;
}
/// @notice Verify `leaf` is the `index`-th of `size` leaves committed by `root`.
/// @dev Convenience wrapper over `computeRoot`. The leaf must already be
/// `hashLeaf`-tagged. Returns `false` only when the recomputed root differs;
/// structurally invalid proofs revert inside `computeRoot` (fail-closed).
function verifyInclusion(
bytes32 leaf,
uint256 index,
uint256 size,
bytes32 domain,
bytes32[] memory proof,
bytes32 root
) internal pure returns (bool) {
return computeRoot(leaf, index, size, domain, proof) == root;
}
/// @notice Verify inclusion of a raw `payload` (hashes the leaf internally).
function verifyInclusionOfPayload(
bytes32 payload,
uint256 index,
uint256 size,
bytes32 domain,
bytes32[] memory proof,
bytes32 root
) internal pure returns (bool) {
return computeRoot(hashLeaf(domain, payload), index, size, domain, proof) == root;
}
/// @notice Bit length of `x` (position of the highest set bit + 1); `0` for `x == 0`.
/// @dev `internal` rather than `private` so `FinalBundleLog` can derive a
/// proof's shape with the SAME arithmetic the verifier derives it with.
/// Two copies of this decomposition is a proof built to one shape and
/// checked against another, which reverts on the gateway naming neither.
function bitLength(uint256 x) internal pure returns (uint256 n) {
while (x != 0) {
x >>= 1;
unchecked { ++n; }
}
}
/// @notice Population count (number of set bits) of `x`.
/// @dev `internal` for the same reason as {bitLength}.
function popcount(uint256 x) internal pure returns (uint256 c) {
while (x != 0) {
unchecked {
c += x & 1;
x >>= 1;
}
}
}
}
abi
[
{
"type": "constructor",
"inputs": [
{
"name": "registry_",
"type": "address",
"internalType": "contract FinalIdentityRegistry"
},
{
"name": "intentLog_",
"type": "address",
"internalType": "contract FinalIntentLog"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "ACTION_CONFIGURE",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "ACTION_SEED",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "ACTION_SEED_PAYLOADS",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "DOMAIN",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "append",
"inputs": [
{
"name": "termsLeaves",
"type": "bytes32[]",
"internalType": "bytes32[]"
},
{
"name": "bundles",
"type": "bytes32[][]",
"internalType": "bytes32[][]"
},
{
"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": [
{
"name": "firstIndex",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "appendPayloads",
"inputs": [
{
"name": "payloads",
"type": "bytes32[]",
"internalType": "bytes32[]"
},
{
"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": [
{
"name": "firstIndex",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "nonpayable"
},
{
"type": "function",
"name": "bundleAt",
"inputs": [
{
"name": "leafIndex",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "atSize",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "payload",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "proof",
"type": "bytes32[]",
"internalType": "bytes32[]"
},
{
"name": "root_",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "configure",
"inputs": [
{
"name": "role",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "k",
"type": "uint256",
"internalType": "uint256"
},
{
"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": "function",
"name": "head",
"inputs": [],
"outputs": [
{
"name": "root_",
"type": "bytes32",
"internalType": "bytes32"
},
{
"name": "size_",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "indexOf",
"inputs": [
{
"name": "payload",
"type": "bytes32",
"internalType": "bytes32"
}
],
"outputs": [
{
"name": "index",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "found",
"type": "bool",
"internalType": "bool"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "intentLog",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract FinalIntentLog"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "nonce",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint64",
"internalType": "uint64"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "payloadAt",
"inputs": [
{
"name": "index",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "payloadsBetween",
"inputs": [
{
"name": "from",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "to",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "out",
"type": "bytes32[]",
"internalType": "bytes32[]"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "peaks",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bytes32[]",
"internalType": "bytes32[]"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "peaksAt",
"inputs": [
{
"name": "atSize",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "bag",
"type": "bytes32[]",
"internalType": "bytes32[]"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "proofAt",
"inputs": [
{
"name": "leafIndex",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "atSize",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "proof",
"type": "bytes32[]",
"internalType": "bytes32[]"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "registry",
"inputs": [],
"outputs": [
{
"name": "",
"type": "address",
"internalType": "contract FinalIdentityRegistry"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "root",
"inputs": [],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "rootAt",
"inputs": [
{
"name": "atSize",
"type": "uint256",
"internalType": "uint256"
}
],
"outputs": [
{
"name": "",
"type": "bytes32",
"internalType": "bytes32"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "seed",
"inputs": [
{
"name": "peaks_",
"type": "bytes32[]",
"internalType": "bytes32[]"
},
{
"name": "size_",
"type": "uint256",
"internalType": "uint256"
},
{
"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": "function",
"name": "seedPayloads",
"inputs": [
{
"name": "payloads",
"type": "bytes32[]",
"internalType": "bytes32[]"
},
{
"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": "function",
"name": "size",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "threshold",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "function",
"name": "writerRole",
"inputs": [],
"outputs": [
{
"name": "",
"type": "uint256",
"internalType": "uint256"
}
],
"stateMutability": "view"
},
{
"type": "event",
"name": "BundleAppended",
"inputs": [
{
"name": "index",
"type": "uint256",
"indexed": true,
"internalType": "uint256"
},
{
"name": "payload",
"type": "bytes32",
"indexed": true,
"internalType": "bytes32"
},
{
"name": "root",
"type": "bytes32",
"indexed": false,
"internalType": "bytes32"
},
{
"name": "size",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"type": "event",
"name": "LogConfigured",
"inputs": [
{
"name": "writerRole",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "threshold",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"type": "event",
"name": "Seeded",
"inputs": [
{
"name": "size",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
},
{
"name": "peaks",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"type": "event",
"name": "SeededPayloads",
"inputs": [
{
"name": "size",
"type": "uint256",
"indexed": false,
"internalType": "uint256"
}
],
"anonymous": false
},
{
"type": "error",
"name": "AnchorAhead",
"inputs": [
{
"name": "anchorBlock",
"type": "uint64",
"internalType": "uint64"
},
{
"name": "blockNumber",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"type": "error",
"name": "AnchorStale",
"inputs": [
{
"name": "anchorBlock",
"type": "uint64",
"internalType": "uint64"
},
{
"name": "blockNumber",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"type": "error",
"name": "BadSeal",
"inputs": [
{
"name": "signer",
"type": "address",
"internalType": "address"
}
]
},
{
"type": "error",
"name": "BadSignature",
"inputs": [
{
"name": "signer",
"type": "address",
"internalType": "address"
},
{
"name": "algorithm",
"type": "uint8",
"internalType": "uint8"
}
]
},
{
"type": "error",
"name": "EmptyBatch",
"inputs": []
},
{
"type": "error",
"name": "EmptyBundleLeaves",
"inputs": [
{
"name": "bundleIndex",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"type": "error",
"name": "EmptyBundleTree",
"inputs": []
},
{
"type": "error",
"name": "LogNotConfigured",
"inputs": []
},
{
"type": "error",
"name": "MmrEmptyLog",
"inputs": []
},
{
"type": "error",
"name": "MmrIndexOutOfRange",
"inputs": [
{
"name": "index",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "size",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"type": "error",
"name": "NotAuthorized",
"inputs": [
{
"name": "caller",
"type": "address",
"internalType": "address"
}
]
},
{
"type": "error",
"name": "NotFresh",
"inputs": []
},
{
"type": "error",
"name": "PeaksMismatch",
"inputs": [
{
"name": "expected",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "given",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"type": "error",
"name": "PrecompileUnavailable",
"inputs": [
{
"name": "precompile",
"type": "address",
"internalType": "address"
}
]
},
{
"type": "error",
"name": "ProofShapeMismatch",
"inputs": [
{
"name": "expected",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "walked",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"type": "error",
"name": "SignerLacksRole",
"inputs": [
{
"name": "signer",
"type": "address",
"internalType": "address"
},
{
"name": "roleMask",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"type": "error",
"name": "SignersNotAscending",
"inputs": [
{
"name": "previous",
"type": "address",
"internalType": "address"
},
{
"name": "next",
"type": "address",
"internalType": "address"
}
]
},
{
"type": "error",
"name": "SizeAhead",
"inputs": [
{
"name": "asked",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "held",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"type": "error",
"name": "TermsLeafCountMismatch",
"inputs": [
{
"name": "termsLeaves",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "bundles",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"type": "error",
"name": "ThresholdIsZero",
"inputs": []
},
{
"type": "error",
"name": "ThresholdNotMet",
"inputs": [
{
"name": "valid",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "required",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"type": "error",
"name": "ThresholdUnreachable",
"inputs": [
{
"name": "live",
"type": "uint256",
"internalType": "uint256"
},
{
"name": "required",
"type": "uint256",
"internalType": "uint256"
}
]
},
{
"type": "error",
"name": "WrongAlgorithm",
"inputs": [
{
"name": "signer",
"type": "address",
"internalType": "address"
},
{
"name": "got",
"type": "uint8",
"internalType": "uint8"
},
{
"name": "required",
"type": "uint8",
"internalType": "uint8"
}
]
},
{
"type": "error",
"name": "ZeroBundleRoot",
"inputs": []
},
{
"type": "error",
"name": "ZeroIntentLog",
"inputs": []
},
{
"type": "error",
"name": "ZeroTermsLeaf",
"inputs": [
{
"name": "bundleIndex",
"type": "uint256",
"internalType": "uint256"
}
]
}
]odczyt kontraktu
bajtkod · 9,427 bajtów
0x6080806040526004361015610012575f80fd5b5f3560e01c90816304fedb2f14611597575080631a30f07914611579578063205a094e1461155f5780632441c09b1461104157806342cde4e81461102457806348d316ac14610fd457806352a9674b14610f9a5780635bb8a95114610f7c57806362984f8814610ce65780636ce6041714610cb35780636f4ce56a14610c8757806372f56b2c14610c4d5780637b10399914610c095780638f7dcfa314610be5578063949d225d14610bc857806394bc4e96146108a5578063affed0e01461087f578063b19f480514610845578063c0131f5914610801578063ca2869a0146107db578063cba57358146107bf578063cba8bdf714610382578063ebb3eedb146102d6578063ebf0c717146102b95763f47f54cb1461012f575f80fd5b346102b55761013d3661165e565b9392919082156102a657600154918215610297576001600160401b03916102506102569260025495858716936006549a8b8b6101a78c61019960405193849260208401968d88526040850152606080850152608084019161198a565b03601f1981018352826116f8565b51902060405160208101917fd850f5df47b124511e8e6ec99cf1a0beaf7c6237eff0a31305ce53d85f31267583524660408301523060608301527f5985b2aa0699a556c4b84df321b016abe612f656b53dfdb4741aaa1912f686b460808301528a871660a083015260c082015260c0815261022360e0826116f8565b519020905f54927f0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d540611c48565b506119ae565b16906001600160401b031916176002555f5b81811061027a57602084604051908152f35b8061029161028b60019385876119cc565b35611edd565b01610268565b6382d4481f60e01b5f5260045ffd5b63c2e5347d60e01b5f5260045ffd5b5f80fd5b346102b5575f3660031901126102b5576020600754604051908152f35b346102b5576102e4366115cf565b60065480821161036c57508082116103565761030861030383836116de565b611744565b91805b82811061032c5760405160208082528190610328908201876115e5565b0390f35b806001915f52600460205260405f205461034f61034985846116de565b87611797565b520161030b565b906388c73b2960e01b5f5260045260245260445ffd5b90635b8d5fdb60e11b5f5260045260245260445ffd5b346102b55760803660031901126102b5576004356001600160401b0381116102b5576103b2903690600401611618565b602435906103be611648565b6064356001600160401b0381116102b5576103dd903690600401611618565b6040516328305db160e21b81527f0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d5406001600160a01b03169290602081600481875afa90811561067b575f91610790575b50801561072a575b61058c575b5050505060065461057d575f82805b610559575081810361054457505f8060ff5b60018086831c16146104e1575b801561049057801561047c575f190161045b565b634e487b7160e01b5f52601160045260245ffd5b7f67f9b61bf7b39fd24dd60467083f89ea77979db358db2804069474590a36c035604086868160065581155f146104d3575f5b60075582519182526020820152a1005b6104dc82611b22565b6104c3565b906104ed8385886119cc565b35156105355761052f9061050b61050385611be3565b9486896119cc565b35835f52600360205260405f2082851c5f5260205260405f20556001831b906116eb565b90610468565b634425ca1360e01b5f5260045ffd5b63ecc9b8ed60e01b5f5260045260245260445ffd5b60018082161461056d575b60011c80610449565b9061057790611be3565b90610564565b63dc63d81f60e01b5f5260045ffd5b6040516020810190604082526105be816105aa606082018a8d61198a565b8a604083015203601f1981018352826116f8565b519020833b156102b55790826001600160401b039593926040519687956322f3f44760e11b875260848701927f405bbda3343b6e69c32fb7eafff8f0a1e55a5ee2ec35458b3abc776b2668195260048901526024880152166044860152608060648601525260a4830160a060048460051b8601010192825f90607e19813603015b8383106106865750505050505091815f818582965003925af1801561067b5761066b575b80808061043a565b5f610675916116f8565b83610663565b6040513d5f823e3d90fd5b60a3198a8803018552949650929491939092918635828112156102b557830180356001600160a01b038116908190036102b557825260208101359160ff83168093036102b55761071860209282600195858095015261070a6106ff6106ee6040850185611a13565b608060408601526080850191611a44565b926060810190611a13565b916060818503910152611a44565b9801960193019091889695949261063f565b5060405163f5778b0360e01b8152602081600481875afa90811561067b575f91610761575b506001600160a01b0316331415610435565b610783915060203d602011610789575b61077b81836116f8565b8101906119f4565b8861074f565b503d610771565b6107b2915060203d6020116107b8575b6107aa81836116f8565b8101906119dc565b8861042d565b503d6107a0565b346102b5575f3660031901126102b55760205f54604051908152f35b346102b55760203660031901126102b55760206107f9600435611b22565b604051908152f35b346102b5575f3660031901126102b5576040517f000000000000000000000000f2b3161ed308717da082ee706cfdd27048e0d62d6001600160a01b03168152602090f35b346102b5575f3660031901126102b55760206040517f27c91cbb7cc32319dd47788e8b096cc02ee8ccca641645266d7046769a12fbc38152f35b346102b5575f3660031901126102b55760206001600160401b0360025416604051908152f35b346102b55760803660031901126102b5576024356004356108c4611648565b6064356001600160401b0381116102b5576108e3903690600401611618565b6040516328305db160e21b81527f0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d5406001600160a01b0316939290602081600481885afa90811561067b575f91610ba9575b508015610b53575b610a01575b50505082610983575b7fbfc08a458e488f0e56f7ff4bfe317bed1ba5d3f7ef5a2bda241528695f6fcdf360408385815f558060015582519182526020820152a1005b60206024916040519283809263342f616360e01b82528660048301525afa90811561067b575f916109cf575b508281101561094a579050633770da3360e11b5f5260045260245260445ffd5b90506020813d6020116109f9575b816109ea602093836116f8565b810103126102b55751836109af565b3d91506109dd565b604051602081019086825287604082015260408152610a216060826116f8565b519020843b156102b55790826001600160401b0394926040519586946322f3f44760e11b865260848601927f27c91cbb7cc32319dd47788e8b096cc02ee8ccca641645266d7046769a12fbc360048801526024870152166044850152608060648501525260a4820160a060048560051b8501010193825f90607e19813603015b838310610adb5750505050505080825f9350038183865af1801561067b57610acb575b8080610941565b5f610ad5916116f8565b83610ac4565b60a3198989030185529496939550919390928635828112156102b557830180356001600160a01b038116908190036102b557825260208101359160ff83168093036102b557610b4260209282600195858095015261070a6106ff6106ee6040850185611a13565b980196019301909187959492610aa1565b5060405163f5778b0360e01b8152602081600481885afa90811561067b575f91610b8a575b506001600160a01b031633141561093c565b610ba3915060203d6020116107895761077b81836116f8565b87610b78565b610bc2915060203d6020116107b8576107aa81836116f8565b87610934565b346102b5575f3660031901126102b5576020600654604051908152f35b346102b5575f3660031901126102b557604060075460065482519182526020820152f35b346102b5575f3660031901126102b5576040517f0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d5406001600160a01b03168152602090f35b346102b5575f3660031901126102b55760206040517f405bbda3343b6e69c32fb7eafff8f0a1e55a5ee2ec35458b3abc776b266819528152f35b346102b55760203660031901126102b5576040610ca5600435611af8565b825191825215156020820152f35b346102b55760203660031901126102b557610328610cd2600435611a64565b6040519182916020835260208301906115e5565b346102b557610cf43661165e565b6040516328305db160e21b81529394937f0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d5406001600160a01b03169290602081600481875afa90811561067b575f91610f5d575b508015610f07575b610db2575b5050505060065461057d5781156102a6575f5b828110610d9b577f1c295873c1ce4ce2ac720f43d6909e66b931b42e9246b862278eba9624c0bf05602084604051908152a1005b80610dac61028b60019386866119cc565b01610d67565b604051602081019060208252610dd081610199604082018b8b61198a565b519020833b156102b55790826001600160401b039593926040519687956322f3f44760e11b875260848701927f9abdf9961fd14fd177480eccbad16b2d7f231898b2d763c8e8b50364d8b3b17160048901526024880152166044860152608060648601525260a4830160a060048460051b8601010192825f90607e19813603015b838310610e8d5750505050505091815f818582965003925af1801561067b57610e7d575b808080610d54565b5f610e87916116f8565b82610e75565b60a3198a8803018552949650929491939092918635828112156102b557830180356001600160a01b038116908190036102b557825260208101359160ff83168093036102b557610ef560209282600195858095015261070a6106ff6106ee6040850185611a13565b98019601930190918896959492610e51565b5060405163f5778b0360e01b8152602081600481875afa90811561067b575f91610f3e575b506001600160a01b0316331415610d4f565b610f57915060203d6020116107895761077b81836116f8565b87610f2c565b610f76915060203d6020116107b8576107aa81836116f8565b87610d47565b346102b5575f3660031901126102b557610328610cd2600654611a64565b346102b5575f3660031901126102b55760206040517fb66ca34dc0d9a9daa6230aee35894330ccfa7e4eaa29a198577eed0b26a412058152f35b346102b557610fe2366115cf565b610ffe610ff8610ff283856117ab565b936116c3565b91611b22565b61101a60405193849384526060602085015260608401906115e5565b9060408301520390f35b346102b5575f3660031901126102b5576020600154604051908152f35b346102b55760803660031901126102b5576004356001600160401b0381116102b557611071903690600401611618565b906024356001600160401b0381116102b557611091903690600401611618565b61109c939193611648565b936064356001600160401b0381116102b5576110bc903690600401611618565b9583156102a6578385036115485760015480156102975760029795979693965492600654966040516001600160401b03861660208201528860408201526080606082015261110e60a082018c8961198a565b601f19828203016080830152888152602081019060208a60051b820101918c915f5b8c81106114dc575050505090611156816111dd979695949303601f1981018352826116f8565b6020815191012060405160208101917fd850f5df47b124511e8e6ec99cf1a0beaf7c6237eff0a31305ce53d85f31267583524660408301523060608301527f2e1c2ff2f9bb13fd926fe3e8b209f98e6c873bb259534a2148ca355409247cba60808301526001600160401b03871660a083015260c082015260c0815261022360e0826116f8565b506001600160401b036111f18183166119ae565b67ffffffffffffffff199092169116176002555f947f000000000000000000000000f2b3161ed308717da082ee706cfdd27048e0d62d6001600160a01b03165b838710156114d1578660051b860135601e19873603018112156102b55786018035906001600160401b0382116102b557602001908060051b360382136102b55780156114be576112828985876119cc565b35156114ab576001810180821161047c5761129c90611744565b916112a88a86886119cc565b356112b284611776565b525f5b82811061143457505050805115611425575b805160018111156113f9578060011c9060018116926112e961030385856116eb565b935f5b8481106113795750600114611304575b5050506112c7565b5f19820191821161047c576113709161131c91611797565b516040516020810191600160f91b83527fc976f483968b324bd57de8efa226478a3634db61776dacd4da866f8fa37c0fd560218301526041820152604181526113666061826116f8565b5190209183611797565b528880806112fc565b80600191821b6113968361138d8388611797565b51921786611797565b516040519060208201928560f81b84527fc976f483968b324bd57de8efa226478a3634db61776dacd4da866f8fa37c0fd5602184015260418301526061820152606181526113e56081826116f8565b5190206113f28289611797565b52016112ec565b50966114176114116001939699989598979497611776565b51611edd565b019592949194939093611231565b634f297b6160e11b5f5260045ffd5b61143f8184846119cc565b35853b156102b5576040519063af6f8c1b60e01b825260048201525f81602481838a5af1801561067b5761149b575b5061147a8184846119cc565b3590600181019182821161047c5761149460019387611797565b52016112b5565b5f6114a5916116f8565b8b61146e565b886322566cfd60e01b5f5260045260245ffd5b8863c9cdeff560e01b5f5260045260245ffd5b602085604051908152f35b909192939c9e9c601f9e9b9e19838203018452601e198c360301853512156102b5578b85350190602082359201916001600160401b0381116102b5578060051b360383136102b557611534602092839260019561198a565b9601940191019e9c9e9d9a9d919091611130565b8385635b2d642360e11b5f5260045260245260445ffd5b346102b557610328610cd2611573366115cf565b906117ab565b346102b55760203660031901126102b55760206107f96004356116c3565b346102b5575f3660031901126102b557807f9abdf9961fd14fd177480eccbad16b2d7f231898b2d763c8e8b50364d8b3b17160209252f35b60409060031901126102b5576004359060243590565b90602080835192838152019201905f5b8181106116025750505090565b82518452602093840193909201916001016115f5565b9181601f840112156102b5578235916001600160401b0383116102b5576020808501948460051b0101116102b557565b604435906001600160401b03821682036102b557565b9060606003198301126102b5576004356001600160401b0381116102b5578261168991600401611618565b929092916024356001600160401b03811681036102b55791604435906001600160401b0382116102b5576116bf91600401611618565b9091565b6006548082101561035657505f52600460205260405f205490565b9190820391821161047c57565b9190820180921161047c57565b90601f801991011681019081106001600160401b0382111761171957604052565b634e487b7160e01b5f52604160045260245ffd5b6001600160401b0381116117195760051b60200190565b9061174e8261172d565b61175b60405191826116f8565b828152809261176c601f199161172d565b0190602036910137565b8051156117835760200190565b634e487b7160e01b5f52603260045260245ffd5b80518210156117835760209160051b010190565b919060065480821161036c5750801561197b5780831015611965575f1981019080821161047c57906117f56103036117e4838718611bf1565b6117ef87821c611c0a565b906116eb565b9390915f835f945b61182757505050508251808203611812575050565b63383613b560e01b5f5260045260245260445ffd5b90919293600185188281105f14611874578392916001949185925f52600360205260405f20905f5260205260405f2054611861828b611797565b5201945b831c9392918201911c806117fd565b82819692961461188a575b509060019291611865565b839591951b61189981866116de565b6118a561030382611c0a565b905f92906118b281611bf1565b805b611918575050505f19820191821161047c576118d08282611797565b5191805b6118f85750506001939291818592506118ed828b611797565b52019490919261187f565b5f19019182906119129061190c8385611797565b51612048565b926118d4565b5f190160018083831c161461192e575b806118b4565b6001819395825f52600360205260405f2087841c5f5260205260405f20546119568288611797565b5201946001821b019250611928565b826388c73b2960e01b5f5260045260245260445ffd5b635bf77f6760e01b5f5260045ffd5b81835290916001600160fb1b0383116102b55760209260051b809284830137010190565b6001600160401b036001911601906001600160401b03821161047c57565b91908110156117835760051b0190565b908160209103126102b5575180151581036102b55790565b908160209103126102b557516001600160a01b03811681036102b55790565b9035601e19823603018112156102b55701602081359101916001600160401b0382116102b55781360383136102b557565b908060209392818452848401375f828201840152601f01601f1916010190565b90600654808311611ae25750611a7c61030383611c0a565b915f5f91611a8981611bf1565b805b611a955750505050565b5f190160018083831c1614611aab575b80611a8b565b6001819493825f52600360205260405f2085841c5f5260205260405f2054611ad3828a611797565b5201926001821b019350611aa5565b82635b8d5fdb60e11b5f5260045260245260445ffd5b5f52600560205260405f20548015611b1b575f19810190811161047c5790600190565b505f905f90565b60065480821161036c57508015611bde57611b3f61030382611c0a565b5f915f90611b4c81611bf1565b805b611b91575050505f19820191821161047c57611b6a8282611797565b5191805b611b7757505090565b5f1901918290611b8b9061190c8385611797565b92611b6e565b5f190160018083831c1614611ba7575b80611b4e565b6001819395825f52600360205260405f2087841c5f5260205260405f2054611bcf8288611797565b5201946001821b019250611ba1565b505f90565b5f19811461047c5760010190565b90815f925b611bfd5750565b6001928301921c80611bf6565b90815f925b611c165750565b9160018316019160011c80611c0f565b356001600160a01b03811681036102b55790565b3560ff811681036102b55790565b92939195965f978615611ece576001600160401b0316438111611eb857610258611c7282436116de565b11611ea2575060405194602086015260208552611c906040866116f8565b5f955f985b888a1015611e78578960051b840135607e19853603018112156102b557840197611cbe89611c26565b6001600160a01b039182169116811015611e4c5750611cdc88611c26565b97611ce681611c26565b604051632e4bfa5160e11b81526001600160a01b0391821660048201526024810188905290602090829060449082908c165afa90811561067b575f91611e2e575b5015611e075760208101600460ff611d3e83611c3a565b1603611dd557611d4f89838a612183565b15611da25750611d608882896122c8565b15611d795750611d71600191611be3565b990198611c95565b611d8290611c26565b63c082266360e01b5f9081526001600160a01b0391909116600452602490fd5b611db6611db060ff93611c26565b91611c3a565b9063bbf82ba360e01b5f5260018060a01b03166004521660245260445ffd5b611de3611db060ff93611c26565b9063587548c360e11b5f5260018060a01b031660045216602452600460445260645ffd5b611e118691611c26565b63ae8bb03960e01b5f5260018060a01b031660045260245260445ffd5b611e46915060203d81116107b8576107aa81836116f8565b5f611d27565b611e5589611c26565b6311641feb60e21b5f9081526004929092526001600160a01b0316602452604490fd5b98509550955050505050808310611e8c5750565b826305bc216760e51b5f5260045260245260445ffd5b630ed38fd160e41b5f526004524360245260445ffd5b637b51505560e01b5f526004524360245260445ffd5b631fc460bf60e11b5f5260045ffd5b801561053557600654805f5260046020528160405f2055815f52600560205260405f20541561202b575b60405160208101905f82527fb66ca34dc0d9a9daa6230aee35894330ccfa7e4eaa29a198577eed0b26a41205602182015283604182015260418152611f4d6061826116f8565b5190205f8281527f3617319a054d772f909f7c479a2cebe5066e836a939412e32403c99029b92eff602052604081208290559082905b600180831614611fd6575050507f1585fcb2f8b662b0e77609aa657994b280f417bea79b40989d135efaf884054860406001830180600655611fc481611b22565b908160075582519182526020820152a3565b825f52600360205260405f20905f1983019083821161047c57600192612005925f5260205260405f2054612048565b91811c920190815f52600360205260405f20835f526020528060405f2055919091611f83565b6001810180821161047c57825f52600560205260405f2055611f07565b90604051906020820192600160f81b84527fb66ca34dc0d9a9daa6230aee35894330ccfa7e4eaa29a198577eed0b26a41205602184015260418301526061820152606181526120986081826116f8565b51902090565b6001600160401b03811161171957601f01601f191660200190565b6020818303126102b5578051906001600160401b0382116102b5570181601f820112156102b5578051906120ec8261209e565b926120fa60405194856116f8565b828452602083830101116102b557815f9260208093018386015e8301015290565b903590601e19813603018212156102b557018035906001600160401b0382116102b5576020019181360383136102b557565b9291926121598261209e565b9161216760405193846116f8565b8294818452818301116102b5578281602093845f960137010152565b9160208201600460ff61219583611c3a565b16146122475760ff6121a8600592611c3a565b16146121b5575050505f90565b5f6121bf83611c26565b604051639e5adaeb60e01b81526001600160a01b0391821660048201529485916024918391165afa91821561067b57612218935f9361221b575b5061220b81604061221293019061211b565b369161214d565b9161244f565b90565b61221291935061223f61220b913d805f833e61223781836116f8565b8101906120b9565b9391506121f9565b505f61225283611c26565b60405163b7af85d760e01b81526001600160a01b0391821660048201529485916024918391165afa91821561067b57612218935f936122a4575b5061220b81604061229e93019061211b565b91612366565b61229e9193506122c061220b913d805f833e61223781836116f8565b93915061228c565b90915f6122d484611c26565b60405163ad84ad1360e01b81526001600160a01b0391821660048201529384916024918391165afa91821561067b575f9261234a575b508151158015612334575b61232d5761221261220b84606061221896019061211b565b5050505f90565b50612342606084018461211b565b905015612315565b61235f9192503d805f833e61223781836116f8565b905f61230a565b610a20815114801590612442575b61232d5760206123c75f948286958160405195869481808701998051918291018b5e8601908282018b8152815193849201905e010190878252805192839101825e0185815203601f1981018352826116f8565b51906102045afa3d1561243b573d6123de8161209e565b906123ec60405192836116f8565b81523d5f602083013e5b8161242f575b81612405575090565b905060208151910151906020811061241e575b50151590565b5f199060200360031b1b165f612418565b805160201491506123fc565b60606123f6565b5061121383511415612374565b60408151148015906124c6575b61232d5760206124af5f948286958160405195869481808701998051918291018b5e8601908282018b8152815193849201905e010190878252805192839101825e0185815203601f1981018352826116f8565b51906102055afa3d1561243b573d6123de8161209e565b506174608351141561245c56
Brak ogona metadanych CBOR — ten bajtkod zbudowano z wyłączonym cbor_metadata, ustawieniem, które nasze kontrakty przypinają dla niezmienności adresów CREATE2.
deasemblacja (pierwsze 4,000 operacji)
| pc | op | operand |
|---|---|---|
| 0000 | PUSH1 | 0x80 |
| 0002 | DUP1 | |
| 0003 | PUSH1 | 0x40 |
| 0005 | MSTORE | |
| 0006 | PUSH1 | 0x04 |
| 0008 | CALLDATASIZE | |
| 0009 | LT | |
| 000a | ISZERO | |
| 000b | PUSH2 | 0x0012 |
| 000e | JUMPI | |
| 000f | PUSH0 | |
| 0010 | DUP1 | |
| 0011 | REVERT | |
| 0012 | JUMPDEST | |
| 0013 | PUSH0 | |
| 0014 | CALLDATALOAD | |
| 0015 | PUSH1 | 0xe0 |
| 0017 | SHR | |
| 0018 | SWAP1 | |
| 0019 | DUP2 | |
| 001a | PUSH4 | 0x04fedb2f |
| 001f | EQ | |
| 0020 | PUSH2 | 0x1597 |
| 0023 | JUMPI | |
| 0024 | POP | |
| 0025 | DUP1 | |
| 0026 | PUSH4 | 0x1a30f079 |
| 002b | EQ | |
| 002c | PUSH2 | 0x1579 |
| 002f | JUMPI | |
| 0030 | DUP1 | |
| 0031 | PUSH4 | 0x205a094e |
| 0036 | EQ | |
| 0037 | PUSH2 | 0x155f |
| 003a | JUMPI | |
| 003b | DUP1 | |
| 003c | PUSH4 | 0x2441c09b |
| 0041 | EQ | |
| 0042 | PUSH2 | 0x1041 |
| 0045 | JUMPI | |
| 0046 | DUP1 | |
| 0047 | PUSH4 | 0x42cde4e8 |
| 004c | EQ | |
| 004d | PUSH2 | 0x1024 |
| 0050 | JUMPI | |
| 0051 | DUP1 | |
| 0052 | PUSH4 | 0x48d316ac |
| 0057 | EQ | |
| 0058 | PUSH2 | 0x0fd4 |
| 005b | JUMPI | |
| 005c | DUP1 | |
| 005d | PUSH4 | 0x52a9674b |
| 0062 | EQ | |
| 0063 | PUSH2 | 0x0f9a |
| 0066 | JUMPI | |
| 0067 | DUP1 | |
| 0068 | PUSH4 | 0x5bb8a951 |
| 006d | EQ | |
| 006e | PUSH2 | 0x0f7c |
| 0071 | JUMPI | |
| 0072 | DUP1 | |
| 0073 | PUSH4 | 0x62984f88 |
| 0078 | EQ | |
| 0079 | PUSH2 | 0x0ce6 |
| 007c | JUMPI | |
| 007d | DUP1 | |
| 007e | PUSH4 | 0x6ce60417 |
| 0083 | EQ | |
| 0084 | PUSH2 | 0x0cb3 |
| 0087 | JUMPI | |
| 0088 | DUP1 | |
| 0089 | PUSH4 | 0x6f4ce56a |
| 008e | EQ | |
| 008f | PUSH2 | 0x0c87 |
| 0092 | JUMPI | |
| 0093 | DUP1 | |
| 0094 | PUSH4 | 0x72f56b2c |
| 0099 | EQ | |
| 009a | PUSH2 | 0x0c4d |
| 009d | JUMPI | |
| 009e | DUP1 | |
| 009f | PUSH4 | 0x7b103999 |
| 00a4 | EQ | |
| 00a5 | PUSH2 | 0x0c09 |
| 00a8 | JUMPI | |
| 00a9 | DUP1 | |
| 00aa | PUSH4 | 0x8f7dcfa3 |
| 00af | EQ | |
| 00b0 | PUSH2 | 0x0be5 |
| 00b3 | JUMPI | |
| 00b4 | DUP1 | |
| 00b5 | PUSH4 | 0x949d225d |
| 00ba | EQ | |
| 00bb | PUSH2 | 0x0bc8 |
| 00be | JUMPI | |
| 00bf | DUP1 | |
| 00c0 | PUSH4 | 0x94bc4e96 |
| 00c5 | EQ | |
| 00c6 | PUSH2 | 0x08a5 |
| 00c9 | JUMPI | |
| 00ca | DUP1 | |
| 00cb | PUSH4 | 0xaffed0e0 |
| 00d0 | EQ | |
| 00d1 | PUSH2 | 0x087f |
| 00d4 | JUMPI | |
| 00d5 | DUP1 | |
| 00d6 | PUSH4 | 0xb19f4805 |
| 00db | EQ | |
| 00dc | PUSH2 | 0x0845 |
| 00df | JUMPI | |
| 00e0 | DUP1 | |
| 00e1 | PUSH4 | 0xc0131f59 |
| 00e6 | EQ | |
| 00e7 | PUSH2 | 0x0801 |
| 00ea | JUMPI | |
| 00eb | DUP1 | |
| 00ec | PUSH4 | 0xca2869a0 |
| 00f1 | EQ | |
| 00f2 | PUSH2 | 0x07db |
| 00f5 | JUMPI | |
| 00f6 | DUP1 | |
| 00f7 | PUSH4 | 0xcba57358 |
| 00fc | EQ | |
| 00fd | PUSH2 | 0x07bf |
| 0100 | JUMPI | |
| 0101 | DUP1 | |
| 0102 | PUSH4 | 0xcba8bdf7 |
| 0107 | EQ | |
| 0108 | PUSH2 | 0x0382 |
| 010b | JUMPI | |
| 010c | DUP1 | |
| 010d | PUSH4 | 0xebb3eedb |
| 0112 | EQ | |
| 0113 | PUSH2 | 0x02d6 |
| 0116 | JUMPI | |
| 0117 | DUP1 | |
| 0118 | PUSH4 | 0xebf0c717 |
| 011d | EQ | |
| 011e | PUSH2 | 0x02b9 |
| 0121 | JUMPI | |
| 0122 | PUSH4 | 0xf47f54cb |
| 0127 | EQ | |
| 0128 | PUSH2 | 0x012f |
| 012b | JUMPI | |
| 012c | PUSH0 | |
| 012d | DUP1 | |
| 012e | REVERT | |
| 012f | JUMPDEST | |
| 0130 | CALLVALUE | |
| 0131 | PUSH2 | 0x02b5 |
| 0134 | JUMPI | |
| 0135 | PUSH2 | 0x013d |
| 0138 | CALLDATASIZE | |
| 0139 | PUSH2 | 0x165e |
| 013c | JUMP | |
| 013d | JUMPDEST | |
| 013e | SWAP4 | |
| 013f | SWAP3 | |
| 0140 | SWAP2 | |
| 0141 | SWAP1 | |
| 0142 | DUP3 | |
| 0143 | ISZERO | |
| 0144 | PUSH2 | 0x02a6 |
| 0147 | JUMPI | |
| 0148 | PUSH1 | 0x01 |
| 014a | SLOAD | |
| 014b | SWAP2 | |
| 014c | DUP3 | |
| 014d | ISZERO | |
| 014e | PUSH2 | 0x0297 |
| 0151 | JUMPI | |
| 0152 | PUSH1 | 0x01 |
| 0154 | PUSH1 | 0x01 |
| 0156 | PUSH1 | 0x40 |
| 0158 | SHL | |
| 0159 | SUB | |
| 015a | SWAP2 | |
| 015b | PUSH2 | 0x0250 |
| 015e | PUSH2 | 0x0256 |
| 0161 | SWAP3 | |
| 0162 | PUSH1 | 0x02 |
| 0164 | SLOAD | |
| 0165 | SWAP6 | |
| 0166 | DUP6 | |
| 0167 | DUP8 | |
| 0168 | AND | |
| 0169 | SWAP4 | |
| 016a | PUSH1 | 0x06 |
| 016c | SLOAD | |
| 016d | SWAP11 | |
| 016e | DUP12 | |
| 016f | DUP12 | |
| 0170 | PUSH2 | 0x01a7 |
| 0173 | DUP13 | |
| 0174 | PUSH2 | 0x0199 |
| 0177 | PUSH1 | 0x40 |
| 0179 | MLOAD | |
| 017a | SWAP4 | |
| 017b | DUP5 | |
| 017c | SWAP3 | |
| 017d | PUSH1 | 0x20 |
| 017f | DUP5 | |
| 0180 | ADD | |
| 0181 | SWAP7 | |
| 0182 | DUP14 | |
| 0183 | DUP9 | |
| 0184 | MSTORE | |
| 0185 | PUSH1 | 0x40 |
| 0187 | DUP6 | |
| 0188 | ADD | |
| 0189 | MSTORE | |
| 018a | PUSH1 | 0x60 |
| 018c | DUP1 | |
| 018d | DUP6 | |
| 018e | ADD | |
| 018f | MSTORE | |
| 0190 | PUSH1 | 0x80 |
| 0192 | DUP5 | |
| 0193 | ADD | |
| 0194 | SWAP2 | |
| 0195 | PUSH2 | 0x198a |
| 0198 | JUMP | |
| 0199 | JUMPDEST | |
| 019a | SUB | |
| 019b | PUSH1 | 0x1f |
| 019d | NOT | |
| 019e | DUP2 | |
| 019f | ADD | |
| 01a0 | DUP4 | |
| 01a1 | MSTORE | |
| 01a2 | DUP3 | |
| 01a3 | PUSH2 | 0x16f8 |
| 01a6 | JUMP | |
| 01a7 | JUMPDEST | |
| 01a8 | MLOAD | |
| 01a9 | SWAP1 | |
| 01aa | KECCAK256 | |
| 01ab | PUSH1 | 0x40 |
| 01ad | MLOAD | |
| 01ae | PUSH1 | 0x20 |
| 01b0 | DUP2 | |
| 01b1 | ADD | |
| 01b2 | SWAP2 | |
| 01b3 | PUSH32 | 0xd850f5df47b124511e8e6ec99cf1a0beaf7c6237eff0a31305ce53d85f312675 |
| 01d4 | DUP4 | |
| 01d5 | MSTORE | |
| 01d6 | CHAINID | |
| 01d7 | PUSH1 | 0x40 |
| 01d9 | DUP4 | |
| 01da | ADD | |
| 01db | MSTORE | |
| 01dc | ADDRESS | |
| 01dd | PUSH1 | 0x60 |
| 01df | DUP4 | |
| 01e0 | ADD | |
| 01e1 | MSTORE | |
| 01e2 | PUSH32 | 0x5985b2aa0699a556c4b84df321b016abe612f656b53dfdb4741aaa1912f686b4 |
| 0203 | PUSH1 | 0x80 |
| 0205 | DUP4 | |
| 0206 | ADD | |
| 0207 | MSTORE | |
| 0208 | DUP11 | |
| 0209 | DUP8 | |
| 020a | AND | |
| 020b | PUSH1 | 0xa0 |
| 020d | DUP4 | |
| 020e | ADD | |
| 020f | MSTORE | |
| 0210 | PUSH1 | 0xc0 |
| 0212 | DUP3 | |
| 0213 | ADD | |
| 0214 | MSTORE | |
| 0215 | PUSH1 | 0xc0 |
| 0217 | DUP2 | |
| 0218 | MSTORE | |
| 0219 | PUSH2 | 0x0223 |
| 021c | PUSH1 | 0xe0 |
| 021e | DUP3 | |
| 021f | PUSH2 | 0x16f8 |
| 0222 | JUMP | |
| 0223 | JUMPDEST | |
| 0224 | MLOAD | |
| 0225 | SWAP1 | |
| 0226 | KECCAK256 | |
| 0227 | SWAP1 | |
| 0228 | PUSH0 | |
| 0229 | SLOAD | |
| 022a | SWAP3 | |
| 022b | PUSH32 | 0x0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d540 |
| 024c | PUSH2 | 0x1c48 |
| 024f | JUMP | |
| 0250 | JUMPDEST | |
| 0251 | POP | |
| 0252 | PUSH2 | 0x19ae |
| 0255 | JUMP | |
| 0256 | JUMPDEST | |
| 0257 | AND | |
| 0258 | SWAP1 | |
| 0259 | PUSH1 | 0x01 |
| 025b | PUSH1 | 0x01 |
| 025d | PUSH1 | 0x40 |
| 025f | SHL | |
| 0260 | SUB | |
| 0261 | NOT | |
| 0262 | AND | |
| 0263 | OR | |
| 0264 | PUSH1 | 0x02 |
| 0266 | SSTORE | |
| 0267 | PUSH0 | |
| 0268 | JUMPDEST | |
| 0269 | DUP2 | |
| 026a | DUP2 | |
| 026b | LT | |
| 026c | PUSH2 | 0x027a |
| 026f | JUMPI | |
| 0270 | PUSH1 | 0x20 |
| 0272 | DUP5 | |
| 0273 | PUSH1 | 0x40 |
| 0275 | MLOAD | |
| 0276 | SWAP1 | |
| 0277 | DUP2 | |
| 0278 | MSTORE | |
| 0279 | RETURN | |
| 027a | JUMPDEST | |
| 027b | DUP1 | |
| 027c | PUSH2 | 0x0291 |
| 027f | PUSH2 | 0x028b |
| 0282 | PUSH1 | 0x01 |
| 0284 | SWAP4 | |
| 0285 | DUP6 | |
| 0286 | DUP8 | |
| 0287 | PUSH2 | 0x19cc |
| 028a | JUMP | |
| 028b | JUMPDEST | |
| 028c | CALLDATALOAD | |
| 028d | PUSH2 | 0x1edd |
| 0290 | JUMP | |
| 0291 | JUMPDEST | |
| 0292 | ADD | |
| 0293 | PUSH2 | 0x0268 |
| 0296 | JUMP | |
| 0297 | JUMPDEST | |
| 0298 | PUSH4 | 0x82d4481f |
| 029d | PUSH1 | 0xe0 |
| 029f | SHL | |
| 02a0 | PUSH0 | |
| 02a1 | MSTORE | |
| 02a2 | PUSH1 | 0x04 |
| 02a4 | PUSH0 | |
| 02a5 | REVERT | |
| 02a6 | JUMPDEST | |
| 02a7 | PUSH4 | 0xc2e5347d |
| 02ac | PUSH1 | 0xe0 |
| 02ae | SHL | |
| 02af | PUSH0 | |
| 02b0 | MSTORE | |
| 02b1 | PUSH1 | 0x04 |
| 02b3 | PUSH0 | |
| 02b4 | REVERT | |
| 02b5 | JUMPDEST | |
| 02b6 | PUSH0 | |
| 02b7 | DUP1 | |
| 02b8 | REVERT | |
| 02b9 | JUMPDEST | |
| 02ba | CALLVALUE | |
| 02bb | PUSH2 | 0x02b5 |
| 02be | JUMPI | |
| 02bf | PUSH0 | |
| 02c0 | CALLDATASIZE | |
| 02c1 | PUSH1 | 0x03 |
| 02c3 | NOT | |
| 02c4 | ADD | |
| 02c5 | SLT | |
| 02c6 | PUSH2 | 0x02b5 |
| 02c9 | JUMPI | |
| 02ca | PUSH1 | 0x20 |
| 02cc | PUSH1 | 0x07 |
| 02ce | SLOAD | |
| 02cf | PUSH1 | 0x40 |
| 02d1 | MLOAD | |
| 02d2 | SWAP1 | |
| 02d3 | DUP2 | |
| 02d4 | MSTORE | |
| 02d5 | RETURN | |
| 02d6 | JUMPDEST | |
| 02d7 | CALLVALUE | |
| 02d8 | PUSH2 | 0x02b5 |
| 02db | JUMPI | |
| 02dc | PUSH2 | 0x02e4 |
| 02df | CALLDATASIZE | |
| 02e0 | PUSH2 | 0x15cf |
| 02e3 | JUMP | |
| 02e4 | JUMPDEST | |
| 02e5 | PUSH1 | 0x06 |
| 02e7 | SLOAD | |
| 02e8 | DUP1 | |
| 02e9 | DUP3 | |
| 02ea | GT | |
| 02eb | PUSH2 | 0x036c |
| 02ee | JUMPI | |
| 02ef | POP | |
| 02f0 | DUP1 | |
| 02f1 | DUP3 | |
| 02f2 | GT | |
| 02f3 | PUSH2 | 0x0356 |
| 02f6 | JUMPI | |
| 02f7 | PUSH2 | 0x0308 |
| 02fa | PUSH2 | 0x0303 |
| 02fd | DUP4 | |
| 02fe | DUP4 | |
| 02ff | PUSH2 | 0x16de |
| 0302 | JUMP | |
| 0303 | JUMPDEST | |
| 0304 | PUSH2 | 0x1744 |
| 0307 | JUMP | |
| 0308 | JUMPDEST | |
| 0309 | SWAP2 | |
| 030a | DUP1 | |
| 030b | JUMPDEST | |
| 030c | DUP3 | |
| 030d | DUP2 | |
| 030e | LT | |
| 030f | PUSH2 | 0x032c |
| 0312 | JUMPI | |
| 0313 | PUSH1 | 0x40 |
| 0315 | MLOAD | |
| 0316 | PUSH1 | 0x20 |
| 0318 | DUP1 | |
| 0319 | DUP3 | |
| 031a | MSTORE | |
| 031b | DUP2 | |
| 031c | SWAP1 | |
| 031d | PUSH2 | 0x0328 |
| 0320 | SWAP1 | |
| 0321 | DUP3 | |
| 0322 | ADD | |
| 0323 | DUP8 | |
| 0324 | PUSH2 | 0x15e5 |
| 0327 | JUMP | |
| 0328 | JUMPDEST | |
| 0329 | SUB | |
| 032a | SWAP1 | |
| 032b | RETURN | |
| 032c | JUMPDEST | |
| 032d | DUP1 | |
| 032e | PUSH1 | 0x01 |
| 0330 | SWAP2 | |
| 0331 | PUSH0 | |
| 0332 | MSTORE | |
| 0333 | PUSH1 | 0x04 |
| 0335 | PUSH1 | 0x20 |
| 0337 | MSTORE | |
| 0338 | PUSH1 | 0x40 |
| 033a | PUSH0 | |
| 033b | KECCAK256 | |
| 033c | SLOAD | |
| 033d | PUSH2 | 0x034f |
| 0340 | PUSH2 | 0x0349 |
| 0343 | DUP6 | |
| 0344 | DUP5 | |
| 0345 | PUSH2 | 0x16de |
| 0348 | JUMP | |
| 0349 | JUMPDEST | |
| 034a | DUP8 | |
| 034b | PUSH2 | 0x1797 |
| 034e | JUMP | |
| 034f | JUMPDEST | |
| 0350 | MSTORE | |
| 0351 | ADD | |
| 0352 | PUSH2 | 0x030b |
| 0355 | JUMP | |
| 0356 | JUMPDEST | |
| 0357 | SWAP1 | |
| 0358 | PUSH4 | 0x88c73b29 |
| 035d | PUSH1 | 0xe0 |
| 035f | SHL | |
| 0360 | PUSH0 | |
| 0361 | MSTORE | |
| 0362 | PUSH1 | 0x04 |
| 0364 | MSTORE | |
| 0365 | PUSH1 | 0x24 |
| 0367 | MSTORE | |
| 0368 | PUSH1 | 0x44 |
| 036a | PUSH0 | |
| 036b | REVERT | |
| 036c | JUMPDEST | |
| 036d | SWAP1 | |
| 036e | PUSH4 | 0x5b8d5fdb |
| 0373 | PUSH1 | 0xe1 |
| 0375 | SHL | |
| 0376 | PUSH0 | |
| 0377 | MSTORE | |
| 0378 | PUSH1 | 0x04 |
| 037a | MSTORE | |
| 037b | PUSH1 | 0x24 |
| 037d | MSTORE | |
| 037e | PUSH1 | 0x44 |
| 0380 | PUSH0 | |
| 0381 | REVERT | |
| 0382 | JUMPDEST | |
| 0383 | CALLVALUE | |
| 0384 | PUSH2 | 0x02b5 |
| 0387 | JUMPI | |
| 0388 | PUSH1 | 0x80 |
| 038a | CALLDATASIZE | |
| 038b | PUSH1 | 0x03 |
| 038d | NOT | |
| 038e | ADD | |
| 038f | SLT | |
| 0390 | PUSH2 | 0x02b5 |
| 0393 | JUMPI | |
| 0394 | PUSH1 | 0x04 |
| 0396 | CALLDATALOAD | |
| 0397 | PUSH1 | 0x01 |
| 0399 | PUSH1 | 0x01 |
| 039b | PUSH1 | 0x40 |
| 039d | SHL | |
| 039e | SUB | |
| 039f | DUP2 | |
| 03a0 | GT | |
| 03a1 | PUSH2 | 0x02b5 |
| 03a4 | JUMPI | |
| 03a5 | PUSH2 | 0x03b2 |
| 03a8 | SWAP1 | |
| 03a9 | CALLDATASIZE | |
| 03aa | SWAP1 | |
| 03ab | PUSH1 | 0x04 |
| 03ad | ADD | |
| 03ae | PUSH2 | 0x1618 |
| 03b1 | JUMP | |
| 03b2 | JUMPDEST | |
| 03b3 | PUSH1 | 0x24 |
| 03b5 | CALLDATALOAD | |
| 03b6 | SWAP1 | |
| 03b7 | PUSH2 | 0x03be |
| 03ba | PUSH2 | 0x1648 |
| 03bd | JUMP | |
| 03be | JUMPDEST | |
| 03bf | PUSH1 | 0x64 |
| 03c1 | CALLDATALOAD | |
| 03c2 | PUSH1 | 0x01 |
| 03c4 | PUSH1 | 0x01 |
| 03c6 | PUSH1 | 0x40 |
| 03c8 | SHL | |
| 03c9 | SUB | |
| 03ca | DUP2 | |
| 03cb | GT | |
| 03cc | PUSH2 | 0x02b5 |
| 03cf | JUMPI | |
| 03d0 | PUSH2 | 0x03dd |
| 03d3 | SWAP1 | |
| 03d4 | CALLDATASIZE | |
| 03d5 | SWAP1 | |
| 03d6 | PUSH1 | 0x04 |
| 03d8 | ADD | |
| 03d9 | PUSH2 | 0x1618 |
| 03dc | JUMP | |
| 03dd | JUMPDEST | |
| 03de | PUSH1 | 0x40 |
| 03e0 | MLOAD | |
| 03e1 | PUSH4 | 0x28305db1 |
| 03e6 | PUSH1 | 0xe2 |
| 03e8 | SHL | |
| 03e9 | DUP2 | |
| 03ea | MSTORE | |
| 03eb | PUSH32 | 0x0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d540 |
| 040c | PUSH1 | 0x01 |
| 040e | PUSH1 | 0x01 |
| 0410 | PUSH1 | 0xa0 |
| 0412 | SHL | |
| 0413 | SUB | |
| 0414 | AND | |
| 0415 | SWAP3 | |
| 0416 | SWAP1 | |
| 0417 | PUSH1 | 0x20 |
| 0419 | DUP2 | |
| 041a | PUSH1 | 0x04 |
| 041c | DUP2 | |
| 041d | DUP8 | |
| 041e | GAS | |
| 041f | STATICCALL | |
| 0420 | SWAP1 | |
| 0421 | DUP2 | |
| 0422 | ISZERO | |
| 0423 | PUSH2 | 0x067b |
| 0426 | JUMPI | |
| 0427 | PUSH0 | |
| 0428 | SWAP2 | |
| 0429 | PUSH2 | 0x0790 |
| 042c | JUMPI | |
| 042d | JUMPDEST | |
| 042e | POP | |
| 042f | DUP1 | |
| 0430 | ISZERO | |
| 0431 | PUSH2 | 0x072a |
| 0434 | JUMPI | |
| 0435 | JUMPDEST | |
| 0436 | PUSH2 | 0x058c |
| 0439 | JUMPI | |
| 043a | JUMPDEST | |
| 043b | POP | |
| 043c | POP | |
| 043d | POP | |
| 043e | POP | |
| 043f | PUSH1 | 0x06 |
| 0441 | SLOAD | |
| 0442 | PUSH2 | 0x057d |
| 0445 | JUMPI | |
| 0446 | PUSH0 | |
| 0447 | DUP3 | |
| 0448 | DUP1 | |
| 0449 | JUMPDEST | |
| 044a | PUSH2 | 0x0559 |
| 044d | JUMPI | |
| 044e | POP | |
| 044f | DUP2 | |
| 0450 | DUP2 | |
| 0451 | SUB | |
| 0452 | PUSH2 | 0x0544 |
| 0455 | JUMPI | |
| 0456 | POP | |
| 0457 | PUSH0 | |
| 0458 | DUP1 | |
| 0459 | PUSH1 | 0xff |
| 045b | JUMPDEST | |
| 045c | PUSH1 | 0x01 |
| 045e | DUP1 | |
| 045f | DUP7 | |
| 0460 | DUP4 | |
| 0461 | SHR | |
| 0462 | AND | |
| 0463 | EQ | |
| 0464 | PUSH2 | 0x04e1 |
| 0467 | JUMPI | |
| 0468 | JUMPDEST | |
| 0469 | DUP1 | |
| 046a | ISZERO | |
| 046b | PUSH2 | 0x0490 |
| 046e | JUMPI | |
| 046f | DUP1 | |
| 0470 | ISZERO | |
| 0471 | PUSH2 | 0x047c |
| 0474 | JUMPI | |
| 0475 | PUSH0 | |
| 0476 | NOT | |
| 0477 | ADD | |
| 0478 | PUSH2 | 0x045b |
| 047b | JUMP | |
| 047c | JUMPDEST | |
| 047d | PUSH4 | 0x4e487b71 |
| 0482 | PUSH1 | 0xe0 |
| 0484 | SHL | |
| 0485 | PUSH0 | |
| 0486 | MSTORE | |
| 0487 | PUSH1 | 0x11 |
| 0489 | PUSH1 | 0x04 |
| 048b | MSTORE | |
| 048c | PUSH1 | 0x24 |
| 048e | PUSH0 | |
| 048f | REVERT | |
| 0490 | JUMPDEST | |
| 0491 | PUSH32 | 0x67f9b61bf7b39fd24dd60467083f89ea77979db358db2804069474590a36c035 |
| 04b2 | PUSH1 | 0x40 |
| 04b4 | DUP7 | |
| 04b5 | DUP7 | |
| 04b6 | DUP2 | |
| 04b7 | PUSH1 | 0x06 |
| 04b9 | SSTORE | |
| 04ba | DUP2 | |
| 04bb | ISZERO | |
| 04bc | PUSH0 | |
| 04bd | EQ | |
| 04be | PUSH2 | 0x04d3 |
| 04c1 | JUMPI | |
| 04c2 | PUSH0 | |
| 04c3 | JUMPDEST | |
| 04c4 | PUSH1 | 0x07 |
| 04c6 | SSTORE | |
| 04c7 | DUP3 | |
| 04c8 | MLOAD | |
| 04c9 | SWAP2 | |
| 04ca | DUP3 | |
| 04cb | MSTORE | |
| 04cc | PUSH1 | 0x20 |
| 04ce | DUP3 | |
| 04cf | ADD | |
| 04d0 | MSTORE | |
| 04d1 | LOG1 | |
| 04d2 | STOP | |
| 04d3 | JUMPDEST | |
| 04d4 | PUSH2 | 0x04dc |
| 04d7 | DUP3 | |
| 04d8 | PUSH2 | 0x1b22 |
| 04db | JUMP | |
| 04dc | JUMPDEST | |
| 04dd | PUSH2 | 0x04c3 |
| 04e0 | JUMP | |
| 04e1 | JUMPDEST | |
| 04e2 | SWAP1 | |
| 04e3 | PUSH2 | 0x04ed |
| 04e6 | DUP4 | |
| 04e7 | DUP6 | |
| 04e8 | DUP9 | |
| 04e9 | PUSH2 | 0x19cc |
| 04ec | JUMP | |
| 04ed | JUMPDEST | |
| 04ee | CALLDATALOAD | |
| 04ef | ISZERO | |
| 04f0 | PUSH2 | 0x0535 |
| 04f3 | JUMPI | |
| 04f4 | PUSH2 | 0x052f |
| 04f7 | SWAP1 | |
| 04f8 | PUSH2 | 0x050b |
| 04fb | PUSH2 | 0x0503 |
| 04fe | DUP6 | |
| 04ff | PUSH2 | 0x1be3 |
| 0502 | JUMP | |
| 0503 | JUMPDEST | |
| 0504 | SWAP5 | |
| 0505 | DUP7 | |
| 0506 | DUP10 | |
| 0507 | PUSH2 | 0x19cc |
| 050a | JUMP | |
| 050b | JUMPDEST | |
| 050c | CALLDATALOAD | |
| 050d | DUP4 | |
| 050e | PUSH0 | |
| 050f | MSTORE | |
| 0510 | PUSH1 | 0x03 |
| 0512 | PUSH1 | 0x20 |
| 0514 | MSTORE | |
| 0515 | PUSH1 | 0x40 |
| 0517 | PUSH0 | |
| 0518 | KECCAK256 | |
| 0519 | DUP3 | |
| 051a | DUP6 | |
| 051b | SHR | |
| 051c | PUSH0 | |
| 051d | MSTORE | |
| 051e | PUSH1 | 0x20 |
| 0520 | MSTORE | |
| 0521 | PUSH1 | 0x40 |
| 0523 | PUSH0 | |
| 0524 | KECCAK256 | |
| 0525 | SSTORE | |
| 0526 | PUSH1 | 0x01 |
| 0528 | DUP4 | |
| 0529 | SHL | |
| 052a | SWAP1 | |
| 052b | PUSH2 | 0x16eb |
| 052e | JUMP | |
| 052f | JUMPDEST | |
| 0530 | SWAP1 | |
| 0531 | PUSH2 | 0x0468 |
| 0534 | JUMP | |
| 0535 | JUMPDEST | |
| 0536 | PUSH4 | 0x4425ca13 |
| 053b | PUSH1 | 0xe0 |
| 053d | SHL | |
| 053e | PUSH0 | |
| 053f | MSTORE | |
| 0540 | PUSH1 | 0x04 |
| 0542 | PUSH0 | |
| 0543 | REVERT | |
| 0544 | JUMPDEST | |
| 0545 | PUSH4 | 0xecc9b8ed |
| 054a | PUSH1 | 0xe0 |
| 054c | SHL | |
| 054d | PUSH0 | |
| 054e | MSTORE | |
| 054f | PUSH1 | 0x04 |
| 0551 | MSTORE | |
| 0552 | PUSH1 | 0x24 |
| 0554 | MSTORE | |
| 0555 | PUSH1 | 0x44 |
| 0557 | PUSH0 | |
| 0558 | REVERT | |
| 0559 | JUMPDEST | |
| 055a | PUSH1 | 0x01 |
| 055c | DUP1 | |
| 055d | DUP3 | |
| 055e | AND | |
| 055f | EQ | |
| 0560 | PUSH2 | 0x056d |
| 0563 | JUMPI | |
| 0564 | JUMPDEST | |
| 0565 | PUSH1 | 0x01 |
| 0567 | SHR | |
| 0568 | DUP1 | |
| 0569 | PUSH2 | 0x0449 |
| 056c | JUMP | |
| 056d | JUMPDEST | |
| 056e | SWAP1 | |
| 056f | PUSH2 | 0x0577 |
| 0572 | SWAP1 | |
| 0573 | PUSH2 | 0x1be3 |
| 0576 | JUMP | |
| 0577 | JUMPDEST | |
| 0578 | SWAP1 | |
| 0579 | PUSH2 | 0x0564 |
| 057c | JUMP | |
| 057d | JUMPDEST | |
| 057e | PUSH4 | 0xdc63d81f |
| 0583 | PUSH1 | 0xe0 |
| 0585 | SHL | |
| 0586 | PUSH0 | |
| 0587 | MSTORE | |
| 0588 | PUSH1 | 0x04 |
| 058a | PUSH0 | |
| 058b | REVERT | |
| 058c | JUMPDEST | |
| 058d | PUSH1 | 0x40 |
| 058f | MLOAD | |
| 0590 | PUSH1 | 0x20 |
| 0592 | DUP2 | |
| 0593 | ADD | |
| 0594 | SWAP1 | |
| 0595 | PUSH1 | 0x40 |
| 0597 | DUP3 | |
| 0598 | MSTORE | |
| 0599 | PUSH2 | 0x05be |
| 059c | DUP2 | |
| 059d | PUSH2 | 0x05aa |
| 05a0 | PUSH1 | 0x60 |
| 05a2 | DUP3 | |
| 05a3 | ADD | |
| 05a4 | DUP11 | |
| 05a5 | DUP14 | |
| 05a6 | PUSH2 | 0x198a |
| 05a9 | JUMP | |
| 05aa | JUMPDEST | |
| 05ab | DUP11 | |
| 05ac | PUSH1 | 0x40 |
| 05ae | DUP4 | |
| 05af | ADD | |
| 05b0 | MSTORE | |
| 05b1 | SUB | |
| 05b2 | PUSH1 | 0x1f |
| 05b4 | NOT | |
| 05b5 | DUP2 | |
| 05b6 | ADD | |
| 05b7 | DUP4 | |
| 05b8 | MSTORE | |
| 05b9 | DUP3 | |
| 05ba | PUSH2 | 0x16f8 |
| 05bd | JUMP | |
| 05be | JUMPDEST | |
| 05bf | MLOAD | |
| 05c0 | SWAP1 | |
| 05c1 | KECCAK256 | |
| 05c2 | DUP4 | |
| 05c3 | EXTCODESIZE | |
| 05c4 | ISZERO | |
| 05c5 | PUSH2 | 0x02b5 |
| 05c8 | JUMPI | |
| 05c9 | SWAP1 | |
| 05ca | DUP3 | |
| 05cb | PUSH1 | 0x01 |
| 05cd | PUSH1 | 0x01 |
| 05cf | PUSH1 | 0x40 |
| 05d1 | SHL | |
| 05d2 | SUB | |
| 05d3 | SWAP6 | |
| 05d4 | SWAP4 | |
| 05d5 | SWAP3 | |
| 05d6 | PUSH1 | 0x40 |
| 05d8 | MLOAD | |
| 05d9 | SWAP7 | |
| 05da | DUP8 | |
| 05db | SWAP6 | |
| 05dc | PUSH4 | 0x22f3f447 |
| 05e1 | PUSH1 | 0xe1 |
| 05e3 | SHL | |
| 05e4 | DUP8 | |
| 05e5 | MSTORE | |
| 05e6 | PUSH1 | 0x84 |
| 05e8 | DUP8 | |
| 05e9 | ADD | |
| 05ea | SWAP3 | |
| 05eb | PUSH32 | 0x405bbda3343b6e69c32fb7eafff8f0a1e55a5ee2ec35458b3abc776b26681952 |
| 060c | PUSH1 | 0x04 |
| 060e | DUP10 | |
| 060f | ADD | |
| 0610 | MSTORE | |
| 0611 | PUSH1 | 0x24 |
| 0613 | DUP9 | |
| 0614 | ADD | |
| 0615 | MSTORE | |
| 0616 | AND | |
| 0617 | PUSH1 | 0x44 |
| 0619 | DUP7 | |
| 061a | ADD | |
| 061b | MSTORE | |
| 061c | PUSH1 | 0x80 |
| 061e | PUSH1 | 0x64 |
| 0620 | DUP7 | |
| 0621 | ADD | |
| 0622 | MSTORE | |
| 0623 | MSTORE | |
| 0624 | PUSH1 | 0xa4 |
| 0626 | DUP4 | |
| 0627 | ADD | |
| 0628 | PUSH1 | 0xa0 |
| 062a | PUSH1 | 0x04 |
| 062c | DUP5 | |
| 062d | PUSH1 | 0x05 |
| 062f | SHL | |
| 0630 | DUP7 | |
| 0631 | ADD | |
| 0632 | ADD | |
| 0633 | ADD | |
| 0634 | SWAP3 | |
| 0635 | DUP3 | |
| 0636 | PUSH0 | |
| 0637 | SWAP1 | |
| 0638 | PUSH1 | 0x7e |
| 063a | NOT | |
| 063b | DUP2 | |
| 063c | CALLDATASIZE | |
| 063d | SUB | |
| 063e | ADD | |
| 063f | JUMPDEST | |
| 0640 | DUP4 | |
| 0641 | DUP4 | |
| 0642 | LT | |
| 0643 | PUSH2 | 0x0686 |
| 0646 | JUMPI | |
| 0647 | POP | |
| 0648 | POP | |
| 0649 | POP | |
| 064a | POP | |
| 064b | POP | |
| 064c | POP | |
| 064d | SWAP2 | |
| 064e | DUP2 | |
| 064f | PUSH0 | |
| 0650 | DUP2 | |
| 0651 | DUP6 | |
| 0652 | DUP3 | |
| 0653 | SWAP7 | |
| 0654 | POP | |
| 0655 | SUB | |
| 0656 | SWAP3 | |
| 0657 | GAS | |
| 0658 | CALL | |
| 0659 | DUP1 | |
| 065a | ISZERO | |
| 065b | PUSH2 | 0x067b |
| 065e | JUMPI | |
| 065f | PUSH2 | 0x066b |
| 0662 | JUMPI | |
| 0663 | JUMPDEST | |
| 0664 | DUP1 | |
| 0665 | DUP1 | |
| 0666 | DUP1 | |
| 0667 | PUSH2 | 0x043a |
| 066a | JUMP | |
| 066b | JUMPDEST | |
| 066c | PUSH0 | |
| 066d | PUSH2 | 0x0675 |
| 0670 | SWAP2 | |
| 0671 | PUSH2 | 0x16f8 |
| 0674 | JUMP | |
| 0675 | JUMPDEST | |
| 0676 | DUP4 | |
| 0677 | PUSH2 | 0x0663 |
| 067a | JUMP | |
| 067b | JUMPDEST | |
| 067c | PUSH1 | 0x40 |
| 067e | MLOAD | |
| 067f | RETURNDATASIZE | |
| 0680 | PUSH0 | |
| 0681 | DUP3 | |
| 0682 | RETURNDATACOPY | |
| 0683 | RETURNDATASIZE | |
| 0684 | SWAP1 | |
| 0685 | REVERT | |
| 0686 | JUMPDEST | |
| 0687 | PUSH1 | 0xa3 |
| 0689 | NOT | |
| 068a | DUP11 | |
| 068b | DUP9 | |
| 068c | SUB | |
| 068d | ADD | |
| 068e | DUP6 | |
| 068f | MSTORE | |
| 0690 | SWAP5 | |
| 0691 | SWAP7 | |
| 0692 | POP | |
| 0693 | SWAP3 | |
| 0694 | SWAP5 | |
| 0695 | SWAP2 | |
| 0696 | SWAP4 | |
| 0697 | SWAP1 | |
| 0698 | SWAP3 | |
| 0699 | SWAP2 | |
| 069a | DUP7 | |
| 069b | CALLDATALOAD | |
| 069c | DUP3 | |
| 069d | DUP2 | |
| 069e | SLT | |
| 069f | ISZERO | |
| 06a0 | PUSH2 | 0x02b5 |
| 06a3 | JUMPI | |
| 06a4 | DUP4 | |
| 06a5 | ADD | |
| 06a6 | DUP1 | |
| 06a7 | CALLDATALOAD | |
| 06a8 | PUSH1 | 0x01 |
| 06aa | PUSH1 | 0x01 |
| 06ac | PUSH1 | 0xa0 |
| 06ae | SHL | |
| 06af | SUB | |
| 06b0 | DUP2 | |
| 06b1 | AND | |
| 06b2 | SWAP1 | |
| 06b3 | DUP2 | |
| 06b4 | SWAP1 | |
| 06b5 | SUB | |
| 06b6 | PUSH2 | 0x02b5 |
| 06b9 | JUMPI | |
| 06ba | DUP3 | |
| 06bb | MSTORE | |
| 06bc | PUSH1 | 0x20 |
| 06be | DUP2 | |
| 06bf | ADD | |
| 06c0 | CALLDATALOAD | |
| 06c1 | SWAP2 | |
| 06c2 | PUSH1 | 0xff |
| 06c4 | DUP4 | |
| 06c5 | AND | |
| 06c6 | DUP1 | |
| 06c7 | SWAP4 | |
| 06c8 | SUB | |
| 06c9 | PUSH2 | 0x02b5 |
| 06cc | JUMPI | |
| 06cd | PUSH2 | 0x0718 |
| 06d0 | PUSH1 | 0x20 |
| 06d2 | SWAP3 | |
| 06d3 | DUP3 | |
| 06d4 | PUSH1 | 0x01 |
| 06d6 | SWAP6 | |
| 06d7 | DUP6 | |
| 06d8 | DUP1 | |
| 06d9 | SWAP6 | |
| 06da | ADD | |
| 06db | MSTORE | |
| 06dc | PUSH2 | 0x070a |
| 06df | PUSH2 | 0x06ff |
| 06e2 | PUSH2 | 0x06ee |
| 06e5 | PUSH1 | 0x40 |
| 06e7 | DUP6 | |
| 06e8 | ADD | |
| 06e9 | DUP6 | |
| 06ea | PUSH2 | 0x1a13 |
| 06ed | JUMP | |
| 06ee | JUMPDEST | |
| 06ef | PUSH1 | 0x80 |
| 06f1 | PUSH1 | 0x40 |
| 06f3 | DUP7 | |
| 06f4 | ADD | |
| 06f5 | MSTORE | |
| 06f6 | PUSH1 | 0x80 |
| 06f8 | DUP6 | |
| 06f9 | ADD | |
| 06fa | SWAP2 | |
| 06fb | PUSH2 | 0x1a44 |
| 06fe | JUMP | |
| 06ff | JUMPDEST | |
| 0700 | SWAP3 | |
| 0701 | PUSH1 | 0x60 |
| 0703 | DUP2 | |
| 0704 | ADD | |
| 0705 | SWAP1 | |
| 0706 | PUSH2 | 0x1a13 |
| 0709 | JUMP | |
| 070a | JUMPDEST | |
| 070b | SWAP2 | |
| 070c | PUSH1 | 0x60 |
| 070e | DUP2 | |
| 070f | DUP6 | |
| 0710 | SUB | |
| 0711 | SWAP2 | |
| 0712 | ADD | |
| 0713 | MSTORE | |
| 0714 | PUSH2 | 0x1a44 |
| 0717 | JUMP | |
| 0718 | JUMPDEST | |
| 0719 | SWAP9 | |
| 071a | ADD | |
| 071b | SWAP7 | |
| 071c | ADD | |
| 071d | SWAP4 | |
| 071e | ADD | |
| 071f | SWAP1 | |
| 0720 | SWAP2 | |
| 0721 | DUP9 | |
| 0722 | SWAP7 | |
| 0723 | SWAP6 | |
| 0724 | SWAP5 | |
| 0725 | SWAP3 | |
| 0726 | PUSH2 | 0x063f |
| 0729 | JUMP | |
| 072a | JUMPDEST | |
| 072b | POP | |
| 072c | PUSH1 | 0x40 |
| 072e | MLOAD | |
| 072f | PUSH4 | 0xf5778b03 |
| 0734 | PUSH1 | 0xe0 |
| 0736 | SHL | |
| 0737 | DUP2 | |
| 0738 | MSTORE | |
| 0739 | PUSH1 | 0x20 |
| 073b | DUP2 | |
| 073c | PUSH1 | 0x04 |
| 073e | DUP2 | |
| 073f | DUP8 | |
| 0740 | GAS | |
| 0741 | STATICCALL | |
| 0742 | SWAP1 | |
| 0743 | DUP2 | |
| 0744 | ISZERO | |
| 0745 | PUSH2 | 0x067b |
| 0748 | JUMPI | |
| 0749 | PUSH0 | |
| 074a | SWAP2 | |
| 074b | PUSH2 | 0x0761 |
| 074e | JUMPI | |
| 074f | JUMPDEST | |
| 0750 | POP | |
| 0751 | PUSH1 | 0x01 |
| 0753 | PUSH1 | 0x01 |
| 0755 | PUSH1 | 0xa0 |
| 0757 | SHL | |
| 0758 | SUB | |
| 0759 | AND | |
| 075a | CALLER | |
| 075b | EQ | |
| 075c | ISZERO | |
| 075d | PUSH2 | 0x0435 |
| 0760 | JUMP | |
| 0761 | JUMPDEST | |
| 0762 | PUSH2 | 0x0783 |
| 0765 | SWAP2 | |
| 0766 | POP | |
| 0767 | PUSH1 | 0x20 |
| 0769 | RETURNDATASIZE | |
| 076a | PUSH1 | 0x20 |
| 076c | GT | |
| 076d | PUSH2 | 0x0789 |
| 0770 | JUMPI | |
| 0771 | JUMPDEST | |
| 0772 | PUSH2 | 0x077b |
| 0775 | DUP2 | |
| 0776 | DUP4 | |
| 0777 | PUSH2 | 0x16f8 |
| 077a | JUMP | |
| 077b | JUMPDEST | |
| 077c | DUP2 | |
| 077d | ADD | |
| 077e | SWAP1 | |
| 077f | PUSH2 | 0x19f4 |
| 0782 | JUMP | |
| 0783 | JUMPDEST | |
| 0784 | DUP9 | |
| 0785 | PUSH2 | 0x074f |
| 0788 | JUMP | |
| 0789 | JUMPDEST | |
| 078a | POP | |
| 078b | RETURNDATASIZE | |
| 078c | PUSH2 | 0x0771 |
| 078f | JUMP | |
| 0790 | JUMPDEST | |
| 0791 | PUSH2 | 0x07b2 |
| 0794 | SWAP2 | |
| 0795 | POP | |
| 0796 | PUSH1 | 0x20 |
| 0798 | RETURNDATASIZE | |
| 0799 | PUSH1 | 0x20 |
| 079b | GT | |
| 079c | PUSH2 | 0x07b8 |
| 079f | JUMPI | |
| 07a0 | JUMPDEST | |
| 07a1 | PUSH2 | 0x07aa |
| 07a4 | DUP2 | |
| 07a5 | DUP4 | |
| 07a6 | PUSH2 | 0x16f8 |
| 07a9 | JUMP | |
| 07aa | JUMPDEST | |
| 07ab | DUP2 | |
| 07ac | ADD | |
| 07ad | SWAP1 | |
| 07ae | PUSH2 | 0x19dc |
| 07b1 | JUMP | |
| 07b2 | JUMPDEST | |
| 07b3 | DUP9 | |
| 07b4 | PUSH2 | 0x042d |
| 07b7 | JUMP | |
| 07b8 | JUMPDEST | |
| 07b9 | POP | |
| 07ba | RETURNDATASIZE | |
| 07bb | PUSH2 | 0x07a0 |
| 07be | JUMP | |
| 07bf | JUMPDEST | |
| 07c0 | CALLVALUE | |
| 07c1 | PUSH2 | 0x02b5 |
| 07c4 | JUMPI | |
| 07c5 | PUSH0 | |
| 07c6 | CALLDATASIZE | |
| 07c7 | PUSH1 | 0x03 |
| 07c9 | NOT | |
| 07ca | ADD | |
| 07cb | SLT | |
| 07cc | PUSH2 | 0x02b5 |
| 07cf | JUMPI | |
| 07d0 | PUSH1 | 0x20 |
| 07d2 | PUSH0 | |
| 07d3 | SLOAD | |
| 07d4 | PUSH1 | 0x40 |
| 07d6 | MLOAD | |
| 07d7 | SWAP1 | |
| 07d8 | DUP2 | |
| 07d9 | MSTORE | |
| 07da | RETURN | |
| 07db | JUMPDEST | |
| 07dc | CALLVALUE | |
| 07dd | PUSH2 | 0x02b5 |
| 07e0 | JUMPI | |
| 07e1 | PUSH1 | 0x20 |
| 07e3 | CALLDATASIZE | |
| 07e4 | PUSH1 | 0x03 |
| 07e6 | NOT | |
| 07e7 | ADD | |
| 07e8 | SLT | |
| 07e9 | PUSH2 | 0x02b5 |
| 07ec | JUMPI | |
| 07ed | PUSH1 | 0x20 |
| 07ef | PUSH2 | 0x07f9 |
| 07f2 | PUSH1 | 0x04 |
| 07f4 | CALLDATALOAD | |
| 07f5 | PUSH2 | 0x1b22 |
| 07f8 | JUMP | |
| 07f9 | JUMPDEST | |
| 07fa | PUSH1 | 0x40 |
| 07fc | MLOAD | |
| 07fd | SWAP1 | |
| 07fe | DUP2 | |
| 07ff | MSTORE | |
| 0800 | RETURN | |
| 0801 | JUMPDEST | |
| 0802 | CALLVALUE | |
| 0803 | PUSH2 | 0x02b5 |
| 0806 | JUMPI | |
| 0807 | PUSH0 | |
| 0808 | CALLDATASIZE | |
| 0809 | PUSH1 | 0x03 |
| 080b | NOT | |
| 080c | ADD | |
| 080d | SLT | |
| 080e | PUSH2 | 0x02b5 |
| 0811 | JUMPI | |
| 0812 | PUSH1 | 0x40 |
| 0814 | MLOAD | |
| 0815 | PUSH32 | 0x000000000000000000000000f2b3161ed308717da082ee706cfdd27048e0d62d |
| 0836 | PUSH1 | 0x01 |
| 0838 | PUSH1 | 0x01 |
| 083a | PUSH1 | 0xa0 |
| 083c | SHL | |
| 083d | SUB | |
| 083e | AND | |
| 083f | DUP2 | |
| 0840 | MSTORE | |
| 0841 | PUSH1 | 0x20 |
| 0843 | SWAP1 | |
| 0844 | RETURN | |
| 0845 | JUMPDEST | |
| 0846 | CALLVALUE | |
| 0847 | PUSH2 | 0x02b5 |
| 084a | JUMPI | |
| 084b | PUSH0 | |
| 084c | CALLDATASIZE | |
| 084d | PUSH1 | 0x03 |
| 084f | NOT | |
| 0850 | ADD | |
| 0851 | SLT | |
| 0852 | PUSH2 | 0x02b5 |
| 0855 | JUMPI | |
| 0856 | PUSH1 | 0x20 |
| 0858 | PUSH1 | 0x40 |
| 085a | MLOAD | |
| 085b | PUSH32 | 0x27c91cbb7cc32319dd47788e8b096cc02ee8ccca641645266d7046769a12fbc3 |
| 087c | DUP2 | |
| 087d | MSTORE | |
| 087e | RETURN | |
| 087f | JUMPDEST | |
| 0880 | CALLVALUE | |
| 0881 | PUSH2 | 0x02b5 |
| 0884 | JUMPI | |
| 0885 | PUSH0 | |
| 0886 | CALLDATASIZE | |
| 0887 | PUSH1 | 0x03 |
| 0889 | NOT | |
| 088a | ADD | |
| 088b | SLT | |
| 088c | PUSH2 | 0x02b5 |
| 088f | JUMPI | |
| 0890 | PUSH1 | 0x20 |
| 0892 | PUSH1 | 0x01 |
| 0894 | PUSH1 | 0x01 |
| 0896 | PUSH1 | 0x40 |
| 0898 | SHL | |
| 0899 | SUB | |
| 089a | PUSH1 | 0x02 |
| 089c | SLOAD | |
| 089d | AND | |
| 089e | PUSH1 | 0x40 |
| 08a0 | MLOAD | |
| 08a1 | SWAP1 | |
| 08a2 | DUP2 | |
| 08a3 | MSTORE | |
| 08a4 | RETURN | |
| 08a5 | JUMPDEST | |
| 08a6 | CALLVALUE | |
| 08a7 | PUSH2 | 0x02b5 |
| 08aa | JUMPI | |
| 08ab | PUSH1 | 0x80 |
| 08ad | CALLDATASIZE | |
| 08ae | PUSH1 | 0x03 |
| 08b0 | NOT | |
| 08b1 | ADD | |
| 08b2 | SLT | |
| 08b3 | PUSH2 | 0x02b5 |
| 08b6 | JUMPI | |
| 08b7 | PUSH1 | 0x24 |
| 08b9 | CALLDATALOAD | |
| 08ba | PUSH1 | 0x04 |
| 08bc | CALLDATALOAD | |
| 08bd | PUSH2 | 0x08c4 |
| 08c0 | PUSH2 | 0x1648 |
| 08c3 | JUMP | |
| 08c4 | JUMPDEST | |
| 08c5 | PUSH1 | 0x64 |
| 08c7 | CALLDATALOAD | |
| 08c8 | PUSH1 | 0x01 |
| 08ca | PUSH1 | 0x01 |
| 08cc | PUSH1 | 0x40 |
| 08ce | SHL | |
| 08cf | SUB | |
| 08d0 | DUP2 | |
| 08d1 | GT | |
| 08d2 | PUSH2 | 0x02b5 |
| 08d5 | JUMPI | |
| 08d6 | PUSH2 | 0x08e3 |
| 08d9 | SWAP1 | |
| 08da | CALLDATASIZE | |
| 08db | SWAP1 | |
| 08dc | PUSH1 | 0x04 |
| 08de | ADD | |
| 08df | PUSH2 | 0x1618 |
| 08e2 | JUMP | |
| 08e3 | JUMPDEST | |
| 08e4 | PUSH1 | 0x40 |
| 08e6 | MLOAD | |
| 08e7 | PUSH4 | 0x28305db1 |
| 08ec | PUSH1 | 0xe2 |
| 08ee | SHL | |
| 08ef | DUP2 | |
| 08f0 | MSTORE | |
| 08f1 | PUSH32 | 0x0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d540 |
| 0912 | PUSH1 | 0x01 |
| 0914 | PUSH1 | 0x01 |
| 0916 | PUSH1 | 0xa0 |
| 0918 | SHL | |
| 0919 | SUB | |
| 091a | AND | |
| 091b | SWAP4 | |
| 091c | SWAP3 | |
| 091d | SWAP1 | |
| 091e | PUSH1 | 0x20 |
| 0920 | DUP2 | |
| 0921 | PUSH1 | 0x04 |
| 0923 | DUP2 | |
| 0924 | DUP9 | |
| 0925 | GAS | |
| 0926 | STATICCALL | |
| 0927 | SWAP1 | |
| 0928 | DUP2 | |
| 0929 | ISZERO | |
| 092a | PUSH2 | 0x067b |
| 092d | JUMPI | |
| 092e | PUSH0 | |
| 092f | SWAP2 | |
| 0930 | PUSH2 | 0x0ba9 |
| 0933 | JUMPI | |
| 0934 | JUMPDEST | |
| 0935 | POP | |
| 0936 | DUP1 | |
| 0937 | ISZERO | |
| 0938 | PUSH2 | 0x0b53 |
| 093b | JUMPI | |
| 093c | JUMPDEST | |
| 093d | PUSH2 | 0x0a01 |
| 0940 | JUMPI | |
| 0941 | JUMPDEST | |
| 0942 | POP | |
| 0943 | POP | |
| 0944 | POP | |
| 0945 | DUP3 | |
| 0946 | PUSH2 | 0x0983 |
| 0949 | JUMPI | |
| 094a | JUMPDEST | |
| 094b | PUSH32 | 0xbfc08a458e488f0e56f7ff4bfe317bed1ba5d3f7ef5a2bda241528695f6fcdf3 |
| 096c | PUSH1 | 0x40 |
| 096e | DUP4 | |
| 096f | DUP6 | |
| 0970 | DUP2 | |
| 0971 | PUSH0 | |
| 0972 | SSTORE | |
| 0973 | DUP1 | |
| 0974 | PUSH1 | 0x01 |
| 0976 | SSTORE | |
| 0977 | DUP3 | |
| 0978 | MLOAD | |
| 0979 | SWAP2 | |
| 097a | DUP3 | |
| 097b | MSTORE | |
| 097c | PUSH1 | 0x20 |
| 097e | DUP3 | |
| 097f | ADD | |
| 0980 | MSTORE | |
| 0981 | LOG1 | |
| 0982 | STOP | |
| 0983 | JUMPDEST | |
| 0984 | PUSH1 | 0x20 |
| 0986 | PUSH1 | 0x24 |
| 0988 | SWAP2 | |
| 0989 | PUSH1 | 0x40 |
| 098b | MLOAD | |
| 098c | SWAP3 | |
| 098d | DUP4 | |
| 098e | DUP1 | |
| 098f | SWAP3 | |
| 0990 | PUSH4 | 0x342f6163 |
| 0995 | PUSH1 | 0xe0 |
| 0997 | SHL | |
| 0998 | DUP3 | |
| 0999 | MSTORE | |
| 099a | DUP7 | |
| 099b | PUSH1 | 0x04 |
| 099d | DUP4 | |
| 099e | ADD | |
| 099f | MSTORE | |
| 09a0 | GAS | |
| 09a1 | STATICCALL | |
| 09a2 | SWAP1 | |
| 09a3 | DUP2 | |
| 09a4 | ISZERO | |
| 09a5 | PUSH2 | 0x067b |
| 09a8 | JUMPI | |
| 09a9 | PUSH0 | |
| 09aa | SWAP2 | |
| 09ab | PUSH2 | 0x09cf |
| 09ae | JUMPI | |
| 09af | JUMPDEST | |
| 09b0 | POP | |
| 09b1 | DUP3 | |
| 09b2 | DUP2 | |
| 09b3 | LT | |
| 09b4 | ISZERO | |
| 09b5 | PUSH2 | 0x094a |
| 09b8 | JUMPI | |
| 09b9 | SWAP1 | |
| 09ba | POP | |
| 09bb | PUSH4 | 0x3770da33 |
| 09c0 | PUSH1 | 0xe1 |
| 09c2 | SHL | |
| 09c3 | PUSH0 | |
| 09c4 | MSTORE | |
| 09c5 | PUSH1 | 0x04 |
| 09c7 | MSTORE | |
| 09c8 | PUSH1 | 0x24 |
| 09ca | MSTORE | |
| 09cb | PUSH1 | 0x44 |
| 09cd | PUSH0 | |
| 09ce | REVERT | |
| 09cf | JUMPDEST | |
| 09d0 | SWAP1 | |
| 09d1 | POP | |
| 09d2 | PUSH1 | 0x20 |
| 09d4 | DUP2 | |
| 09d5 | RETURNDATASIZE | |
| 09d6 | PUSH1 | 0x20 |
| 09d8 | GT | |
| 09d9 | PUSH2 | 0x09f9 |
| 09dc | JUMPI | |
| 09dd | JUMPDEST | |
| 09de | DUP2 | |
| 09df | PUSH2 | 0x09ea |
| 09e2 | PUSH1 | 0x20 |
| 09e4 | SWAP4 | |
| 09e5 | DUP4 | |
| 09e6 | PUSH2 | 0x16f8 |
| 09e9 | JUMP | |
| 09ea | JUMPDEST | |
| 09eb | DUP2 | |
| 09ec | ADD | |
| 09ed | SUB | |
| 09ee | SLT | |
| 09ef | PUSH2 | 0x02b5 |
| 09f2 | JUMPI | |
| 09f3 | MLOAD | |
| 09f4 | DUP4 | |
| 09f5 | PUSH2 | 0x09af |
| 09f8 | JUMP | |
| 09f9 | JUMPDEST | |
| 09fa | RETURNDATASIZE | |
| 09fb | SWAP2 | |
| 09fc | POP | |
| 09fd | PUSH2 | 0x09dd |
| 0a00 | JUMP | |
| 0a01 | JUMPDEST | |
| 0a02 | PUSH1 | 0x40 |
| 0a04 | MLOAD | |
| 0a05 | PUSH1 | 0x20 |
| 0a07 | DUP2 | |
| 0a08 | ADD | |
| 0a09 | SWAP1 | |
| 0a0a | DUP7 | |
| 0a0b | DUP3 | |
| 0a0c | MSTORE | |
| 0a0d | DUP8 | |
| 0a0e | PUSH1 | 0x40 |
| 0a10 | DUP3 | |
| 0a11 | ADD | |
| 0a12 | MSTORE | |
| 0a13 | PUSH1 | 0x40 |
| 0a15 | DUP2 | |
| 0a16 | MSTORE | |
| 0a17 | PUSH2 | 0x0a21 |
| 0a1a | PUSH1 | 0x60 |
| 0a1c | DUP3 | |
| 0a1d | PUSH2 | 0x16f8 |
| 0a20 | JUMP | |
| 0a21 | JUMPDEST | |
| 0a22 | MLOAD | |
| 0a23 | SWAP1 | |
| 0a24 | KECCAK256 | |
| 0a25 | DUP5 | |
| 0a26 | EXTCODESIZE | |
| 0a27 | ISZERO | |
| 0a28 | PUSH2 | 0x02b5 |
| 0a2b | JUMPI | |
| 0a2c | SWAP1 | |
| 0a2d | DUP3 | |
| 0a2e | PUSH1 | 0x01 |
| 0a30 | PUSH1 | 0x01 |
| 0a32 | PUSH1 | 0x40 |
| 0a34 | SHL | |
| 0a35 | SUB | |
| 0a36 | SWAP5 | |
| 0a37 | SWAP3 | |
| 0a38 | PUSH1 | 0x40 |
| 0a3a | MLOAD | |
| 0a3b | SWAP6 | |
| 0a3c | DUP7 | |
| 0a3d | SWAP5 | |
| 0a3e | PUSH4 | 0x22f3f447 |
| 0a43 | PUSH1 | 0xe1 |
| 0a45 | SHL | |
| 0a46 | DUP7 | |
| 0a47 | MSTORE | |
| 0a48 | PUSH1 | 0x84 |
| 0a4a | DUP7 | |
| 0a4b | ADD | |
| 0a4c | SWAP3 | |
| 0a4d | PUSH32 | 0x27c91cbb7cc32319dd47788e8b096cc02ee8ccca641645266d7046769a12fbc3 |
| 0a6e | PUSH1 | 0x04 |
| 0a70 | DUP9 | |
| 0a71 | ADD | |
| 0a72 | MSTORE | |
| 0a73 | PUSH1 | 0x24 |
| 0a75 | DUP8 | |
| 0a76 | ADD | |
| 0a77 | MSTORE | |
| 0a78 | AND | |
| 0a79 | PUSH1 | 0x44 |
| 0a7b | DUP6 | |
| 0a7c | ADD | |
| 0a7d | MSTORE | |
| 0a7e | PUSH1 | 0x80 |
| 0a80 | PUSH1 | 0x64 |
| 0a82 | DUP6 | |
| 0a83 | ADD | |
| 0a84 | MSTORE | |
| 0a85 | MSTORE | |
| 0a86 | PUSH1 | 0xa4 |
| 0a88 | DUP3 | |
| 0a89 | ADD | |
| 0a8a | PUSH1 | 0xa0 |
| 0a8c | PUSH1 | 0x04 |
| 0a8e | DUP6 | |
| 0a8f | PUSH1 | 0x05 |
| 0a91 | SHL | |
| 0a92 | DUP6 | |
| 0a93 | ADD | |
| 0a94 | ADD | |
| 0a95 | ADD | |
| 0a96 | SWAP4 | |
| 0a97 | DUP3 | |
| 0a98 | PUSH0 | |
| 0a99 | SWAP1 | |
| 0a9a | PUSH1 | 0x7e |
| 0a9c | NOT | |
| 0a9d | DUP2 | |
| 0a9e | CALLDATASIZE | |
| 0a9f | SUB | |
| 0aa0 | ADD | |
| 0aa1 | JUMPDEST | |
| 0aa2 | DUP4 | |
| 0aa3 | DUP4 | |
| 0aa4 | LT | |
| 0aa5 | PUSH2 | 0x0adb |
| 0aa8 | JUMPI | |
| 0aa9 | POP | |
| 0aaa | POP | |
| 0aab | POP | |
| 0aac | POP | |
| 0aad | POP | |
| 0aae | POP | |
| 0aaf | DUP1 | |
| 0ab0 | DUP3 | |
| 0ab1 | PUSH0 | |
| 0ab2 | SWAP4 | |
| 0ab3 | POP | |
| 0ab4 | SUB | |
| 0ab5 | DUP2 | |
| 0ab6 | DUP4 | |
| 0ab7 | DUP7 | |
| 0ab8 | GAS | |
| 0ab9 | CALL | |
| 0aba | DUP1 | |
| 0abb | ISZERO | |
| 0abc | PUSH2 | 0x067b |
| 0abf | JUMPI | |
| 0ac0 | PUSH2 | 0x0acb |
| 0ac3 | JUMPI | |
| 0ac4 | JUMPDEST | |
| 0ac5 | DUP1 | |
| 0ac6 | DUP1 | |
| 0ac7 | PUSH2 | 0x0941 |
| 0aca | JUMP | |
| 0acb | JUMPDEST | |
| 0acc | PUSH0 | |
| 0acd | PUSH2 | 0x0ad5 |
| 0ad0 | SWAP2 | |
| 0ad1 | PUSH2 | 0x16f8 |
| 0ad4 | JUMP | |
| 0ad5 | JUMPDEST | |
| 0ad6 | DUP4 | |
| 0ad7 | PUSH2 | 0x0ac4 |
| 0ada | JUMP | |
| 0adb | JUMPDEST | |
| 0adc | PUSH1 | 0xa3 |
| 0ade | NOT | |
| 0adf | DUP10 | |
| 0ae0 | DUP10 | |
| 0ae1 | SUB | |
| 0ae2 | ADD | |
| 0ae3 | DUP6 | |
| 0ae4 | MSTORE | |
| 0ae5 | SWAP5 | |
| 0ae6 | SWAP7 | |
| 0ae7 | SWAP4 | |
| 0ae8 | SWAP6 | |
| 0ae9 | POP | |
| 0aea | SWAP2 | |
| 0aeb | SWAP4 | |
| 0aec | SWAP1 | |
| 0aed | SWAP3 | |
| 0aee | DUP7 | |
| 0aef | CALLDATALOAD | |
| 0af0 | DUP3 | |
| 0af1 | DUP2 | |
| 0af2 | SLT | |
| 0af3 | ISZERO | |
| 0af4 | PUSH2 | 0x02b5 |
| 0af7 | JUMPI | |
| 0af8 | DUP4 | |
| 0af9 | ADD | |
| 0afa | DUP1 | |
| 0afb | CALLDATALOAD | |
| 0afc | PUSH1 | 0x01 |
| 0afe | PUSH1 | 0x01 |
| 0b00 | PUSH1 | 0xa0 |
| 0b02 | SHL | |
| 0b03 | SUB | |
| 0b04 | DUP2 | |
| 0b05 | AND | |
| 0b06 | SWAP1 | |
| 0b07 | DUP2 | |
| 0b08 | SWAP1 | |
| 0b09 | SUB | |
| 0b0a | PUSH2 | 0x02b5 |
| 0b0d | JUMPI | |
| 0b0e | DUP3 | |
| 0b0f | MSTORE | |
| 0b10 | PUSH1 | 0x20 |
| 0b12 | DUP2 | |
| 0b13 | ADD | |
| 0b14 | CALLDATALOAD | |
| 0b15 | SWAP2 | |
| 0b16 | PUSH1 | 0xff |
| 0b18 | DUP4 | |
| 0b19 | AND | |
| 0b1a | DUP1 | |
| 0b1b | SWAP4 | |
| 0b1c | SUB | |
| 0b1d | PUSH2 | 0x02b5 |
| 0b20 | JUMPI | |
| 0b21 | PUSH2 | 0x0b42 |
| 0b24 | PUSH1 | 0x20 |
| 0b26 | SWAP3 | |
| 0b27 | DUP3 | |
| 0b28 | PUSH1 | 0x01 |
| 0b2a | SWAP6 | |
| 0b2b | DUP6 | |
| 0b2c | DUP1 | |
| 0b2d | SWAP6 | |
| 0b2e | ADD | |
| 0b2f | MSTORE | |
| 0b30 | PUSH2 | 0x070a |
| 0b33 | PUSH2 | 0x06ff |
| 0b36 | PUSH2 | 0x06ee |
| 0b39 | PUSH1 | 0x40 |
| 0b3b | DUP6 | |
| 0b3c | ADD | |
| 0b3d | DUP6 | |
| 0b3e | PUSH2 | 0x1a13 |
| 0b41 | JUMP | |
| 0b42 | JUMPDEST | |
| 0b43 | SWAP9 | |
| 0b44 | ADD | |
| 0b45 | SWAP7 | |
| 0b46 | ADD | |
| 0b47 | SWAP4 | |
| 0b48 | ADD | |
| 0b49 | SWAP1 | |
| 0b4a | SWAP2 | |
| 0b4b | DUP8 | |
| 0b4c | SWAP6 | |
| 0b4d | SWAP5 | |
| 0b4e | SWAP3 | |
| 0b4f | PUSH2 | 0x0aa1 |
| 0b52 | JUMP | |
| 0b53 | JUMPDEST | |
| 0b54 | POP | |
| 0b55 | PUSH1 | 0x40 |
| 0b57 | MLOAD | |
| 0b58 | PUSH4 | 0xf5778b03 |
| 0b5d | PUSH1 | 0xe0 |
| 0b5f | SHL | |
| 0b60 | DUP2 | |
| 0b61 | MSTORE | |
| 0b62 | PUSH1 | 0x20 |
| 0b64 | DUP2 | |
| 0b65 | PUSH1 | 0x04 |
| 0b67 | DUP2 | |
| 0b68 | DUP9 | |
| 0b69 | GAS | |
| 0b6a | STATICCALL | |
| 0b6b | SWAP1 | |
| 0b6c | DUP2 | |
| 0b6d | ISZERO | |
| 0b6e | PUSH2 | 0x067b |
| 0b71 | JUMPI | |
| 0b72 | PUSH0 | |
| 0b73 | SWAP2 | |
| 0b74 | PUSH2 | 0x0b8a |
| 0b77 | JUMPI | |
| 0b78 | JUMPDEST | |
| 0b79 | POP | |
| 0b7a | PUSH1 | 0x01 |
| 0b7c | PUSH1 | 0x01 |
| 0b7e | PUSH1 | 0xa0 |
| 0b80 | SHL | |
| 0b81 | SUB | |
| 0b82 | AND | |
| 0b83 | CALLER | |
| 0b84 | EQ | |
| 0b85 | ISZERO | |
| 0b86 | PUSH2 | 0x093c |
| 0b89 | JUMP | |
| 0b8a | JUMPDEST | |
| 0b8b | PUSH2 | 0x0ba3 |
| 0b8e | SWAP2 | |
| 0b8f | POP | |
| 0b90 | PUSH1 | 0x20 |
| 0b92 | RETURNDATASIZE | |
| 0b93 | PUSH1 | 0x20 |
| 0b95 | GT | |
| 0b96 | PUSH2 | 0x0789 |
| 0b99 | JUMPI | |
| 0b9a | PUSH2 | 0x077b |
| 0b9d | DUP2 | |
| 0b9e | DUP4 | |
| 0b9f | PUSH2 | 0x16f8 |
| 0ba2 | JUMP | |
| 0ba3 | JUMPDEST | |
| 0ba4 | DUP8 | |
| 0ba5 | PUSH2 | 0x0b78 |
| 0ba8 | JUMP | |
| 0ba9 | JUMPDEST | |
| 0baa | PUSH2 | 0x0bc2 |
| 0bad | SWAP2 | |
| 0bae | POP | |
| 0baf | PUSH1 | 0x20 |
| 0bb1 | RETURNDATASIZE | |
| 0bb2 | PUSH1 | 0x20 |
| 0bb4 | GT | |
| 0bb5 | PUSH2 | 0x07b8 |
| 0bb8 | JUMPI | |
| 0bb9 | PUSH2 | 0x07aa |
| 0bbc | DUP2 | |
| 0bbd | DUP4 | |
| 0bbe | PUSH2 | 0x16f8 |
| 0bc1 | JUMP | |
| 0bc2 | JUMPDEST | |
| 0bc3 | DUP8 | |
| 0bc4 | PUSH2 | 0x0934 |
| 0bc7 | JUMP | |
| 0bc8 | JUMPDEST | |
| 0bc9 | CALLVALUE | |
| 0bca | PUSH2 | 0x02b5 |
| 0bcd | JUMPI | |
| 0bce | PUSH0 | |
| 0bcf | CALLDATASIZE | |
| 0bd0 | PUSH1 | 0x03 |
| 0bd2 | NOT | |
| 0bd3 | ADD | |
| 0bd4 | SLT | |
| 0bd5 | PUSH2 | 0x02b5 |
| 0bd8 | JUMPI | |
| 0bd9 | PUSH1 | 0x20 |
| 0bdb | PUSH1 | 0x06 |
| 0bdd | SLOAD | |
| 0bde | PUSH1 | 0x40 |
| 0be0 | MLOAD | |
| 0be1 | SWAP1 | |
| 0be2 | DUP2 | |
| 0be3 | MSTORE | |
| 0be4 | RETURN | |
| 0be5 | JUMPDEST | |
| 0be6 | CALLVALUE | |
| 0be7 | PUSH2 | 0x02b5 |
| 0bea | JUMPI | |
| 0beb | PUSH0 | |
| 0bec | CALLDATASIZE | |
| 0bed | PUSH1 | 0x03 |
| 0bef | NOT | |
| 0bf0 | ADD | |
| 0bf1 | SLT | |
| 0bf2 | PUSH2 | 0x02b5 |
| 0bf5 | JUMPI | |
| 0bf6 | PUSH1 | 0x40 |
| 0bf8 | PUSH1 | 0x07 |
| 0bfa | SLOAD | |
| 0bfb | PUSH1 | 0x06 |
| 0bfd | SLOAD | |
| 0bfe | DUP3 | |
| 0bff | MLOAD | |
| 0c00 | SWAP2 | |
| 0c01 | DUP3 | |
| 0c02 | MSTORE | |
| 0c03 | PUSH1 | 0x20 |
| 0c05 | DUP3 | |
| 0c06 | ADD | |
| 0c07 | MSTORE | |
| 0c08 | RETURN | |
| 0c09 | JUMPDEST | |
| 0c0a | CALLVALUE | |
| 0c0b | PUSH2 | 0x02b5 |
| 0c0e | JUMPI | |
| 0c0f | PUSH0 | |
| 0c10 | CALLDATASIZE | |
| 0c11 | PUSH1 | 0x03 |
| 0c13 | NOT | |
| 0c14 | ADD | |
| 0c15 | SLT | |
| 0c16 | PUSH2 | 0x02b5 |
| 0c19 | JUMPI | |
| 0c1a | PUSH1 | 0x40 |
| 0c1c | MLOAD | |
| 0c1d | PUSH32 | 0x0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d540 |
| 0c3e | PUSH1 | 0x01 |
| 0c40 | PUSH1 | 0x01 |
| 0c42 | PUSH1 | 0xa0 |
| 0c44 | SHL | |
| 0c45 | SUB | |
| 0c46 | AND | |
| 0c47 | DUP2 | |
| 0c48 | MSTORE | |
| 0c49 | PUSH1 | 0x20 |
| 0c4b | SWAP1 | |
| 0c4c | RETURN | |
| 0c4d | JUMPDEST | |
| 0c4e | CALLVALUE | |
| 0c4f | PUSH2 | 0x02b5 |
| 0c52 | JUMPI | |
| 0c53 | PUSH0 | |
| 0c54 | CALLDATASIZE | |
| 0c55 | PUSH1 | 0x03 |
| 0c57 | NOT | |
| 0c58 | ADD | |
| 0c59 | SLT | |
| 0c5a | PUSH2 | 0x02b5 |
| 0c5d | JUMPI | |
| 0c5e | PUSH1 | 0x20 |
| 0c60 | PUSH1 | 0x40 |
| 0c62 | MLOAD | |
| 0c63 | PUSH32 | 0x405bbda3343b6e69c32fb7eafff8f0a1e55a5ee2ec35458b3abc776b26681952 |
| 0c84 | DUP2 | |
| 0c85 | MSTORE | |
| 0c86 | RETURN | |
| 0c87 | JUMPDEST | |
| 0c88 | CALLVALUE | |
| 0c89 | PUSH2 | 0x02b5 |
| 0c8c | JUMPI | |
| 0c8d | PUSH1 | 0x20 |
| 0c8f | CALLDATASIZE | |
| 0c90 | PUSH1 | 0x03 |
| 0c92 | NOT | |
| 0c93 | ADD | |
| 0c94 | SLT | |
| 0c95 | PUSH2 | 0x02b5 |
| 0c98 | JUMPI | |
| 0c99 | PUSH1 | 0x40 |
| 0c9b | PUSH2 | 0x0ca5 |
| 0c9e | PUSH1 | 0x04 |
| 0ca0 | CALLDATALOAD | |
| 0ca1 | PUSH2 | 0x1af8 |
| 0ca4 | JUMP | |
| 0ca5 | JUMPDEST | |
| 0ca6 | DUP3 | |
| 0ca7 | MLOAD | |
| 0ca8 | SWAP2 | |
| 0ca9 | DUP3 | |
| 0caa | MSTORE | |
| 0cab | ISZERO | |
| 0cac | ISZERO | |
| 0cad | PUSH1 | 0x20 |
| 0caf | DUP3 | |
| 0cb0 | ADD | |
| 0cb1 | MSTORE | |
| 0cb2 | RETURN | |
| 0cb3 | JUMPDEST | |
| 0cb4 | CALLVALUE | |
| 0cb5 | PUSH2 | 0x02b5 |
| 0cb8 | JUMPI | |
| 0cb9 | PUSH1 | 0x20 |
| 0cbb | CALLDATASIZE | |
| 0cbc | PUSH1 | 0x03 |
| 0cbe | NOT | |
| 0cbf | ADD | |
| 0cc0 | SLT | |
| 0cc1 | PUSH2 | 0x02b5 |
| 0cc4 | JUMPI | |
| 0cc5 | PUSH2 | 0x0328 |
| 0cc8 | PUSH2 | 0x0cd2 |
| 0ccb | PUSH1 | 0x04 |
| 0ccd | CALLDATALOAD | |
| 0cce | PUSH2 | 0x1a64 |
| 0cd1 | JUMP | |
| 0cd2 | JUMPDEST | |
| 0cd3 | PUSH1 | 0x40 |
| 0cd5 | MLOAD | |
| 0cd6 | SWAP2 | |
| 0cd7 | DUP3 | |
| 0cd8 | SWAP2 | |
| 0cd9 | PUSH1 | 0x20 |
| 0cdb | DUP4 | |
| 0cdc | MSTORE | |
| 0cdd | PUSH1 | 0x20 |
| 0cdf | DUP4 | |
| 0ce0 | ADD | |
| 0ce1 | SWAP1 | |
| 0ce2 | PUSH2 | 0x15e5 |
| 0ce5 | JUMP | |
| 0ce6 | JUMPDEST | |
| 0ce7 | CALLVALUE | |
| 0ce8 | PUSH2 | 0x02b5 |
| 0ceb | JUMPI | |
| 0cec | PUSH2 | 0x0cf4 |
| 0cef | CALLDATASIZE | |
| 0cf0 | PUSH2 | 0x165e |
| 0cf3 | JUMP | |
| 0cf4 | JUMPDEST | |
| 0cf5 | PUSH1 | 0x40 |
| 0cf7 | MLOAD | |
| 0cf8 | PUSH4 | 0x28305db1 |
| 0cfd | PUSH1 | 0xe2 |
| 0cff | SHL | |
| 0d00 | DUP2 | |
| 0d01 | MSTORE | |
| 0d02 | SWAP4 | |
| 0d03 | SWAP5 | |
| 0d04 | SWAP4 | |
| 0d05 | PUSH32 | 0x0000000000000000000000003c0698e02a10fec9a5cd5939d0a0f2d484e8d540 |
| 0d26 | PUSH1 | 0x01 |
| 0d28 | PUSH1 | 0x01 |
| 0d2a | PUSH1 | 0xa0 |
| 0d2c | SHL | |
| 0d2d | SUB | |
| 0d2e | AND | |
| 0d2f | SWAP3 | |
| 0d30 | SWAP1 | |
| 0d31 | PUSH1 | 0x20 |
| 0d33 | DUP2 | |
| 0d34 | PUSH1 | 0x04 |
| 0d36 | DUP2 | |
| 0d37 | DUP8 | |
| 0d38 | GAS | |
| 0d39 | STATICCALL | |
| 0d3a | SWAP1 | |
| 0d3b | DUP2 | |
| 0d3c | ISZERO | |
| 0d3d | PUSH2 | 0x067b |
| 0d40 | JUMPI | |
| 0d41 | PUSH0 | |
| 0d42 | SWAP2 | |
| 0d43 | PUSH2 | 0x0f5d |
| 0d46 | JUMPI | |
| 0d47 | JUMPDEST | |
| 0d48 | POP | |
| 0d49 | DUP1 | |
| 0d4a | ISZERO | |
| 0d4b | PUSH2 | 0x0f07 |
| 0d4e | JUMPI | |
| 0d4f | JUMPDEST | |
| 0d50 | PUSH2 | 0x0db2 |
| 0d53 | JUMPI | |
| 0d54 | JUMPDEST | |
| 0d55 | POP | |
| 0d56 | POP | |
| 0d57 | POP | |
| 0d58 | POP | |
| 0d59 | PUSH1 | 0x06 |
| 0d5b | SLOAD | |
| 0d5c | PUSH2 | 0x057d |
| 0d5f | JUMPI | |
| 0d60 | DUP2 | |
| 0d61 | ISZERO | |
| 0d62 | PUSH2 | 0x02a6 |
| 0d65 | JUMPI | |
| 0d66 | PUSH0 | |
| 0d67 | JUMPDEST | |
| 0d68 | DUP3 | |
| 0d69 | DUP2 | |
| 0d6a | LT | |
| 0d6b | PUSH2 | 0x0d9b |
| 0d6e | JUMPI | |
| 0d6f | PUSH32 | 0x1c295873c1ce4ce2ac720f43d6909e66b931b42e9246b862278eba9624c0bf05 |
| 0d90 | PUSH1 | 0x20 |
| 0d92 | DUP5 | |
| 0d93 | PUSH1 | 0x40 |
| 0d95 | MLOAD | |
| 0d96 | SWAP1 | |
| 0d97 | DUP2 | |
| 0d98 | MSTORE | |
| 0d99 | LOG1 | |
| 0d9a | STOP | |
| 0d9b | JUMPDEST | |
| 0d9c | DUP1 | |
| 0d9d | PUSH2 | 0x0dac |
| 0da0 | PUSH2 | 0x028b |
| 0da3 | PUSH1 | 0x01 |
| 0da5 | SWAP4 | |
| 0da6 | DUP7 | |
| 0da7 | DUP7 | |
| 0da8 | PUSH2 | 0x19cc |
| 0dab | JUMP | |
| 0dac | JUMPDEST | |
| 0dad | ADD | |
| 0dae | PUSH2 | 0x0d67 |
| 0db1 | JUMP | |
| 0db2 | JUMPDEST | |
| 0db3 | PUSH1 | 0x40 |
| 0db5 | MLOAD | |
| 0db6 | PUSH1 | 0x20 |
| 0db8 | DUP2 | |
| 0db9 | ADD | |
| 0dba | SWAP1 | |
| 0dbb | PUSH1 | 0x20 |
| 0dbd | DUP3 | |
| 0dbe | MSTORE | |
| 0dbf | PUSH2 | 0x0dd0 |
| 0dc2 | DUP2 | |
| 0dc3 | PUSH2 | 0x0199 |
| 0dc6 | PUSH1 | 0x40 |
| 0dc8 | DUP3 | |
| 0dc9 | ADD | |
| 0dca | DUP12 | |
| 0dcb | DUP12 | |
| 0dcc | PUSH2 | 0x198a |
| 0dcf | JUMP | |
| 0dd0 | JUMPDEST | |
| 0dd1 | MLOAD | |
| 0dd2 | SWAP1 | |
| 0dd3 | KECCAK256 | |
| 0dd4 | DUP4 | |
| 0dd5 | EXTCODESIZE | |
| 0dd6 | ISZERO | |
| 0dd7 | PUSH2 | 0x02b5 |
| 0dda | JUMPI | |
| 0ddb | SWAP1 | |
| 0ddc | DUP3 | |
| 0ddd | PUSH1 | 0x01 |
| 0ddf | PUSH1 | 0x01 |
| 0de1 | PUSH1 | 0x40 |
| 0de3 | SHL | |
| 0de4 | SUB | |
| 0de5 | SWAP6 | |
| 0de6 | SWAP4 | |
| 0de7 | SWAP3 | |
| 0de8 | PUSH1 | 0x40 |
| 0dea | MLOAD | |
| 0deb | SWAP7 | |
| 0dec | DUP8 | |
| 0ded | SWAP6 | |
| 0dee | PUSH4 | 0x22f3f447 |
| 0df3 | PUSH1 | 0xe1 |
| 0df5 | SHL | |
| 0df6 | DUP8 | |
| 0df7 | MSTORE | |
| 0df8 | PUSH1 | 0x84 |
| 0dfa | DUP8 | |
| 0dfb | ADD | |
| 0dfc | SWAP3 | |
| 0dfd | PUSH32 | 0x9abdf9961fd14fd177480eccbad16b2d7f231898b2d763c8e8b50364d8b3b171 |
| 0e1e | PUSH1 | 0x04 |
| 0e20 | DUP10 | |
| 0e21 | ADD | |
| 0e22 | MSTORE | |
| 0e23 | PUSH1 | 0x24 |
| 0e25 | DUP9 | |
| 0e26 | ADD | |
| 0e27 | MSTORE | |
| 0e28 | AND | |
| 0e29 | PUSH1 | 0x44 |
| 0e2b | DUP7 | |
| 0e2c | ADD | |
| 0e2d | MSTORE | |
| 0e2e | PUSH1 | 0x80 |
| 0e30 | PUSH1 | 0x64 |
| 0e32 | DUP7 | |
| 0e33 | ADD | |
| 0e34 | MSTORE | |
| 0e35 | MSTORE | |
| 0e36 | PUSH1 | 0xa4 |
| 0e38 | DUP4 | |
| 0e39 | ADD | |
| 0e3a | PUSH1 | 0xa0 |
| 0e3c | PUSH1 | 0x04 |
| 0e3e | DUP5 | |
| 0e3f | PUSH1 | 0x05 |
| 0e41 | SHL | |
| 0e42 | DUP7 | |
| 0e43 | ADD | |
| 0e44 | ADD | |
| 0e45 | ADD | |
| 0e46 | SWAP3 | |
| 0e47 | DUP3 | |
| 0e48 | PUSH0 | |
| 0e49 | SWAP1 | |
| 0e4a | PUSH1 | 0x7e |
| 0e4c | NOT | |
| 0e4d | DUP2 | |
| 0e4e | CALLDATASIZE | |
| 0e4f | SUB | |
| 0e50 | ADD | |
| 0e51 | JUMPDEST | |
| 0e52 | DUP4 | |
| 0e53 | DUP4 | |
| 0e54 | LT | |
| 0e55 | PUSH2 | 0x0e8d |
| 0e58 | JUMPI | |
| 0e59 | POP | |
| 0e5a | POP | |
| 0e5b | POP | |
| 0e5c | POP | |
| 0e5d | POP | |
| 0e5e | POP | |
| 0e5f | SWAP2 | |
| 0e60 | DUP2 | |
| 0e61 | PUSH0 | |
| 0e62 | DUP2 | |
| 0e63 | DUP6 | |
| 0e64 | DUP3 | |
| 0e65 | SWAP7 | |
| 0e66 | POP | |
| 0e67 | SUB | |
| 0e68 | SWAP3 | |
| 0e69 | GAS | |
| 0e6a | CALL | |
| 0e6b | DUP1 | |
| 0e6c | ISZERO | |
| 0e6d | PUSH2 | 0x067b |
| 0e70 | JUMPI | |
| 0e71 | PUSH2 | 0x0e7d |
| 0e74 | JUMPI | |
| 0e75 | JUMPDEST | |
| 0e76 | DUP1 | |
| 0e77 | DUP1 | |
| 0e78 | DUP1 | |
| 0e79 | PUSH2 | 0x0d54 |
| 0e7c | JUMP | |
| 0e7d | JUMPDEST | |
| 0e7e | PUSH0 | |
| 0e7f | PUSH2 | 0x0e87 |
| 0e82 | SWAP2 | |
| 0e83 | PUSH2 | 0x16f8 |
| 0e86 | JUMP | |
| 0e87 | JUMPDEST | |
| 0e88 | DUP3 | |
| 0e89 | PUSH2 | 0x0e75 |
| 0e8c | JUMP | |
| 0e8d | JUMPDEST | |
| 0e8e | PUSH1 | 0xa3 |
| 0e90 | NOT | |
| 0e91 | DUP11 | |
| 0e92 | DUP9 | |
| 0e93 | SUB | |
| 0e94 | ADD | |
| 0e95 | DUP6 | |
| 0e96 | MSTORE | |
| 0e97 | SWAP5 | |
| 0e98 | SWAP7 | |
| 0e99 | POP | |
| 0e9a | SWAP3 | |
| 0e9b | SWAP5 | |
| 0e9c | SWAP2 | |
| 0e9d | SWAP4 | |
| 0e9e | SWAP1 | |
| 0e9f | SWAP3 | |
| 0ea0 | SWAP2 | |
| 0ea1 | DUP7 | |
| 0ea2 | CALLDATALOAD | |
| 0ea3 | DUP3 | |
| 0ea4 | DUP2 | |
| 0ea5 | SLT | |
| 0ea6 | ISZERO | |
| 0ea7 | PUSH2 | 0x02b5 |
| 0eaa | JUMPI | |
| 0eab | DUP4 | |
| 0eac | ADD | |
| 0ead | DUP1 | |
| 0eae | CALLDATALOAD | |
| 0eaf | PUSH1 | 0x01 |
| 0eb1 | PUSH1 | 0x01 |
| 0eb3 | PUSH1 | 0xa0 |
| 0eb5 | SHL | |
| 0eb6 | SUB | |
| 0eb7 | DUP2 | |
| 0eb8 | AND | |
| 0eb9 | SWAP1 | |
| 0eba | DUP2 | |
| 0ebb | SWAP1 | |
| 0ebc | SUB | |
| 0ebd | PUSH2 | 0x02b5 |
| 0ec0 | JUMPI | |
| 0ec1 | DUP3 | |
| 0ec2 | MSTORE | |
| 0ec3 | PUSH1 | 0x20 |
| 0ec5 | DUP2 | |
| 0ec6 | ADD | |
| 0ec7 | CALLDATALOAD | |
| 0ec8 | SWAP2 | |
| 0ec9 | PUSH1 | 0xff |
| 0ecb | DUP4 | |
| 0ecc | AND | |
| 0ecd | DUP1 | |
| 0ece | SWAP4 | |
| 0ecf | SUB | |
| 0ed0 | PUSH2 | 0x02b5 |
| 0ed3 | JUMPI | |
| 0ed4 | PUSH2 | 0x0ef5 |
| 0ed7 | PUSH1 | 0x20 |
| 0ed9 | SWAP3 | |
| 0eda | DUP3 | |
| 0edb | PUSH1 | 0x01 |
| 0edd | SWAP6 | |
| 0ede | DUP6 | |
| 0edf | DUP1 | |
| 0ee0 | SWAP6 | |
| 0ee1 | ADD | |
| 0ee2 | MSTORE | |
| 0ee3 | PUSH2 | 0x070a |
| 0ee6 | PUSH2 | 0x06ff |
| 0ee9 | PUSH2 | 0x06ee |
| 0eec | PUSH1 | 0x40 |
| 0eee | DUP6 | |
| 0eef | ADD | |
| 0ef0 | DUP6 | |
| 0ef1 | PUSH2 | 0x1a13 |
| 0ef4 | JUMP | |
| 0ef5 | JUMPDEST | |
| 0ef6 | SWAP9 | |
| 0ef7 | ADD | |
| 0ef8 | SWAP7 | |
| 0ef9 | ADD | |
| 0efa | SWAP4 | |
| 0efb | ADD | |
| 0efc | SWAP1 | |
| 0efd | SWAP2 | |
| 0efe | DUP9 | |
| 0eff | SWAP7 | |
| 0f00 | SWAP6 | |
| 0f01 | SWAP5 | |
| 0f02 | SWAP3 | |
| 0f03 | PUSH2 | 0x0e51 |
| 0f06 | JUMP | |
| 0f07 | JUMPDEST | |
| 0f08 | POP | |
| 0f09 | PUSH1 | 0x40 |
| 0f0b | MLOAD | |
| 0f0c | PUSH4 | 0xf5778b03 |
| 0f11 | PUSH1 | 0xe0 |
| 0f13 | SHL | |
| 0f14 | DUP2 | |
| 0f15 | MSTORE | |
| 0f16 | PUSH1 | 0x20 |
| 0f18 | DUP2 | |
| 0f19 | PUSH1 | 0x04 |
| 0f1b | DUP2 | |
| 0f1c | DUP8 | |
| 0f1d | GAS | |
| 0f1e | STATICCALL | |
| 0f1f | SWAP1 | |
| 0f20 | DUP2 | |
| 0f21 | ISZERO | |
| 0f22 | PUSH2 | 0x067b |
| 0f25 | JUMPI | |
| 0f26 | PUSH0 | |
| 0f27 | SWAP2 | |
| 0f28 | PUSH2 | 0x0f3e |
| 0f2b | JUMPI | |
| 0f2c | JUMPDEST | |
| 0f2d | POP | |
| 0f2e | PUSH1 | 0x01 |
| 0f30 | PUSH1 | 0x01 |
| 0f32 | PUSH1 | 0xa0 |
| 0f34 | SHL | |
| 0f35 | SUB | |
| 0f36 | AND | |
| 0f37 | CALLER | |
| 0f38 | EQ | |
| 0f39 | ISZERO | |
| 0f3a | PUSH2 | 0x0d4f |
| 0f3d | JUMP | |
| 0f3e | JUMPDEST | |
| 0f3f | PUSH2 | 0x0f57 |
| 0f42 | SWAP2 | |
| 0f43 | POP | |
| 0f44 | PUSH1 | 0x20 |
| 0f46 | RETURNDATASIZE | |
| 0f47 | PUSH1 | 0x20 |
| 0f49 | GT | |
| 0f4a | PUSH2 | 0x0789 |
| 0f4d | JUMPI | |
| 0f4e | PUSH2 | 0x077b |
| 0f51 | DUP2 | |
| 0f52 | DUP4 | |
| 0f53 | PUSH2 | 0x16f8 |
| 0f56 | JUMP | |
| 0f57 | JUMPDEST | |
| 0f58 | DUP8 | |
| 0f59 | PUSH2 | 0x0f2c |
| 0f5c | JUMP | |
| 0f5d | JUMPDEST | |
| 0f5e | PUSH2 | 0x0f76 |
| 0f61 | SWAP2 | |
| 0f62 | POP | |
| 0f63 | PUSH1 | 0x20 |
| 0f65 | RETURNDATASIZE | |
| 0f66 | PUSH1 | 0x20 |
| 0f68 | GT | |
| 0f69 | PUSH2 | 0x07b8 |
| 0f6c | JUMPI | |
| 0f6d | PUSH2 | 0x07aa |
| 0f70 | DUP2 | |
| 0f71 | DUP4 | |
| 0f72 | PUSH2 | 0x16f8 |
| 0f75 | JUMP | |
| 0f76 | JUMPDEST | |
| 0f77 | DUP8 | |
| 0f78 | PUSH2 | 0x0d47 |
| 0f7b | JUMP | |
| 0f7c | JUMPDEST | |
| 0f7d | CALLVALUE | |
| 0f7e | PUSH2 | 0x02b5 |
| 0f81 | JUMPI | |
| 0f82 | PUSH0 | |
| 0f83 | CALLDATASIZE | |
| 0f84 | PUSH1 | 0x03 |
| 0f86 | NOT | |
| 0f87 | ADD | |
| 0f88 | SLT | |
| 0f89 | PUSH2 | 0x02b5 |
| 0f8c | JUMPI | |
| 0f8d | PUSH2 | 0x0328 |
| 0f90 | PUSH2 | 0x0cd2 |
| 0f93 | PUSH1 | 0x06 |
| 0f95 | SLOAD | |
| 0f96 | PUSH2 | 0x1a64 |
| 0f99 | JUMP | |
| 0f9a | JUMPDEST | |
| 0f9b | CALLVALUE | |
| 0f9c | PUSH2 | 0x02b5 |
| 0f9f | JUMPI | |
| 0fa0 | PUSH0 | |
| 0fa1 | CALLDATASIZE | |
| 0fa2 | PUSH1 | 0x03 |
| 0fa4 | NOT | |
| 0fa5 | ADD | |
| 0fa6 | SLT | |
| 0fa7 | PUSH2 | 0x02b5 |
| 0faa | JUMPI | |
| 0fab | PUSH1 | 0x20 |
| 0fad | PUSH1 | 0x40 |
| 0faf | MLOAD | |
| 0fb0 | PUSH32 | 0xb66ca34dc0d9a9daa6230aee35894330ccfa7e4eaa29a198577eed0b26a41205 |
| 0fd1 | DUP2 | |
| 0fd2 | MSTORE | |
| 0fd3 | RETURN | |
| 0fd4 | JUMPDEST | |
| 0fd5 | CALLVALUE | |
| 0fd6 | PUSH2 | 0x02b5 |
| 0fd9 | JUMPI | |
| 0fda | PUSH2 | 0x0fe2 |
| 0fdd | CALLDATASIZE | |
| 0fde | PUSH2 | 0x15cf |
| 0fe1 | JUMP | |
| 0fe2 | JUMPDEST | |
| 0fe3 | PUSH2 | 0x0ffe |
| 0fe6 | PUSH2 | 0x0ff8 |
| 0fe9 | PUSH2 | 0x0ff2 |
| 0fec | DUP4 | |
| 0fed | DUP6 | |
| 0fee | PUSH2 | 0x17ab |
| 0ff1 | JUMP | |
| 0ff2 | JUMPDEST | |
| 0ff3 | SWAP4 | |
| 0ff4 | PUSH2 | 0x16c3 |
| 0ff7 | JUMP | |
| 0ff8 | JUMPDEST | |
| 0ff9 | SWAP2 | |
| 0ffa | PUSH2 | 0x1b22 |
| 0ffd | JUMP | |
| 0ffe | JUMPDEST | |
| 0fff | PUSH2 | 0x101a |
| 1002 | PUSH1 | 0x40 |
| 1004 | MLOAD | |
| 1005 | SWAP4 | |
| 1006 | DUP5 | |
| 1007 | SWAP4 | |
| 1008 | DUP5 | |
| 1009 | MSTORE | |
| 100a | PUSH1 | 0x60 |
| 100c | PUSH1 | 0x20 |
| 100e | DUP6 | |
| 100f | ADD | |
| 1010 | MSTORE | |
| 1011 | PUSH1 | 0x60 |
| 1013 | DUP5 | |
| 1014 | ADD | |
| 1015 | SWAP1 | |
| 1016 | PUSH2 | 0x15e5 |
| 1019 | JUMP | |
| 101a | JUMPDEST | |
| 101b | SWAP1 | |
| 101c | PUSH1 | 0x40 |
| 101e | DUP4 | |
| 101f | ADD | |
| 1020 | MSTORE | |
| 1021 | SUB | |
| 1022 | SWAP1 | |
| 1023 | RETURN | |
| 1024 | JUMPDEST | |
| 1025 | CALLVALUE | |
| 1026 | PUSH2 | 0x02b5 |
| 1029 | JUMPI | |
| 102a | PUSH0 | |
| 102b | CALLDATASIZE | |
| 102c | PUSH1 | 0x03 |
| 102e | NOT | |
| 102f | ADD | |
| 1030 | SLT | |
| 1031 | PUSH2 | 0x02b5 |
| 1034 | JUMPI | |
| 1035 | PUSH1 | 0x20 |
| 1037 | PUSH1 | 0x01 |
| 1039 | SLOAD | |
| 103a | PUSH1 | 0x40 |
| 103c | MLOAD | |
| 103d | SWAP1 | |
| 103e | DUP2 | |
| 103f | MSTORE | |
| 1040 | RETURN | |
| 1041 | JUMPDEST | |
| 1042 | CALLVALUE | |
| 1043 | PUSH2 | 0x02b5 |
| 1046 | JUMPI | |
| 1047 | PUSH1 | 0x80 |
| 1049 | CALLDATASIZE | |
| 104a | PUSH1 | 0x03 |
| 104c | NOT | |
| 104d | ADD | |
| 104e | SLT | |
| 104f | PUSH2 | 0x02b5 |
| 1052 | JUMPI | |
| 1053 | PUSH1 | 0x04 |
| 1055 | CALLDATALOAD | |
| 1056 | PUSH1 | 0x01 |
| 1058 | PUSH1 | 0x01 |
| 105a | PUSH1 | 0x40 |
| 105c | SHL | |
| 105d | SUB | |
| 105e | DUP2 | |
| 105f | GT | |
| 1060 | PUSH2 | 0x02b5 |
| 1063 | JUMPI | |
| 1064 | PUSH2 | 0x1071 |
| 1067 | SWAP1 | |
| 1068 | CALLDATASIZE | |
| 1069 | SWAP1 | |
| 106a | PUSH1 | 0x04 |
| 106c | ADD | |
| 106d | PUSH2 | 0x1618 |
| 1070 | JUMP | |
| 1071 | JUMPDEST | |
| 1072 | SWAP1 | |
| 1073 | PUSH1 | 0x24 |
| 1075 | CALLDATALOAD | |
| 1076 | PUSH1 | 0x01 |
| 1078 | PUSH1 | 0x01 |
| 107a | PUSH1 | 0x40 |
| 107c | SHL | |
| 107d | SUB | |
| 107e | DUP2 | |
| 107f | GT | |
| 1080 | PUSH2 | 0x02b5 |
| 1083 | JUMPI | |
| 1084 | PUSH2 | 0x1091 |
| 1087 | SWAP1 | |
| 1088 | CALLDATASIZE | |
| 1089 | SWAP1 | |
| 108a | PUSH1 | 0x04 |
| 108c | ADD | |
| 108d | PUSH2 | 0x1618 |
| 1090 | JUMP | |
| 1091 | JUMPDEST | |
| 1092 | PUSH2 | 0x109c |
| 1095 | SWAP4 | |
| 1096 | SWAP2 | |
| 1097 | SWAP4 | |
| 1098 | PUSH2 | 0x1648 |
| 109b | JUMP | |
| 109c | JUMPDEST | |
| 109d | SWAP4 | |
| 109e | PUSH1 | 0x64 |
| 10a0 | CALLDATALOAD | |
| 10a1 | PUSH1 | 0x01 |
| 10a3 | PUSH1 | 0x01 |
| 10a5 | PUSH1 | 0x40 |
| 10a7 | SHL | |
| 10a8 | SUB | |
| 10a9 | DUP2 | |
| 10aa | GT | |
| 10ab | PUSH2 | 0x02b5 |
| 10ae | JUMPI | |
| 10af | PUSH2 | 0x10bc |
| 10b2 | SWAP1 | |
| 10b3 | CALLDATASIZE | |
| 10b4 | SWAP1 | |
| 10b5 | PUSH1 | 0x04 |
| 10b7 | ADD | |
| 10b8 | PUSH2 | 0x1618 |
| 10bb | JUMP | |
| 10bc | JUMPDEST | |
| 10bd | SWAP6 | |
| 10be | DUP4 | |
| 10bf | ISZERO | |
| 10c0 | PUSH2 | 0x02a6 |
| 10c3 | JUMPI | |
| 10c4 | DUP4 | |
| 10c5 | DUP6 | |
| 10c6 | SUB | |
| 10c7 | PUSH2 | 0x1548 |
| 10ca | JUMPI | |
| 10cb | PUSH1 | 0x01 |
| 10cd | SLOAD | |
| 10ce | DUP1 | |
| 10cf | ISZERO | |
| 10d0 | PUSH2 | 0x0297 |
| 10d3 | JUMPI | |
| 10d4 | PUSH1 | 0x02 |
| 10d6 | SWAP8 | |
| 10d7 | SWAP6 | |
| 10d8 | SWAP8 | |
| 10d9 | SWAP7 | |
| 10da | SWAP4 | |
| 10db | SWAP7 | |
| 10dc | SLOAD | |
| 10dd | SWAP3 | |
| 10de | PUSH1 | 0x06 |
| 10e0 | SLOAD | |
| 10e1 | SWAP7 | |
| 10e2 | PUSH1 | 0x40 |
| 10e4 | MLOAD | |
| 10e5 | PUSH1 | 0x01 |
| 10e7 | PUSH1 | 0x01 |
| 10e9 | PUSH1 | 0x40 |
| 10eb | SHL | |
| 10ec | SUB | |
| 10ed | DUP7 | |
| 10ee | AND | |
| 10ef | PUSH1 | 0x20 |
| 10f1 | DUP3 | |
| 10f2 | ADD | |
| 10f3 | MSTORE | |
| 10f4 | DUP9 | |
| 10f5 | PUSH1 | 0x40 |
| 10f7 | DUP3 | |
| 10f8 | ADD | |
| 10f9 | MSTORE | |
| 10fa | PUSH1 | 0x80 |
| 10fc | PUSH1 | 0x60 |
| 10fe | DUP3 | |
| 10ff | ADD | |
| 1100 | MSTORE | |
| 1101 | PUSH2 | 0x110e |
| 1104 | PUSH1 | 0xa0 |
| 1106 | DUP3 | |
| 1107 | ADD | |
| 1108 | DUP13 | |
| 1109 | DUP10 | |
| 110a | PUSH2 | 0x198a |
| 110d | JUMP | |
| 110e | JUMPDEST | |
| 110f | PUSH1 | 0x1f |
| 1111 | NOT | |
| 1112 | DUP3 | |
| 1113 | DUP3 | |
| 1114 | SUB | |
| 1115 | ADD | |
| 1116 | PUSH1 | 0x80 |
| 1118 | DUP4 | |
| 1119 | ADD | |
| 111a | MSTORE | |
| 111b | DUP9 | |
| 111c | DUP2 | |
| 111d | MSTORE | |
| 111e | PUSH1 | 0x20 |
| 1120 | DUP2 | |
| 1121 | ADD | |
| 1122 | SWAP1 | |
| 1123 | PUSH1 | 0x20 |
| 1125 | DUP11 | |
| 1126 | PUSH1 | 0x05 |
| 1128 | SHL | |
| 1129 | DUP3 | |
| 112a | ADD | |
| 112b | ADD | |
| 112c | SWAP2 | |
| 112d | DUP13 | |
| 112e | SWAP2 | |
| 112f | PUSH0 | |
| 1130 | JUMPDEST | |
| 1131 | DUP13 | |
| 1132 | DUP2 | |
| 1133 | LT | |
| 1134 | PUSH2 | 0x14dc |
| 1137 | JUMPI | |
| 1138 | POP | |
| 1139 | POP | |
| 113a | POP | |
| 113b | POP | |
| 113c | SWAP1 | |
| 113d | PUSH2 | 0x1156 |
| 1140 | DUP2 | |
| 1141 | PUSH2 | 0x11dd |
| 1144 | SWAP8 | |
| 1145 | SWAP7 | |
| 1146 | SWAP6 | |
| 1147 | SWAP5 | |
| 1148 | SWAP4 | |
| 1149 | SUB | |
| 114a | PUSH1 | 0x1f |
| 114c | NOT | |
| 114d | DUP2 | |
| 114e | ADD | |
| 114f | DUP4 | |
| 1150 | MSTORE | |
| 1151 | DUP3 | |
| 1152 | PUSH2 | 0x16f8 |
| 1155 | JUMP | |
| 1156 | JUMPDEST | |
| 1157 | PUSH1 | 0x20 |
| 1159 | DUP2 | |
| 115a | MLOAD | |
| 115b | SWAP2 | |
| 115c | ADD | |
| 115d | KECCAK256 | |
| 115e | PUSH1 | 0x40 |
| 1160 | MLOAD | |
| 1161 | PUSH1 | 0x20 |
| 1163 | DUP2 | |
| 1164 | ADD | |
| 1165 | SWAP2 | |
| 1166 | PUSH32 | 0xd850f5df47b124511e8e6ec99cf1a0beaf7c6237eff0a31305ce53d85f312675 |
| 1187 | DUP4 | |
| 1188 | MSTORE | |
| 1189 | CHAINID | |
| 118a | PUSH1 | 0x40 |
| 118c | DUP4 | |
| 118d | ADD | |
| 118e | MSTORE | |
| 118f | ADDRESS | |
| 1190 | PUSH1 | 0x60 |
| 1192 | DUP4 | |
| 1193 | ADD | |
| 1194 | MSTORE | |
| 1195 | PUSH32 | 0x2e1c2ff2f9bb13fd926fe3e8b209f98e6c873bb259534a2148ca355409247cba |
| 11b6 | PUSH1 | 0x80 |
| 11b8 | DUP4 | |
| 11b9 | ADD | |
| 11ba | MSTORE | |
| 11bb | PUSH1 | 0x01 |
| 11bd | PUSH1 | 0x01 |
| 11bf | PUSH1 | 0x40 |
| 11c1 | SHL | |
| 11c2 | SUB | |
| 11c3 | DUP8 | |
| 11c4 | AND | |
| 11c5 | PUSH1 | 0xa0 |
| 11c7 | DUP4 | |
| 11c8 | ADD | |
| 11c9 | MSTORE | |
| 11ca | PUSH1 | 0xc0 |
| 11cc | DUP3 | |
| 11cd | ADD | |
| 11ce | MSTORE | |
| 11cf | PUSH1 | 0xc0 |
| 11d1 | DUP2 | |
| 11d2 | MSTORE | |
| 11d3 | PUSH2 | 0x0223 |
| 11d6 | PUSH1 | 0xe0 |
| 11d8 | DUP3 | |
| 11d9 | PUSH2 | 0x16f8 |
| 11dc | JUMP | |
| 11dd | JUMPDEST | |
| 11de | POP | |
| 11df | PUSH1 | 0x01 |
| 11e1 | PUSH1 | 0x01 |
| 11e3 | PUSH1 | 0x40 |
| 11e5 | SHL | |
| 11e6 | SUB | |
| 11e7 | PUSH2 | 0x11f1 |
| 11ea | DUP2 | |
| 11eb | DUP4 | |
| 11ec | AND | |
| 11ed | PUSH2 | 0x19ae |
| 11f0 | JUMP | |
| 11f1 | JUMPDEST | |
| 11f2 | PUSH8 | 0xffffffffffffffff |
| 11fb | NOT | |
| 11fc | SWAP1 | |
| 11fd | SWAP3 | |
| 11fe | AND | |
| 11ff | SWAP2 | |
| 1200 | AND | |
| 1201 | OR | |
| 1202 | PUSH1 | 0x02 |
| 1204 | SSTORE | |
| 1205 | PUSH0 | |
| 1206 | SWAP5 | |
| 1207 | PUSH32 | 0x000000000000000000000000f2b3161ed308717da082ee706cfdd27048e0d62d |
| 1228 | PUSH1 | 0x01 |
| 122a | PUSH1 | 0x01 |
| 122c | PUSH1 | 0xa0 |
| 122e | SHL | |
| 122f | SUB | |
| 1230 | AND | |
| 1231 | JUMPDEST | |
| 1232 | DUP4 | |
| 1233 | DUP8 | |
| 1234 | LT | |
| 1235 | ISZERO | |
| 1236 | PUSH2 | 0x14d1 |
| 1239 | JUMPI | |
| 123a | DUP7 | |
| 123b | PUSH1 | 0x05 |
| 123d | SHL | |
| 123e | DUP7 | |
| 123f | ADD | |
| 1240 | CALLDATALOAD | |
| 1241 | PUSH1 | 0x1e |
| 1243 | NOT | |
| 1244 | DUP8 | |
| 1245 | CALLDATASIZE | |
| 1246 | SUB | |
| 1247 | ADD | |
| 1248 | DUP2 | |
| 1249 | SLT | |
| 124a | ISZERO | |
| 124b | PUSH2 | 0x02b5 |
| 124e | JUMPI | |
| 124f | DUP7 | |
| 1250 | ADD | |
| 1251 | DUP1 | |
| 1252 | CALLDATALOAD | |
| 1253 | SWAP1 | |
| 1254 | PUSH1 | 0x01 |
| 1256 | PUSH1 | 0x01 |
| 1258 | PUSH1 | 0x40 |
| 125a | SHL | |
| 125b | SUB | |
| 125c | DUP3 | |
| 125d | GT | |
| 125e | PUSH2 | 0x02b5 |
| 1261 | JUMPI | |
| 1262 | PUSH1 | 0x20 |
| 1264 | ADD | |
| 1265 | SWAP1 | |
| 1266 | DUP1 | |
| 1267 | PUSH1 | 0x05 |
| 1269 | SHL | |
| 126a | CALLDATASIZE | |
| 126b | SUB | |
| 126c | DUP3 | |
| 126d | SGT | |
| 126e | PUSH2 | 0x02b5 |
| 1271 | JUMPI | |
| 1272 | DUP1 | |
| 1273 | ISZERO | |
| 1274 | PUSH2 | 0x14be |
| 1277 | JUMPI | |
| 1278 | PUSH2 | 0x1282 |
| 127b | DUP10 | |
| 127c | DUP6 | |
| 127d | DUP8 | |
| 127e | PUSH2 | 0x19cc |
| 1281 | JUMP | |
| 1282 | JUMPDEST | |
| 1283 | CALLDATALOAD | |
| 1284 | ISZERO | |
| 1285 | PUSH2 | 0x14ab |
| 1288 | JUMPI | |
| 1289 | PUSH1 | 0x01 |
| 128b | DUP2 | |
| 128c | ADD | |
| 128d | DUP1 | |
| 128e | DUP3 | |
| 128f | GT | |
| 1290 | PUSH2 | 0x047c |
| 1293 | JUMPI | |
| 1294 | PUSH2 | 0x129c |
| 1297 | SWAP1 | |
| 1298 | PUSH2 | 0x1744 |
| 129b | JUMP | |
| 129c | JUMPDEST | |
| 129d | SWAP2 | |
| 129e | PUSH2 | 0x12a8 |
| 12a1 | DUP11 | |
| 12a2 | DUP7 | |
| 12a3 | DUP9 | |
| 12a4 | PUSH2 | 0x19cc |
| 12a7 | JUMP | |
| 12a8 | JUMPDEST | |
| 12a9 | CALLDATALOAD | |
| 12aa | PUSH2 | 0x12b2 |
| 12ad | DUP5 | |
| 12ae | PUSH2 | 0x1776 |
| 12b1 | JUMP | |
| 12b2 | JUMPDEST | |
| 12b3 | MSTORE | |
| 12b4 | PUSH0 | |
| 12b5 | JUMPDEST | |
| 12b6 | DUP3 | |
| 12b7 | DUP2 | |
| 12b8 | LT | |
| 12b9 | PUSH2 | 0x1434 |
| 12bc | JUMPI | |
| 12bd | POP | |
| 12be | POP | |
| 12bf | POP | |
| 12c0 | DUP1 | |
| 12c1 | MLOAD | |
| 12c2 | ISZERO | |
| 12c3 | PUSH2 | 0x1425 |
| 12c6 | JUMPI | |
| 12c7 | JUMPDEST | |
| 12c8 | DUP1 | |
| 12c9 | MLOAD | |
| 12ca | PUSH1 | 0x01 |
| 12cc | DUP2 | |
| 12cd | GT | |
| 12ce | ISZERO | |
| 12cf | PUSH2 | 0x13f9 |
| 12d2 | JUMPI | |
| 12d3 | DUP1 | |
| 12d4 | PUSH1 | 0x01 |
| 12d6 | SHR | |
| 12d7 | SWAP1 | |
| 12d8 | PUSH1 | 0x01 |
| 12da | DUP2 | |
| 12db | AND | |
| 12dc | SWAP3 | |
| 12dd | PUSH2 | 0x12e9 |
| 12e0 | PUSH2 | 0x0303 |
| 12e3 | DUP6 | |
| 12e4 | DUP6 | |
| 12e5 | PUSH2 | 0x16eb |
| 12e8 | JUMP | |
| 12e9 | JUMPDEST | |
| 12ea | SWAP4 | |
| 12eb | PUSH0 | |
| 12ec | JUMPDEST | |
| 12ed | DUP5 | |
| 12ee | DUP2 | |
| 12ef | LT | |
| 12f0 | PUSH2 | 0x1379 |
| 12f3 | JUMPI | |
| 12f4 | POP | |
| 12f5 | PUSH1 | 0x01 |
| 12f7 | EQ | |
| 12f8 | PUSH2 | 0x1304 |
| 12fb | JUMPI | |
| 12fc | JUMPDEST | |
| 12fd | POP | |
| 12fe | POP | |
| 12ff | POP | |
| 1300 | PUSH2 | 0x12c7 |
| 1303 | JUMP | |
| 1304 | JUMPDEST | |
| 1305 | PUSH0 | |
| 1306 | NOT | |
| 1307 | DUP3 | |
| 1308 | ADD | |
| 1309 | SWAP2 | |
| 130a | DUP3 | |
| 130b | GT | |
| 130c | PUSH2 | 0x047c |
| 130f | JUMPI | |
| 1310 | PUSH2 | 0x1370 |
| 1313 | SWAP2 | |
| 1314 | PUSH2 | 0x131c |
| 1317 | SWAP2 | |
| 1318 | PUSH2 | 0x1797 |
| 131b | JUMP | |
| 131c | JUMPDEST | |
| 131d | MLOAD | |
| 131e | PUSH1 | 0x40 |
| 1320 | MLOAD | |
| 1321 | PUSH1 | 0x20 |
| 1323 | DUP2 | |
| 1324 | ADD | |
| 1325 | SWAP2 | |
| 1326 | PUSH1 | 0x01 |
| 1328 | PUSH1 | 0xf9 |
| 132a | SHL | |
| 132b | DUP4 | |
| 132c | MSTORE | |
| 132d | PUSH32 | 0xc976f483968b324bd57de8efa226478a3634db61776dacd4da866f8fa37c0fd5 |
| 134e | PUSH1 | 0x21 |
| 1350 | DUP4 | |
| 1351 | ADD | |
| 1352 | MSTORE | |
| 1353 | PUSH1 | 0x41 |
| 1355 | DUP3 | |
| 1356 | ADD | |
| 1357 | MSTORE | |
| 1358 | PUSH1 | 0x41 |
| 135a | DUP2 | |
| 135b | MSTORE | |
| 135c | PUSH2 | 0x1366 |
| 135f | PUSH1 | 0x61 |
| 1361 | DUP3 | |
| 1362 | PUSH2 | 0x16f8 |
| 1365 | JUMP | |
| 1366 | JUMPDEST | |
| 1367 | MLOAD | |
| 1368 | SWAP1 | |
| 1369 | KECCAK256 | |
| 136a | SWAP2 | |
| 136b | DUP4 | |
| 136c | PUSH2 | 0x1797 |
| 136f | JUMP | |
| 1370 | JUMPDEST | |
| 1371 | MSTORE | |
| 1372 | DUP9 | |
| 1373 | DUP1 | |
| 1374 | DUP1 | |
| 1375 | PUSH2 | 0x12fc |
| 1378 | JUMP | |
| 1379 | JUMPDEST | |
| 137a | DUP1 | |
| 137b | PUSH1 | 0x01 |
| 137d | SWAP2 | |
| 137e | DUP3 | |
| 137f | SHL | |
| 1380 | PUSH2 | 0x1396 |
| 1383 | DUP4 | |
| 1384 | PUSH2 | 0x138d |
| 1387 | DUP4 | |
| 1388 | DUP9 | |
| 1389 | PUSH2 | 0x1797 |
| 138c | JUMP | |
| 138d | JUMPDEST | |
| 138e | MLOAD | |
| 138f | SWAP3 | |
| 1390 | OR | |
| 1391 | DUP7 | |
| 1392 | PUSH2 | 0x1797 |
| 1395 | JUMP | |
| 1396 | JUMPDEST | |
| 1397 | MLOAD | |
| 1398 | PUSH1 | 0x40 |
| 139a | MLOAD | |
| 139b | SWAP1 | |
| 139c | PUSH1 | 0x20 |
| 139e | DUP3 | |
| 139f | ADD | |
| 13a0 | SWAP3 | |
| 13a1 | DUP6 | |
| 13a2 | PUSH1 | 0xf8 |
| 13a4 | SHL | |
| 13a5 | DUP5 | |
| 13a6 | MSTORE | |
| 13a7 | PUSH32 | 0xc976f483968b324bd57de8efa226478a3634db61776dacd4da866f8fa37c0fd5 |
| 13c8 | PUSH1 | 0x21 |
| 13ca | DUP5 | |
| 13cb | ADD | |
| 13cc | MSTORE | |
| 13cd | PUSH1 | 0x41 |
| 13cf | DUP4 | |
| 13d0 | ADD | |
| 13d1 | MSTORE | |
| 13d2 | PUSH1 | 0x61 |
| 13d4 | DUP3 | |
| 13d5 | ADD | |
| 13d6 | MSTORE | |
| 13d7 | PUSH1 | 0x61 |
| 13d9 | DUP2 | |
| 13da | MSTORE | |
| 13db | PUSH2 | 0x13e5 |
| 13de | PUSH1 | 0x81 |
| 13e0 | DUP3 | |
| 13e1 | PUSH2 | 0x16f8 |
| 13e4 | JUMP | |
| 13e5 | JUMPDEST | |
| 13e6 | MLOAD | |
| 13e7 | SWAP1 | |
| 13e8 | KECCAK256 | |
| 13e9 | PUSH2 | 0x13f2 |
| 13ec | DUP3 | |
| 13ed | DUP10 | |
| 13ee | PUSH2 | 0x1797 |
| 13f1 | JUMP | |
| 13f2 | JUMPDEST | |
| 13f3 | MSTORE | |
| 13f4 | ADD | |
| 13f5 | PUSH2 | 0x12ec |
| 13f8 | JUMP | |
| 13f9 | JUMPDEST | |
| 13fa | POP | |
| 13fb | SWAP7 | |
| 13fc | PUSH2 | 0x1417 |
| 13ff | PUSH2 | 0x1411 |
| 1402 | PUSH1 | 0x01 |
| 1404 | SWAP4 | |
| 1405 | SWAP7 | |
| 1406 | SWAP10 | |
| 1407 | SWAP9 | |
| 1408 | SWAP6 | |
| 1409 | SWAP9 | |
| 140a | SWAP8 | |
| 140b | SWAP5 | |
| 140c | SWAP8 | |
| 140d | PUSH2 | 0x1776 |
| 1410 | JUMP | |
| 1411 | JUMPDEST | |
| 1412 | MLOAD | |
| 1413 | PUSH2 | 0x1edd |
| 1416 | JUMP | |
| 1417 | JUMPDEST | |
| 1418 | ADD | |
| 1419 | SWAP6 | |
| 141a | SWAP3 | |
| 141b | SWAP5 | |
| 141c | SWAP2 | |
| 141d | SWAP5 | |
| 141e | SWAP4 | |
| 141f | SWAP1 | |
| 1420 | SWAP4 | |
| 1421 | PUSH2 | 0x1231 |
| 1424 | JUMP | |
| 1425 | JUMPDEST | |
| 1426 | PUSH4 | 0x4f297b61 |
| 142b | PUSH1 | 0xe1 |
| 142d | SHL | |
| 142e | PUSH0 | |
| 142f | MSTORE | |
| 1430 | PUSH1 | 0x04 |
| 1432 | PUSH0 | |
| 1433 | REVERT | |
| 1434 | JUMPDEST | |
| 1435 | PUSH2 | 0x143f |
| 1438 | DUP2 | |
| 1439 | DUP5 | |
| 143a | DUP5 | |
| 143b | PUSH2 | 0x19cc |
| 143e | JUMP | |
| 143f | JUMPDEST | |
| 1440 | CALLDATALOAD | |
| 1441 | DUP6 | |
| 1442 | EXTCODESIZE | |
| 1443 | ISZERO | |
| 1444 | PUSH2 | 0x02b5 |
| 1447 | JUMPI | |
| 1448 | PUSH1 | 0x40 |
| 144a | MLOAD | |
| 144b | SWAP1 | |
| 144c | PUSH4 | 0xaf6f8c1b |
| 1451 | PUSH1 | 0xe0 |
| 1453 | SHL | |
| 1454 | DUP3 | |
| 1455 | MSTORE | |
| 1456 | PUSH1 | 0x04 |
| 1458 | DUP3 | |
| 1459 | ADD | |
| 145a | MSTORE | |
| 145b | PUSH0 | |
| 145c | DUP2 | |
| 145d | PUSH1 | 0x24 |
| 145f | DUP2 | |
| 1460 | DUP4 | |
| 1461 | DUP11 | |
| 1462 | GAS | |
| 1463 | CALL | |
| 1464 | DUP1 | |
| 1465 | ISZERO | |
| 1466 | PUSH2 | 0x067b |
| 1469 | JUMPI | |
| 146a | PUSH2 | 0x149b |
| 146d | JUMPI | |
| 146e | JUMPDEST | |
| 146f | POP | |
| 1470 | PUSH2 | 0x147a |
| 1473 | DUP2 | |
| 1474 | DUP5 | |
| 1475 | DUP5 | |
| 1476 | PUSH2 | 0x19cc |
| 1479 | JUMP | |
| 147a | JUMPDEST | |
| 147b | CALLDATALOAD | |
| 147c | SWAP1 | |
| 147d | PUSH1 | 0x01 |
| 147f | DUP2 | |
| 1480 | ADD | |
| 1481 | SWAP2 | |
| 1482 | DUP3 | |
| 1483 | DUP3 | |
| 1484 | GT | |
| 1485 | PUSH2 | 0x047c |
| 1488 | JUMPI | |
| 1489 | PUSH2 | 0x1494 |
| 148c | PUSH1 | 0x01 |
| 148e | SWAP4 | |
| 148f | DUP8 | |
| 1490 | PUSH2 | 0x1797 |
| 1493 | JUMP | |
| 1494 | JUMPDEST | |
| 1495 | MSTORE | |
| 1496 | ADD | |
| 1497 | PUSH2 | 0x12b5 |
| 149a | JUMP | |
| 149b | JUMPDEST | |
| 149c | PUSH0 | |
| 149d | PUSH2 | 0x14a5 |
| 14a0 | SWAP2 | |
| 14a1 | PUSH2 | 0x16f8 |
| 14a4 | JUMP | |
| 14a5 | JUMPDEST | |
| 14a6 | DUP12 | |
| 14a7 | PUSH2 | 0x146e |
| 14aa | JUMP | |
| 14ab | JUMPDEST | |
| 14ac | DUP9 | |
| 14ad | PUSH4 | 0x22566cfd |
| 14b2 | PUSH1 | 0xe0 |
| 14b4 | SHL | |
| 14b5 | PUSH0 | |
| 14b6 | MSTORE | |
| 14b7 | PUSH1 | 0x04 |
| 14b9 | MSTORE | |
| 14ba | PUSH1 | 0x24 |
| 14bc | PUSH0 | |
| 14bd | REVERT | |
| 14be | JUMPDEST | |
| 14bf | DUP9 | |
| 14c0 | PUSH4 | 0xc9cdeff5 |
| 14c5 | PUSH1 | 0xe0 |
| 14c7 | SHL | |
| 14c8 | PUSH0 | |
| 14c9 | MSTORE | |
| 14ca | PUSH1 | 0x04 |
| 14cc | MSTORE | |
| 14cd | PUSH1 | 0x24 |
| 14cf | PUSH0 | |
| 14d0 | REVERT | |
| 14d1 | JUMPDEST | |
| 14d2 | PUSH1 | 0x20 |
| 14d4 | DUP6 | |
| 14d5 | PUSH1 | 0x40 |
| 14d7 | MLOAD | |
| 14d8 | SWAP1 | |
| 14d9 | DUP2 | |
| 14da | MSTORE | |
| 14db | RETURN | |
| 14dc | JUMPDEST | |
| 14dd | SWAP1 | |
| 14de | SWAP2 | |
| 14df | SWAP3 | |
| 14e0 | SWAP4 | |
| 14e1 | SWAP13 | |
| 14e2 | SWAP15 | |
| 14e3 | SWAP13 | |
| 14e4 | PUSH1 | 0x1f |
| 14e6 | SWAP15 | |
| 14e7 | SWAP12 | |
| 14e8 | SWAP15 | |
| 14e9 | NOT | |
| 14ea | DUP4 | |
| 14eb | DUP3 | |
| 14ec | SUB | |
| 14ed | ADD | |
| 14ee | DUP5 | |
| 14ef | MSTORE | |
| 14f0 | PUSH1 | 0x1e |
| 14f2 | NOT | |
| 14f3 | DUP13 | |
| 14f4 | CALLDATASIZE | |
| 14f5 | SUB | |
| 14f6 | ADD | |
| 14f7 | DUP6 | |
| 14f8 | CALLDATALOAD | |
| 14f9 | SLT | |
| 14fa | ISZERO | |
| 14fb | PUSH2 | 0x02b5 |
| 14fe | JUMPI | |
| 14ff | DUP12 | |
| 1500 | DUP6 | |
| 1501 | CALLDATALOAD | |
| 1502 | ADD | |
| 1503 | SWAP1 | |
| 1504 | PUSH1 | 0x20 |
| 1506 | DUP3 | |
| 1507 | CALLDATALOAD | |
| 1508 | SWAP3 | |
| 1509 | ADD | |
| 150a | SWAP2 | |
| 150b | PUSH1 | 0x01 |
| 150d | PUSH1 | 0x01 |
| 150f | PUSH1 | 0x40 |
| 1511 | SHL | |
| 1512 | SUB | |
| 1513 | DUP2 | |
| 1514 | GT | |
| 1515 | PUSH2 | 0x02b5 |
| 1518 | JUMPI | |
| 1519 | DUP1 | |
| 151a | PUSH1 | 0x05 |
| 151c | SHL | |
| 151d | CALLDATASIZE | |
| 151e | SUB | |
| 151f | DUP4 | |
| 1520 | SGT | |
| 1521 | PUSH2 | 0x02b5 |
| 1524 | JUMPI | |
| 1525 | PUSH2 | 0x1534 |
| 1528 | PUSH1 | 0x20 |
| 152a | SWAP3 | |
| 152b | DUP4 | |
| 152c | SWAP3 | |
| 152d | PUSH1 | 0x01 |
| 152f | SWAP6 | |
| 1530 | PUSH2 | 0x198a |
| 1533 | JUMP | |
| 1534 | JUMPDEST | |
| 1535 | SWAP7 | |
| 1536 | ADD | |
| 1537 | SWAP5 | |
| 1538 | ADD | |
| 1539 | SWAP2 | |
| 153a | ADD | |
| 153b | SWAP15 | |
| 153c | SWAP13 | |
| 153d | SWAP15 | |
| 153e | SWAP14 | |
| 153f | SWAP11 | |
| 1540 | SWAP14 | |
| 1541 | SWAP2 | |
| 1542 | SWAP1 | |
| 1543 | SWAP2 | |
| 1544 | PUSH2 | 0x1130 |
| 1547 | JUMP | |
| 1548 | JUMPDEST | |
| 1549 | DUP4 | |
| 154a | DUP6 | |
| 154b | PUSH4 | 0x5b2d6423 |
| 1550 | PUSH1 | 0xe1 |
| 1552 | SHL | |
| 1553 | PUSH0 | |
| 1554 | MSTORE | |
| 1555 | PUSH1 | 0x04 |
| 1557 | MSTORE | |
| 1558 | PUSH1 | 0x24 |
| 155a | MSTORE | |
| 155b | PUSH1 | 0x44 |
| 155d | PUSH0 | |
| 155e | REVERT | |
| 155f | JUMPDEST | |
| 1560 | CALLVALUE | |
| 1561 | PUSH2 | 0x02b5 |
| 1564 | JUMPI | |
| 1565 | PUSH2 | 0x0328 |
| 1568 | PUSH2 | 0x0cd2 |
| 156b | PUSH2 | 0x1573 |
| 156e | CALLDATASIZE | |
| 156f | PUSH2 | 0x15cf |
| 1572 | JUMP | |
| 1573 | JUMPDEST | |
| 1574 | SWAP1 | |
| 1575 | PUSH2 | 0x17ab |
| 1578 | JUMP | |
| 1579 | JUMPDEST | |
| 157a | CALLVALUE | |
| 157b | PUSH2 | 0x02b5 |
| 157e | JUMPI | |
| 157f | PUSH1 | 0x20 |
| 1581 | CALLDATASIZE | |
| 1582 | PUSH1 | 0x03 |
| 1584 | NOT | |
| 1585 | ADD | |
| 1586 | SLT | |
| 1587 | PUSH2 | 0x02b5 |
| 158a | JUMPI | |
| 158b | PUSH1 | 0x20 |
| 158d | PUSH2 | 0x07f9 |
| 1590 | PUSH1 | 0x04 |
| 1592 | CALLDATALOAD | |
| 1593 | PUSH2 | 0x16c3 |
| 1596 | JUMP | |
| 1597 | JUMPDEST | |
| 1598 | CALLVALUE | |
| 1599 | PUSH2 | 0x02b5 |
| 159c | JUMPI | |
| 159d | PUSH0 | |
| 159e | CALLDATASIZE | |
| 159f | PUSH1 | 0x03 |
| 15a1 | NOT | |
| 15a2 | ADD | |
| 15a3 | SLT | |
| 15a4 | PUSH2 | 0x02b5 |
| 15a7 | JUMPI | |
| 15a8 | DUP1 | |
| 15a9 | PUSH32 | 0x9abdf9961fd14fd177480eccbad16b2d7f231898b2d763c8e8b50364d8b3b171 |
| 15ca | PUSH1 | 0x20 |
| 15cc | SWAP3 | |
| 15cd | MSTORE | |
| 15ce | RETURN | |
| 15cf | JUMPDEST | |
| 15d0 | PUSH1 | 0x40 |
| 15d2 | SWAP1 | |
| 15d3 | PUSH1 | 0x03 |
| 15d5 | NOT | |
| 15d6 | ADD | |
| 15d7 | SLT | |
| 15d8 | PUSH2 | 0x02b5 |
| 15db | JUMPI | |
| 15dc | PUSH1 | 0x04 |
| 15de | CALLDATALOAD | |
| 15df | SWAP1 | |
| 15e0 | PUSH1 | 0x24 |
| 15e2 | CALLDATALOAD | |
| 15e3 | SWAP1 | |
| 15e4 | JUMP | |
| 15e5 | JUMPDEST | |
| 15e6 | SWAP1 | |
| 15e7 | PUSH1 | 0x20 |
| 15e9 | DUP1 | |
| 15ea | DUP4 | |
| 15eb | MLOAD | |
| 15ec | SWAP3 | |
| 15ed | DUP4 | |
| 15ee | DUP2 | |
| 15ef | MSTORE | |
| 15f0 | ADD | |
| 15f1 | SWAP3 | |
| 15f2 | ADD | |
| 15f3 | SWAP1 | |
| 15f4 | PUSH0 | |
| 15f5 | JUMPDEST | |
| 15f6 | DUP2 | |
| 15f7 | DUP2 | |
| 15f8 | LT | |
| 15f9 | PUSH2 | 0x1602 |
| 15fc | JUMPI | |
| 15fd | POP | |
| 15fe | POP | |
| 15ff | POP | |
| 1600 | SWAP1 | |
| 1601 | JUMP | |
| 1602 | JUMPDEST | |
| 1603 | DUP3 | |
| 1604 | MLOAD | |
| 1605 | DUP5 | |
| 1606 | MSTORE | |
| 1607 | PUSH1 | 0x20 |
| 1609 | SWAP4 | |
| 160a | DUP5 | |
| 160b | ADD | |
| 160c | SWAP4 | |
| 160d | SWAP1 | |
| 160e | SWAP3 | |
| 160f | ADD | |
| 1610 | SWAP2 | |
| 1611 | PUSH1 | 0x01 |
| 1613 | ADD | |
| 1614 | PUSH2 | 0x15f5 |
| 1617 | JUMP | |
| 1618 | JUMPDEST | |
| 1619 | SWAP2 | |
| 161a | DUP2 | |
| 161b | PUSH1 | 0x1f |
| 161d | DUP5 | |
| 161e | ADD | |
| 161f | SLT | |
| 1620 | ISZERO | |
| 1621 | PUSH2 | 0x02b5 |
| 1624 | JUMPI | |
| 1625 | DUP3 | |
| 1626 | CALLDATALOAD | |
| 1627 | SWAP2 | |
| 1628 | PUSH1 | 0x01 |
| 162a | PUSH1 | 0x01 |
| 162c | PUSH1 | 0x40 |
| 162e | SHL | |
| 162f | SUB | |
| 1630 | DUP4 | |
| 1631 | GT | |
| 1632 | PUSH2 | 0x02b5 |
| 1635 | JUMPI | |
| 1636 | PUSH1 | 0x20 |
| 1638 | DUP1 | |
| 1639 | DUP6 | |
| 163a | ADD | |
| 163b | SWAP5 | |
| 163c | DUP5 | |
| 163d | PUSH1 | 0x05 |
| 163f | SHL | |
| 1640 | ADD | |
| 1641 | ADD | |
| 1642 | GT | |
| 1643 | PUSH2 | 0x02b5 |
| 1646 | JUMPI | |
| 1647 | JUMP | |
| 1648 | JUMPDEST | |
| 1649 | PUSH1 | 0x44 |
| 164b | CALLDATALOAD | |
| 164c | SWAP1 | |
| 164d | PUSH1 | 0x01 |
| 164f | PUSH1 | 0x01 |
| 1651 | PUSH1 | 0x40 |
| 1653 | SHL | |
| 1654 | SUB | |
| 1655 | DUP3 | |
| 1656 | AND | |
| 1657 | DUP3 | |
| 1658 | SUB | |
| 1659 | PUSH2 | 0x02b5 |
| 165c | JUMPI | |
| 165d | JUMP | |
| 165e | JUMPDEST | |
| 165f | SWAP1 | |
| 1660 | PUSH1 | 0x60 |
| 1662 | PUSH1 | 0x03 |
| 1664 | NOT | |
| 1665 | DUP4 | |
| 1666 | ADD | |
| 1667 | SLT | |
| 1668 | PUSH2 | 0x02b5 |
| 166b | JUMPI | |
| 166c | PUSH1 | 0x04 |
| 166e | CALLDATALOAD | |
| 166f | PUSH1 | 0x01 |
| 1671 | PUSH1 | 0x01 |
| 1673 | PUSH1 | 0x40 |
| 1675 | SHL | |
| 1676 | SUB | |
| 1677 | DUP2 | |
| 1678 | GT | |
| 1679 | PUSH2 | 0x02b5 |
| 167c | JUMPI | |
| 167d | DUP3 | |
| 167e | PUSH2 | 0x1689 |
| 1681 | SWAP2 | |
| 1682 | PUSH1 | 0x04 |
| 1684 | ADD | |
| 1685 | PUSH2 | 0x1618 |
| 1688 | JUMP | |
| 1689 | JUMPDEST | |
| 168a | SWAP3 | |
| 168b | SWAP1 | |
| 168c | SWAP3 | |
| 168d | SWAP2 | |
| 168e | PUSH1 | 0x24 |
| 1690 | CALLDATALOAD | |
| 1691 | PUSH1 | 0x01 |
| 1693 | PUSH1 | 0x01 |
| 1695 | PUSH1 | 0x40 |
| 1697 | SHL | |
| 1698 | SUB | |
| 1699 | DUP2 | |
| 169a | AND | |
| 169b | DUP2 | |
| 169c | SUB | |
| 169d | PUSH2 | 0x02b5 |
| 16a0 | JUMPI | |
| 16a1 | SWAP2 | |
| 16a2 | PUSH1 | 0x44 |
| 16a4 | CALLDATALOAD | |
| 16a5 | SWAP1 | |
| 16a6 | PUSH1 | 0x01 |
| 16a8 | PUSH1 | 0x01 |
| 16aa | PUSH1 | 0x40 |
| 16ac | SHL | |
| 16ad | SUB | |
| 16ae | DUP3 | |
| 16af | GT | |
| 16b0 | PUSH2 | 0x02b5 |
| 16b3 | JUMPI | |
| 16b4 | PUSH2 | 0x16bf |
| 16b7 | SWAP2 | |
| 16b8 | PUSH1 | 0x04 |
| 16ba | ADD | |
| 16bb | PUSH2 | 0x1618 |
| 16be | JUMP | |
| 16bf | JUMPDEST | |
| 16c0 | SWAP1 | |
| 16c1 | SWAP2 | |
| 16c2 | JUMP | |
| 16c3 | JUMPDEST | |
| 16c4 | PUSH1 | 0x06 |
| 16c6 | SLOAD | |
| 16c7 | DUP1 | |
| 16c8 | DUP3 | |
| 16c9 | LT | |
| 16ca | ISZERO | |
| 16cb | PUSH2 | 0x0356 |
| 16ce | JUMPI | |
| 16cf | POP | |
| 16d0 | PUSH0 | |
| 16d1 | MSTORE | |
| 16d2 | PUSH1 | 0x04 |
| 16d4 | PUSH1 | 0x20 |
| 16d6 | MSTORE | |
| 16d7 | PUSH1 | 0x40 |
| 16d9 | PUSH0 | |
| 16da | KECCAK256 | |
| 16db | SLOAD | |
| 16dc | SWAP1 | |
| 16dd | JUMP | |
| 16de | JUMPDEST | |
| 16df | SWAP2 | |
| 16e0 | SWAP1 | |
| 16e1 | DUP3 | |
| 16e2 | SUB | |
| 16e3 | SWAP2 | |
| 16e4 | DUP3 | |
| 16e5 | GT | |
| 16e6 | PUSH2 | 0x047c |
| 16e9 | JUMPI | |
| 16ea | JUMP | |
| 16eb | JUMPDEST | |
| 16ec | SWAP2 | |
| 16ed | SWAP1 | |
| 16ee | DUP3 | |
| 16ef | ADD | |
| 16f0 | DUP1 | |
| 16f1 | SWAP3 | |
| 16f2 | GT | |
| 16f3 | PUSH2 | 0x047c |
| 16f6 | JUMPI | |
| 16f7 | JUMP | |
| 16f8 | JUMPDEST | |
| 16f9 | SWAP1 | |
| 16fa | PUSH1 | 0x1f |
| 16fc | DUP1 | |
| 16fd | NOT | |
| 16fe | SWAP2 | |
| 16ff | ADD | |
| 1700 | AND | |
| 1701 | DUP2 | |
| 1702 | ADD | |
| 1703 | SWAP1 | |
| 1704 | DUP2 | |
| 1705 | LT | |
| 1706 | PUSH1 | 0x01 |
| 1708 | PUSH1 | 0x01 |
| 170a | PUSH1 | 0x40 |
| 170c | SHL | |
| 170d | SUB | |
| 170e | DUP3 | |
| 170f | GT | |
| 1710 | OR | |
| 1711 | PUSH2 | 0x1719 |
| 1714 | JUMPI | |
| 1715 | PUSH1 | 0x40 |
| 1717 | MSTORE | |
| 1718 | JUMP | |
| 1719 | JUMPDEST | |
| 171a | PUSH4 | 0x4e487b71 |
| 171f | PUSH1 | 0xe0 |
| 1721 | SHL | |
| 1722 | PUSH0 | |
| 1723 | MSTORE | |
| 1724 | PUSH1 | 0x41 |
| 1726 | PUSH1 | 0x04 |
| 1728 | MSTORE | |
| 1729 | PUSH1 | 0x24 |
| 172b | PUSH0 | |
| 172c | REVERT | |
| 172d | JUMPDEST | |
| 172e | PUSH1 | 0x01 |
| 1730 | PUSH1 | 0x01 |
| 1732 | PUSH1 | 0x40 |
| 1734 | SHL | |
| 1735 | SUB | |
| 1736 | DUP2 | |
| 1737 | GT | |
| 1738 | PUSH2 | 0x1719 |
| 173b | JUMPI | |
| 173c | PUSH1 | 0x05 |
| 173e | SHL | |
| 173f | PUSH1 | 0x20 |
| 1741 | ADD | |
| 1742 | SWAP1 | |
| 1743 | JUMP | |
| 1744 | JUMPDEST | |
| 1745 | SWAP1 | |
| 1746 | PUSH2 | 0x174e |
| 1749 | DUP3 | |
| 174a | PUSH2 | 0x172d |
| 174d | JUMP | |
| 174e | JUMPDEST | |
| 174f | PUSH2 | 0x175b |
| 1752 | PUSH1 | 0x40 |
| 1754 | MLOAD | |
| 1755 | SWAP2 | |
| 1756 | DUP3 | |
| 1757 | PUSH2 | 0x16f8 |
| 175a | JUMP | |
| 175b | JUMPDEST | |
| 175c | DUP3 | |
| 175d | DUP2 | |
| 175e | MSTORE | |
| 175f | DUP1 | |
| 1760 | SWAP3 | |
| 1761 | PUSH2 | 0x176c |
| 1764 | PUSH1 | 0x1f |
| 1766 | NOT | |
| 1767 | SWAP2 | |
| 1768 | PUSH2 | 0x172d |
| 176b | JUMP | |
| 176c | JUMPDEST | |
| 176d | ADD | |
| 176e | SWAP1 | |
| 176f | PUSH1 | 0x20 |
| 1771 | CALLDATASIZE | |
| 1772 | SWAP2 | |
| 1773 | ADD | |
| 1774 | CALLDATACOPY | |
| 1775 | JUMP | |
| 1776 | JUMPDEST | |
| 1777 | DUP1 | |
| 1778 | MLOAD | |
| 1779 | ISZERO | |
| 177a | PUSH2 | 0x1783 |
| 177d | JUMPI | |
| 177e | PUSH1 | 0x20 |
| 1780 | ADD | |
| 1781 | SWAP1 | |
| 1782 | JUMP | |
| 1783 | JUMPDEST | |
| 1784 | PUSH4 | 0x4e487b71 |
| 1789 | PUSH1 | 0xe0 |
| 178b | SHL | |
| 178c | PUSH0 | |
| 178d | MSTORE | |
| 178e | PUSH1 | 0x32 |
| 1790 | PUSH1 | 0x04 |
| 1792 | MSTORE | |
| 1793 | PUSH1 | 0x24 |
| 1795 | PUSH0 | |
| 1796 | REVERT | |
| 1797 | JUMPDEST | |
| 1798 | DUP1 | |
| 1799 | MLOAD | |
| 179a | DUP3 | |
| 179b | LT | |
| 179c | ISZERO | |
| 179d | PUSH2 | 0x1783 |
| 17a0 | JUMPI | |
| 17a1 | PUSH1 | 0x20 |
| 17a3 | SWAP2 | |
| 17a4 | PUSH1 | 0x05 |
| 17a6 | SHL | |
| 17a7 | ADD | |
| 17a8 | ADD | |
| 17a9 | SWAP1 | |
| 17aa | JUMP | |
| 17ab | JUMPDEST | |
| 17ac | SWAP2 | |
| 17ad | SWAP1 | |
| 17ae | PUSH1 | 0x06 |
| 17b0 | SLOAD | |
| 17b1 | DUP1 | |
| 17b2 | DUP3 | |
| 17b3 | GT | |
| 17b4 | PUSH2 | 0x036c |
| 17b7 | JUMPI | |
| 17b8 | POP | |
| 17b9 | DUP1 | |
| 17ba | ISZERO | |
| 17bb | PUSH2 | 0x197b |
| 17be | JUMPI | |
| 17bf | DUP1 | |
| 17c0 | DUP4 | |
| 17c1 | LT | |
| 17c2 | ISZERO | |
| 17c3 | PUSH2 | 0x1965 |
| 17c6 | JUMPI | |
| 17c7 | PUSH0 | |
| 17c8 | NOT | |
| 17c9 | DUP2 | |
| 17ca | ADD | |
| 17cb | SWAP1 | |
| 17cc | DUP1 | |
| 17cd | DUP3 | |
| 17ce | GT | |
| 17cf | PUSH2 | 0x047c |
| 17d2 | JUMPI | |
| 17d3 | SWAP1 | |
| 17d4 | PUSH2 | 0x17f5 |
| 17d7 | PUSH2 | 0x0303 |
| 17da | PUSH2 | 0x17e4 |
| 17dd | DUP4 | |
| 17de | DUP8 | |
| 17df | XOR | |
| 17e0 | PUSH2 | 0x1bf1 |
| 17e3 | JUMP | |
| 17e4 | JUMPDEST | |
| 17e5 | PUSH2 | 0x17ef |
| 17e8 | DUP8 | |
| 17e9 | DUP3 | |
| 17ea | SHR | |
| 17eb | PUSH2 | 0x1c0a |
| 17ee | JUMP | |
| 17ef | JUMPDEST | |
| 17f0 | SWAP1 | |
| 17f1 | PUSH2 | 0x16eb |
| 17f4 | JUMP | |
| 17f5 | JUMPDEST | |
| 17f6 | SWAP4 | |
| 17f7 | SWAP1 | |
| 17f8 | SWAP2 | |
| 17f9 | PUSH0 | |
| 17fa | DUP4 | |
| 17fb | PUSH0 | |
| 17fc | SWAP5 | |
| 17fd | JUMPDEST | |
| 17fe | PUSH2 | 0x1827 |
| 1801 | JUMPI | |
| 1802 | POP | |
| 1803 | POP | |
| 1804 | POP | |
| 1805 | POP | |
| 1806 | DUP3 | |
| 1807 | MLOAD | |
| 1808 | DUP1 | |
| 1809 | DUP3 | |
| 180a | SUB | |
| 180b | PUSH2 | 0x1812 |
| 180e | JUMPI | |
| 180f | POP | |
| 1810 | POP | |
| 1811 | JUMP | |
| 1812 | JUMPDEST | |
| 1813 | PUSH4 | 0x383613b5 |
| 1818 | PUSH1 | 0xe0 |
| 181a | SHL | |
| 181b | PUSH0 | |
| 181c | MSTORE | |
| 181d | PUSH1 | 0x04 |
| 181f | MSTORE | |
| 1820 | PUSH1 | 0x24 |
| 1822 | MSTORE | |
| 1823 | PUSH1 | 0x44 |
| 1825 | PUSH0 | |
| 1826 | REVERT | |
| 1827 | JUMPDEST | |
| 1828 | SWAP1 | |
| 1829 | SWAP2 | |
| 182a | SWAP3 | |
| 182b | SWAP4 | |
| 182c | PUSH1 | 0x01 |
| 182e | DUP6 | |
| 182f | XOR | |
| 1830 | DUP3 | |
| 1831 | DUP2 | |
| 1832 | LT | |
| 1833 | PUSH0 | |
| 1834 | EQ | |
| 1835 | PUSH2 | 0x1874 |
| 1838 | JUMPI | |
| 1839 | DUP4 | |
| 183a | SWAP3 | |
| 183b | SWAP2 | |
| 183c | PUSH1 | 0x01 |
| 183e | SWAP5 | |
| 183f | SWAP2 | |
| 1840 | DUP6 | |
| 1841 | SWAP3 | |
| 1842 | PUSH0 | |
| 1843 | MSTORE | |
| 1844 | PUSH1 | 0x03 |
| 1846 | PUSH1 | 0x20 |
| 1848 | MSTORE | |
| 1849 | PUSH1 | 0x40 |
| 184b | PUSH0 | |
| 184c | KECCAK256 | |
| 184d | SWAP1 | |
| 184e | PUSH0 | |
| 184f | MSTORE | |
| 1850 | PUSH1 | 0x20 |
| 1852 | MSTORE | |
| 1853 | PUSH1 | 0x40 |
| 1855 | PUSH0 | |
| 1856 | KECCAK256 | |
| 1857 | SLOAD | |
| 1858 | PUSH2 | 0x1861 |
| 185b | DUP3 | |
| 185c | DUP12 | |
| 185d | PUSH2 | 0x1797 |
| 1860 | JUMP | |
| 1861 | JUMPDEST | |
| 1862 | MSTORE | |
| 1863 | ADD | |
| 1864 | SWAP5 | |
| 1865 | JUMPDEST | |
| 1866 | DUP4 | |
| 1867 | SHR | |
| 1868 | SWAP4 | |
| 1869 | SWAP3 | |
| 186a | SWAP2 | |
| 186b | DUP3 | |
| 186c | ADD | |
| 186d | SWAP2 | |
| 186e | SHR | |
| 186f | DUP1 | |
| 1870 | PUSH2 | 0x17fd |
| 1873 | JUMP | |
| 1874 | JUMPDEST | |
| 1875 | DUP3 | |
| 1876 | DUP2 | |
| 1877 | SWAP7 | |
| 1878 | SWAP3 | |
| 1879 | SWAP7 | |
| 187a | EQ | |
| 187b | PUSH2 | 0x188a |
| 187e | JUMPI | |
| 187f | JUMPDEST | |
| 1880 | POP | |
| 1881 | SWAP1 | |
| 1882 | PUSH1 | 0x01 |
| 1884 | SWAP3 | |
| 1885 | SWAP2 | |
| 1886 | PUSH2 | 0x1865 |
| 1889 | JUMP | |
| 188a | JUMPDEST | |
| 188b | DUP4 | |
| 188c | SWAP6 | |
| 188d | SWAP2 | |
| 188e | SWAP6 | |
| 188f | SHL | |
| 1890 | PUSH2 | 0x1899 |
| 1893 | DUP2 | |
| 1894 | DUP7 | |
| 1895 | PUSH2 | 0x16de |
| 1898 | JUMP | |
| 1899 | JUMPDEST | |
| 189a | PUSH2 | 0x18a5 |
| 189d | PUSH2 | 0x0303 |
| 18a0 | DUP3 | |
| 18a1 | PUSH2 | 0x1c0a |
| 18a4 | JUMP | |
| 18a5 | JUMPDEST | |
| 18a6 | SWAP1 | |
| 18a7 | PUSH0 | |
| 18a8 | SWAP3 | |
| 18a9 | SWAP1 | |
| 18aa | PUSH2 | 0x18b2 |
| 18ad | DUP2 | |
| 18ae | PUSH2 | 0x1bf1 |
| 18b1 | JUMP | |
| 18b2 | JUMPDEST | |
| 18b3 | DUP1 | |
| 18b4 | JUMPDEST | |
| 18b5 | PUSH2 | 0x1918 |
| 18b8 | JUMPI | |
| 18b9 | POP | |
| 18ba | POP | |
| 18bb | POP | |
| 18bc | PUSH0 | |
| 18bd | NOT | |
| 18be | DUP3 | |
| 18bf | ADD | |
| 18c0 | SWAP2 | |
| 18c1 | DUP3 | |
| 18c2 | GT | |
| 18c3 | PUSH2 | 0x047c |
| 18c6 | JUMPI | |
| 18c7 | PUSH2 | 0x18d0 |
| 18ca | DUP3 | |
| 18cb | DUP3 | |
| 18cc | PUSH2 | 0x1797 |
| 18cf | JUMP | |
| 18d0 | JUMPDEST | |
| 18d1 | MLOAD | |
| 18d2 | SWAP2 | |
| 18d3 | DUP1 | |
| 18d4 | JUMPDEST | |
| 18d5 | PUSH2 | 0x18f8 |
| 18d8 | JUMPI | |
| 18d9 | POP | |
| 18da | POP | |
| 18db | PUSH1 | 0x01 |
| 18dd | SWAP4 | |
| 18de | SWAP3 | |
| 18df | SWAP2 | |
| 18e0 | DUP2 | |
| 18e1 | DUP6 | |
| 18e2 | SWAP3 | |
| 18e3 | POP | |
| 18e4 | PUSH2 | 0x18ed |
| 18e7 | DUP3 | |
| 18e8 | DUP12 | |
| 18e9 | PUSH2 | 0x1797 |
| 18ec | JUMP | |
| 18ed | JUMPDEST | |
| 18ee | MSTORE | |
| 18ef | ADD | |
| 18f0 | SWAP5 | |
| 18f1 | SWAP1 | |
| 18f2 | SWAP2 | |
| 18f3 | SWAP3 | |
| 18f4 | PUSH2 | 0x187f |
| 18f7 | JUMP | |
| 18f8 | JUMPDEST | |
| 18f9 | PUSH0 | |
| 18fa | NOT | |
| 18fb | ADD | |
| 18fc | SWAP2 | |
| 18fd | DUP3 | |
| 18fe | SWAP1 | |
| 18ff | PUSH2 | 0x1912 |
| 1902 | SWAP1 | |
| 1903 | PUSH2 | 0x190c |
| 1906 | DUP4 | |
| 1907 | DUP6 | |
| 1908 | PUSH2 | 0x1797 |
| 190b | JUMP | |
| 190c | JUMPDEST | |
| 190d | MLOAD | |
| 190e | PUSH2 | 0x2048 |
| 1911 | JUMP | |
| 1912 | JUMPDEST | |
| 1913 | SWAP3 | |
| 1914 | PUSH2 | 0x18d4 |