<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pharos Production</title>
    <description>The latest articles on DEV Community by Pharos Production (pharos_production).</description>
    <link>https://dev.to/pharos_production</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Forganization%2Fprofile_image%2F14317%2F945dcda9-b2d8-4e70-a174-d8b79989dff5.jpg</url>
      <title>DEV Community: Pharos Production</title>
      <link>https://dev.to/pharos_production</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pharos_production"/>
    <language>en</language>
    <item>
      <title>ERC-3643 vs ERC-1400: How to Choose a Standard for Tokenized Instruments</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Wed, 07 Oct 2026 06:16:16 +0000</pubDate>
      <link>https://dev.to/pharos_production/erc-3643-vs-erc-1400-how-to-choose-a-standard-for-tokenized-instruments-67e</link>
      <guid>https://dev.to/pharos_production/erc-3643-vs-erc-1400-how-to-choose-a-standard-for-tokenized-instruments-67e</guid>
      <description>&lt;p&gt;Choose ERC-3643 when the central requirement is that a token can move only to an eligible investor represented through an on-chain identity system. Evaluate an ERC-1400 implementation when the instrument needs explicit subdivisions of a holder's balance, partition-specific operations or the interfaces in that standards family. If the product needs both, neither label finishes the architecture: select an implementation and prove how its identity and partition policies interact.&lt;/p&gt;

&lt;p&gt;The difficult question is what a particular transaction must preserve. A wallet can hold enough tokens and still be unable to transfer the requested amount. A registered investor can lose eligibility after a transfer preview. A transfer agent can have authority to correct ownership while ordinary transfers remain paused. These are different conditions, with different owners and failure paths. In the example below, 150 units cannot fund a 120-unit transfer when only 100 are transferable.&lt;/p&gt;

&lt;p&gt;For &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;blockchain engineering at Pharos Production&lt;/a&gt;, the useful starting point is a behavior specification connecting an instrument requirement to an observable transaction outcome. Ask for that artifact before comparing feature counts. An issuer choosing a token standard needs to know which behavior is supplied, which depends on configuration and which needs new engineering.&lt;/p&gt;

&lt;p&gt;This comparison uses the published ERC-3643 specification, the original ERC-1400 family documents and two pinned public implementations. Sources were checked on October 7, 2026. It makes architecture recommendations from those interfaces and source paths; it does not report a production deployment or a comparative security audit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with the instrument, not the token number
&lt;/h2&gt;

&lt;p&gt;Write the instrument's rights in language a fund administrator and an engineer can both inspect. Does every unit carry the same economic rights? Can two units in the same wallet have different transfer restrictions? Who decides that a subscription is complete, that a holding period has expired or that ownership needs correction?&lt;/p&gt;

&lt;p&gt;Consider a fund share with one class of economic rights. Investors must be approved before receiving it. Transfers have a concentration limit. Lost-key recovery is an operational requirement. Here, investor identity and transfer controls are the primary engineering problem. ERC-3643 is a natural candidate because its interfaces directly describe the identity registries and administrative operations that this workflow needs.&lt;/p&gt;

&lt;p&gt;Now consider an instrument whose units are divided into issuance lots with different release conditions. A holder can own both transferable and restricted units. An intermediary must specify which lot it is moving and retain that information in its records. Partition-aware operations deserve a place in the evaluation. Whether those partitions represent lots, lockups or something else is an issuer-approved design decision.&lt;/p&gt;

&lt;p&gt;Different economic rights need further scrutiny. Separate share classes might require separate contracts, distinct accounting or a carefully specified partition model. The ability to assign a partition key does not establish the legal meaning of that key. Nor does a single ERC-20 balance demonstrate that all the units it aggregates are interchangeable for settlement.&lt;/p&gt;

&lt;p&gt;The selection worksheet should therefore contain four fields: the unit's rights, the state that restricts movement, the actor authorized to change that state and the evidence retained after a change. A standard is useful when its interfaces fit these requirements without hiding a material distinction in an application database.&lt;/p&gt;

&lt;h2&gt;
  
  
  Compare a final specification with a standards family
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://eips.ethereum.org/EIPS/eip-3643" rel="noopener noreferrer"&gt;ERC-3643&lt;/a&gt; is marked Final in the Ethereum Improvement Proposals repository. It defines token, identity registry, identity storage, compliance, trusted issuer registry and claim topic registry interfaces. It requires ERC-20 compatibility and an on-chain identity system. It also describes recovery, freezes, pause, minting, burning and agent authority.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/SecurityTokenStandard/EIP-Spec/blob/8b082caba8adb4f8e351aa6acdd3e9e394e09dea/eip/eip-1400.md" rel="noopener noreferrer"&gt;original ERC-1400 document&lt;/a&gt; describes an umbrella of related interfaces. Its retained front matter says Draft. One of its authors is Fabian Vogelsteller, also associated with the development of Ethereum token interfaces. The authors describe its scope in one short sentence:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Represents a library of standards for security tokens on Ethereum.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That sentence matters in procurement. ERC-1400 on a slide does not identify the exact interface set, restriction system or administrative behavior delivered by a vendor. Ask which members of the family are implemented and which extensions are proprietary.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Family document&lt;/th&gt;
&lt;th&gt;Engineering job&lt;/th&gt;
&lt;th&gt;What still needs a design decision&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ERC-1410&lt;/td&gt;
&lt;td&gt;Partition balances and partition operations&lt;/td&gt;
&lt;td&gt;Meaning of a partition and permitted transitions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ERC-1594&lt;/td&gt;
&lt;td&gt;Transfer validity queries, attached data, issuance and redemption&lt;/td&gt;
&lt;td&gt;Policy source, authorization data and failure handling&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ERC-1643&lt;/td&gt;
&lt;td&gt;Document references and updates&lt;/td&gt;
&lt;td&gt;Document governance, storage and investor access&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ERC-1644&lt;/td&gt;
&lt;td&gt;Declared controller operations&lt;/td&gt;
&lt;td&gt;Controller authorization and correction procedure&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A Final status is a specification maturity signal. It is not evidence that a deployed configuration has been audited, that an upgrade is safe or that a venue supports the token. Conversely, a draft-family implementation may have useful production-oriented features. Evaluate the repository, deployment and operating model separately from the status of the document.&lt;/p&gt;

&lt;h2&gt;
  
  
  ERC-3643 makes investor eligibility an explicit dependency
&lt;/h2&gt;

&lt;p&gt;ERC-3643 connects a receiving wallet to an identity record and checks the required claims against trusted issuers. The claim topics define what must be established; the trusted issuer registry defines whose attestations are accepted. The token's compliance component handles offering-level rules, such as concentration or holder-count restrictions, separately from identity verification.&lt;/p&gt;

&lt;p&gt;This separation helps diagnose a rejected transfer. The recipient might be unregistered, lack a required claim or fail a token-level rule despite having a valid identity. A frozen wallet or insufficient unfrozen balance is another reason to reject movement. A front end that collapses every case into an eligibility error sends the operator to the wrong system.&lt;/p&gt;

&lt;p&gt;The specification's ordinary transfer conditions include recipient verification, wallet freeze checks, free balance, pause state and the compliance result. Do not expand that into a promise that every entry point checks every condition. Administrative operations have different semantics, and the selected implementation must be inspected individually.&lt;/p&gt;

&lt;p&gt;The eligibility owner needs a response procedure as well as contract permissions. If an onboarding provider stops serving an attestation, identify who investigates, who may replace the evidence and how pending instructions are handled. A registry administrator should not make an undocumented exception simply to clear a settlement queue. Keep the rejected instruction linked to its policy evidence so that a subsequent approved retry can be explained.&lt;/p&gt;

&lt;p&gt;Identity freshness also needs an operating policy. Removing an accepted issuer, revoking an attestation or changing a claim requirement can affect future receipts. It does not necessarily remove an existing balance. Decide whether an investor who becomes ineligible may continue holding, redeem, transfer out through a restricted route or require an administrative action. Then encode and test the approved behavior.&lt;/p&gt;

&lt;p&gt;A wallet-to-identity relationship also needs a custody model. For an omnibus custodian, the contract may identify the custodian wallet while the beneficial-owner allocation remains in its ledger. That is a different visibility boundary from individually registered investor wallets. Neither model becomes correct merely because a registry call returns true.&lt;/p&gt;

&lt;p&gt;Avoid putting sensitive onboarding documents into public transaction data to simplify integration. The token system needs verifiable eligibility evidence and controlled links to the off-chain record. The location of personal information, access permissions and correction process should be explicit parts of the identity design.&lt;/p&gt;

&lt;h2&gt;
  
  
  ERC-1400 partitions expose which balance is moving
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/SecurityTokenStandard/EIP-Spec/blob/8b082caba8adb4f8e351aa6acdd3e9e394e09dea/eip/eip-1410.md" rel="noopener noreferrer"&gt;ERC-1410&lt;/a&gt; describes balances grouped under partition keys while retaining aggregate owner balances and supply information. Operations can address a particular partition. This gives an integration a way to distinguish parts of a holding that would disappear into a single aggregate balance.&lt;/p&gt;

&lt;p&gt;A partition is a label and associated balance in the interface. Its restrictions come from the contract's policy. Naming a partition LOCKED does not itself create a time lock. A partition named AVAILABLE does not prove that the recipient is eligible. The implementation must enforce the corresponding conditions through its transfer path and extensions.&lt;/p&gt;

&lt;p&gt;The pinned &lt;a href="https://github.com/Consensys/UniversalToken/tree/54320c6f7a8ee1fd7fcb19073e9c122e1e8f96f9" rel="noopener noreferrer"&gt;Consensys UniversalToken implementation&lt;/a&gt; exposes partition transfers and operator transfers. Its transfer path invokes extensions and maintains partition balances. Its README discusses both signed transfer certificates and on-chain lists of validated investors. These are concrete policy mechanisms in that implementation, not proof that every ERC-1400 token uses the same identity architecture.&lt;/p&gt;

&lt;p&gt;A certificate-based design can attach authorization to an intended transaction. It introduces an issuer or policy service that must remain available and issue the correct authorization. A list-based design moves an eligibility lookup on-chain but needs transactions to keep that state current. An engineering choice between them must account for revocation latency, replay protection and the integration's ability to supply the required data.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl63emu5ro1vc5c8nezkz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl63emu5ro1vc5c8nezkz.png" alt="ERC-3643 organizes eligibility through identity and compliance dependencies; an ERC-1400 implementation can organize balances by partition and enforce policy through transfer extensions. Both require an explicit authority model." width="800" height="560"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Partition behavior also reaches the user interface. A wallet displaying 150 units should distinguish transferable units from restricted units when that difference matters. Custody reports need the partition balances, and a settlement instruction needs an unambiguous source partition. Otherwise the application promises liquidity using an aggregate number that the transfer path cannot spend.&lt;/p&gt;

&lt;h2&gt;
  
  
  Use disqualifiers before assigning scores
&lt;/h2&gt;

&lt;p&gt;A weighted feature score can hide a missing requirement. A candidate with a polished dashboard might score well while lacking the operation an approved instrument needs. Set the rejection conditions first, then compare engineering and operational costs among the remaining candidates.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;ERC-3643 candidate&lt;/th&gt;
&lt;th&gt;ERC-1400 candidate&lt;/th&gt;
&lt;th&gt;Reject the candidate when&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reusable investor eligibility&lt;/td&gt;
&lt;td&gt;Explicit identity and claim registries&lt;/td&gt;
&lt;td&gt;Inspect the chosen list, certificate or identity extension&lt;/td&gt;
&lt;td&gt;Eligibility updates cannot be explained or tested&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Different restrictions inside one holding&lt;/td&gt;
&lt;td&gt;Additional lot or state model may be needed&lt;/td&gt;
&lt;td&gt;Evaluate partition-aware operations&lt;/td&gt;
&lt;td&gt;Required distinctions exist only in a display label&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Explain a refused transfer&lt;/td&gt;
&lt;td&gt;Combine identity, compliance and token-state diagnostics&lt;/td&gt;
&lt;td&gt;Inspect validity queries and implementation reason codes&lt;/td&gt;
&lt;td&gt;Preview contradicts execution without a diagnosable change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Administrative correction&lt;/td&gt;
&lt;td&gt;Agent and recovery operations are specified&lt;/td&gt;
&lt;td&gt;Inspect controller and operator capabilities&lt;/td&gt;
&lt;td&gt;Authority or bypass behavior is undocumented&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Documents tied to an instrument&lt;/td&gt;
&lt;td&gt;Define the document integration separately&lt;/td&gt;
&lt;td&gt;Evaluate the ERC-1643 interface and its implementation&lt;/td&gt;
&lt;td&gt;Updated terms cannot be retrieved and reconciled&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Existing custody or venue integration&lt;/td&gt;
&lt;td&gt;Prove restricted ERC-20 operations work&lt;/td&gt;
&lt;td&gt;Prove aggregate and partition operations work&lt;/td&gt;
&lt;td&gt;Required calls or events are unsupported&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upgrades and policy changes&lt;/td&gt;
&lt;td&gt;Inspect registries, modules and deployment authority&lt;/td&gt;
&lt;td&gt;Inspect extensions, partition semantics and deployment authority&lt;/td&gt;
&lt;td&gt;A change can silently reinterpret outstanding holdings&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Pharos Production's &lt;a href="https://pharosproduction.com/services/tokenization-and-rwa-development/" rel="noopener noreferrer"&gt;tokenization and RWA development process&lt;/a&gt; describes working alongside legal counsel to map asset ownership rights to token mechanics. That mapping addresses the central selection problem: engineering needs approved rights and restrictions before choosing a contract model. Ask for a requirement-to-operation matrix as the handoff, with legal assumptions distinguished from demonstrated software behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walk one holding through both models
&lt;/h2&gt;

&lt;p&gt;Use an illustrative holding of 150 units: 100 currently transferable and 50 restricted. The holder requests a transfer of 120 units to an eligible recipient. Assume the token is not paused and both wallets satisfy the applicable conditions. These numbers are a design example, not measurements from a deployed system.&lt;/p&gt;

&lt;p&gt;An ERC-3643 design could represent the restriction as 50 partially frozen tokens. The ordinary free balance would be 100, so the request should fail even though the aggregate balance is 150. Whether partial freezing is the right representation depends on the reason for the restriction and who may release it. Freezing is an administrative mechanism; it does not automatically implement the instrument's contractual release schedule.&lt;/p&gt;

&lt;p&gt;An ERC-1400 design could represent 100 units in an available partition and 50 in a restricted partition. A request for 120 from the available partition should fail because that partition contains only 100. If the aggregate ERC-20 transfer route can consume multiple partitions, its default selection order and restriction checks must be part of the design. Do not assume the aggregate route behaves like the partition-specific route.&lt;/p&gt;

&lt;p&gt;Reduce the request to 40. After a successful transfer that preserves the partition, the sender would have 60 available and 50 restricted; the recipient would receive 40 available. In the partial-freeze model, the sender would have 110 total and 50 frozen, leaving 60 free. Those arithmetic outcomes look similar, but the retained information differs: partition identifiers can carry lot distinctions that one frozen subtotal cannot express by itself.&lt;/p&gt;

&lt;p&gt;Now revoke the recipient's eligibility after preview but before execution. Execution must use the applicable current policy; a successful earlier query is not a reservation of eligibility. If the transaction is rejected, balances and associated accounting must remain consistent. If the approved model permits the movement, the record needs to explain which policy and authorization allowed it.&lt;/p&gt;

&lt;p&gt;Finally, let the restriction expire. In the freeze model, determine how the frozen quantity is released. In the partition model, determine whether units remain in the same partition with a changed rule or move to another partition. A wall-clock date in a database cannot substitute for an on-chain transition when the contract still blocks the transfer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preflight is a snapshot, not a settlement promise
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://github.com/SecurityTokenStandard/EIP-Spec/blob/8b082caba8adb4f8e351aa6acdd3e9e394e09dea/eip/eip-1594.md" rel="noopener noreferrer"&gt;ERC-1594&lt;/a&gt; describes transfer validity queries with error signalling and attached data. Use the exact return values of the implemented interface to build an actionable response. A recipient rejection should lead to an eligibility investigation; a balance rejection should lead to a position check. A failed authorization certificate requires a different operator than either of those.&lt;/p&gt;

&lt;p&gt;For ERC-3643, inspecting the compliance result alone leaves out other token conditions. The application needs the relevant identity, freeze, pause and balance checks, with a clear warning when its diagnostic view is incomplete. A callback or external dependency can also fail during execution even if an earlier policy query succeeded.&lt;/p&gt;

&lt;p&gt;Store the block identifier used for preview and the instruction parameters. If execution fails later, compare the state and policy changes between the preview and execution blocks. Recomputing against the latest state without preserving the original observation can conceal why the user saw an apparently valid instruction. The user-facing result should name the rejected operation and the next permitted action, without promising that retrying will succeed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Administrative paths need their own contract
&lt;/h2&gt;

&lt;p&gt;Forced transfers, wallet recovery and redemption are part of the operating model. They must have an authority policy that explains who can invoke them and what ordinary restrictions they bypass. A multisig threshold alone cannot explain whether its signers are allowed to perform a particular correction.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/TokenySolutions/T-REX/blob/f1390127d4270e7f31764c068caf0a1526b852d4/contracts/token/Token.sol" rel="noopener noreferrer"&gt;pinned T-REX token source&lt;/a&gt; illustrates why path-specific review matters. Its forced transfer requires agent authority and verifies the recipient. It can release enough frozen tokens to complete the movement and does not call the ordinary compliance pre-check. It does call the post-transfer compliance callback. Those details matter to a module that maintains holder counts or position limits.&lt;/p&gt;

&lt;p&gt;The same source's mint path checks recipient verification and the compliance pre-check. This differs from the published specification's explanatory description of mint bypassing compliance rules. Treat the pinned code as the behavior being evaluated and record the discrepancy. A standards summary is not a substitute for examining the deployed entry point.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/SecurityTokenStandard/EIP-Spec/blob/8b082caba8adb4f8e351aa6acdd3e9e394e09dea/eip/eip-1644.md" rel="noopener noreferrer"&gt;ERC-1644&lt;/a&gt; describes controller operations and transparency about whether unilateral transfers are possible. A candidate implementation must still demonstrate its exact controller configuration, events and interaction with extensions. Never infer that a controller has the same permissions as an ERC-3643 agent.&lt;/p&gt;

&lt;p&gt;For each exceptional path, retain the business authorization, transaction receipt and before-and-after ownership state. A correction transfers ownership in the current ledger; it does not erase the historical transaction. Recovery should preserve the instrument's supply and relevant restrictions while making the old wallet's future role explicit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Integration includes settlement and document state
&lt;/h2&gt;

&lt;p&gt;ERC-20 compatibility provides familiar calls such as balance and allowance queries. It does not mean that an arbitrary exchange, bridge or escrow contract is an eligible holder. A settlement contract can satisfy the ABI and still fail a recipient eligibility check. Partition-aware integrations additionally need to preserve the intended partition through the complete settlement route.&lt;/p&gt;

&lt;p&gt;Test with the actual custody and venue workflow. Does the intermediary support attached transfer data? Can it authorize an operator? Does it display a failure reason to the correct actor? Can its indexer distinguish an ordinary transfer from an administrative movement and reconstruct the relevant balances after a restart?&lt;/p&gt;

&lt;p&gt;Delivery versus payment adds a second asset and a settlement boundary. Token eligibility is one condition; cash availability and atomic execution are separate conditions. If policy changes between instruction creation and settlement, the system needs a defined rejection or cancellation result. A selected security-token standard does not make those conditions atomic by itself.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/SecurityTokenStandard/EIP-Spec/blob/8b082caba8adb4f8e351aa6acdd3e9e394e09dea/eip/eip-1643.md" rel="noopener noreferrer"&gt;ERC-1643&lt;/a&gt; supplies document references and update events. A URI and content hash can help connect a contract to a particular document. They do not establish that every investor received the document or accepted amended terms. Retain the availability and acknowledgement process separately, with versioned mappings to instrument state.&lt;/p&gt;

&lt;p&gt;Redemption also crosses systems. Burning tokens may reduce on-chain supply before an off-chain payment completes. Define whether the workflow escrows units, burns after payment or uses another approved state machine. Reconcile outstanding claims and instrument supply against the administrator's records rather than treating a burn event as proof that cash was delivered.&lt;/p&gt;

&lt;h2&gt;
  
  
  Require a reproducible selection record
&lt;/h2&gt;

&lt;p&gt;Request the same acceptance cases from each candidate, scoped to the approved instrument. Successful transfers alone are weak evidence. Rejected and exceptional operations reveal whether restrictions remain intact when actors or state change.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Acceptance case&lt;/th&gt;
&lt;th&gt;Evidence to retain&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recipient eligibility changes after preview&lt;/td&gt;
&lt;td&gt;Policy change, execution result and unchanged balances on rejection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Aggregate balance exceeds spendable balance&lt;/td&gt;
&lt;td&gt;Partition or frozen-state snapshot and rejected request&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operator authorization is removed&lt;/td&gt;
&lt;td&gt;Revocation transaction and subsequent unauthorized-call rejection&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Administrative correction while ordinary transfers are paused&lt;/td&gt;
&lt;td&gt;Approved bypass policy, exact path and resulting events&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Required attestation issuer is removed&lt;/td&gt;
&lt;td&gt;Registry state and expected receipt behavior&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Redemption fails in the payment system&lt;/td&gt;
&lt;td&gt;Token state, cash state and reconciliation outcome&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Policy module or extension changes&lt;/td&gt;
&lt;td&gt;Old and new configuration plus affected-case results&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are proposed acceptance cases, not a claim that this article executed a conformance suite. Each implementation should provide its own reproducible fixtures, transaction inputs and expected outcomes. Tests using identity stubs need to say which signature, expiry or revocation behavior they do not cover.&lt;/p&gt;

&lt;p&gt;Before release, bind the results to the source commit, compiler configuration, dependency versions and deployed addresses. Include the active identity registries or extensions, privileged-role holders and implementation or proxy relationships. Pinning source makes the comparison reproducible; it does not identify an audited release without matching audit evidence.&lt;/p&gt;

&lt;p&gt;The same record needs an operational owner. Name who maintains the investor registry, reconciles settlement exceptions and reviews privileged changes. For certificates, include signer rotation and incident handling. For partitions, include the dictionary of keys and the procedure for introducing a new one. If these jobs belong to different organizations, make their handoffs visible in the acceptance evidence rather than assigning them implicitly to the token developer.&lt;/p&gt;

&lt;p&gt;An upgrade needs a before-and-after ownership check that includes restrictions. Equal total supply can hide a lost freeze, a reinterpreted partition or an eligibility registry pointing at the wrong implementation. Rehearse ordinary transfers and exceptional paths against representative outstanding holdings, then reconcile the resulting ownership records.&lt;/p&gt;

&lt;p&gt;Record the choice in one sentence with a falsifiable reason. For example: select an ERC-3643 implementation because the instrument has one fungible share class and requires claim-based recipient eligibility plus wallet recovery. Or select the evaluated ERC-1400 implementation because issuance lots must remain explicit through partition-aware custody and settlement. Attach the acceptance evidence that makes the reason true.&lt;/p&gt;

&lt;p&gt;When both identities and partitions are essential, evaluate their composition as a separate design obligation. Specify where eligibility is enforced, how restrictions survive partition transitions and which authority may change either rule. The next engineering deliverable is the behavior matrix and reproducible transaction evidence for that exact composition.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bulbapp.io/p/e2580ef7-324b-442d-b818-92b8c8442b3d/five-smart-contract-upgrade-patterns-compared" rel="noopener noreferrer"&gt;Five Smart Contract Upgrade Patterns Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-2-40942c055d79" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-1-222fe44ee14c" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.medium.com/upgradeable-solidity-smart-contracts-part-1-versioning-7e6e97cafc28" rel="noopener noreferrer"&gt;Upgradeable Solidity Smart Contracts. Part 1 — Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/web3-smart-contracts-oracles-part-1-3905b127c01d" rel="noopener noreferrer"&gt;Web3. Smart Contracts. Oracles. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foclejrac8sjqi79uy7hm.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Foclejrac8sjqi79uy7hm.jpg" width="112" height="112" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>architecture</category>
    </item>
    <item>
      <title>How to Implement ERC-3643 Transfer Controls for Permissioned Tokens</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Wed, 30 Sep 2026 07:46:18 +0000</pubDate>
      <link>https://dev.to/pharos_production/how-to-implement-erc-3643-transfer-controls-for-permissioned-tokens-2e40</link>
      <guid>https://dev.to/pharos_production/how-to-implement-erc-3643-transfer-controls-for-permissioned-tokens-2e40</guid>
      <description>&lt;p&gt;A permissioned token needs a testable answer to a simple question: why did this movement of tokens succeed? A successful wallet transfer proves little if an agent operation follows different rules, a registry change removes an eligibility requirement or a frontend mistakes a compliance check for complete transaction validation.&lt;/p&gt;

&lt;p&gt;This walkthrough implements a recipient-country rule against the actual ERC-3643 contract suite and exercises it with 19 passing tests. It also records a deliberately broken version of the module, the failing regression and the correction. The test fixture uses real token, registry and compliance contracts.&lt;/p&gt;

&lt;p&gt;Identity signatures are replaced with explicit test doubles, so the result establishes transfer-control behavior within that boundary.&lt;/p&gt;

&lt;p&gt;The distinction between documentation and executable behavior matters here. The ERC-3643 specification describes minting as bypassing compliance rules. The pinned implementation used below calls &lt;code&gt;canTransfer&lt;/code&gt; during minting. A release review must resolve that difference for its selected contracts before anyone signs an issuance transaction.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with a policy-to-operation map
&lt;/h2&gt;

&lt;p&gt;Write the intended rule before choosing a module. In this example, an ordinary transfer requires an eligible recipient whose registered country appears in an allowlist. Wallet freezes, available balance and the token's pause state remain separate token-level checks. Country values represent registry records maintained by authorized operators; they do not reveal a wallet's physical location.&lt;/p&gt;

&lt;p&gt;The implementation question in &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;blockchain engineering work at Pharos Production&lt;/a&gt; is how a requirement becomes an observable contract behavior. For this walkthrough, the useful deliverable is a table connecting each rule to a transaction and an assertion. A statement that the token supports compliance cannot tell an integration engineer which call should revert.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Requirement&lt;/th&gt;
&lt;th&gt;Enforcement point&lt;/th&gt;
&lt;th&gt;Observable test&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Recipient has the required trusted claim&lt;/td&gt;
&lt;td&gt;Identity registry&lt;/td&gt;
&lt;td&gt;Missing claim or removed issuer rejects transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recipient country is permitted&lt;/td&gt;
&lt;td&gt;Custom compliance module&lt;/td&gt;
&lt;td&gt;Registered recipient in a denied country cannot receive an ordinary transfer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Token is available for ordinary movement&lt;/td&gt;
&lt;td&gt;Token pause modifier&lt;/td&gt;
&lt;td&gt;Paused transfer fails even when &lt;code&gt;canTransfer&lt;/code&gt; returns true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wallet and balance restrictions apply&lt;/td&gt;
&lt;td&gt;Token freeze checks&lt;/td&gt;
&lt;td&gt;Frozen wallet fails; partially frozen balance retains its boundary&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Delegated spending stays bounded&lt;/td&gt;
&lt;td&gt;Token allowance accounting&lt;/td&gt;
&lt;td&gt;Successful spending reduces allowance; rejected transfer preserves it&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Privileged movement has explicit authority&lt;/td&gt;
&lt;td&gt;Token agent role&lt;/td&gt;
&lt;td&gt;Unauthorized forced transfer fails; authorized exceptional path is tested separately&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Decide separately what should happen when an investor's country changes while they already own tokens. This module restricts recipients. It does not automatically confiscate an existing holding, prevent every outbound movement or calculate ownership concentration. Those are different policy decisions and require their own enforcement points and tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin the contracts before interpreting the standard
&lt;/h2&gt;

&lt;p&gt;The code below targets &lt;a href="https://github.com/ERC-3643/ERC-3643/tree/2f0704dee9658ad9cd5b6ed82e7427da74c28345" rel="noopener noreferrer"&gt;ERC-3643/ERC-3643 at commit &lt;code&gt;2f0704dee9658ad9cd5b6ed82e7427da74c28345&lt;/code&gt;&lt;/a&gt;. Its package identifies version 4.1.3. The compiler is Solidity 0.8.17, with OpenZeppelin contracts and upgradeable contracts pinned to 4.8.3 and the ONCHAINID Solidity package pinned to 2.0.0. This is a reproducible source selection, not a recommendation to deploy an unreviewed repository head.&lt;/p&gt;

&lt;p&gt;Read the selected entry points directly. In this version, &lt;code&gt;transfer&lt;/code&gt; and &lt;code&gt;transferFrom&lt;/code&gt; combine token checks with recipient verification and modular compliance. Do not describe that as universal verification of both participants. If your policy requires renewed sender eligibility on every ordinary movement, add and test that requirement explicitly rather than assuming the recipient check covers it.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://eips.ethereum.org/EIPS/eip-3643#transfer" rel="noopener noreferrer"&gt;standard's transfer description&lt;/a&gt; groups minting with forced transfers as paths that bypass compliance. However, the pinned &lt;a href="https://github.com/ERC-3643/ERC-3643/blob/2f0704dee9658ad9cd5b6ed82e7427da74c28345/contracts/token/Token.sol" rel="noopener noreferrer"&gt;Token implementation&lt;/a&gt; checks &lt;code&gt;canTransfer(address(0), recipient, amount)&lt;/code&gt; inside &lt;code&gt;mint&lt;/code&gt;. Our mint-denial test captures this behavior. Preserve that test when upgrading the dependency; a changed result needs an explicit issuance-policy decision.&lt;/p&gt;

&lt;p&gt;Review inherited behavior and callbacks too. An entry point can omit an ordinary permission check while still reverting in a downstream module action. An interface name alone cannot establish that an operation will succeed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make identity requirements explicit
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://docs.erc3643.org/erc-3643/smart-contracts-library/onchain-identities/identity-registry" rel="noopener noreferrer"&gt;identity registry documentation&lt;/a&gt; separates a wallet's registration from its verification. Registration associates the wallet with an identity and a country. Verification evaluates the required claim topics against the trusted issuer configuration. Keep those responsibilities distinct in onboarding screens and operational runbooks.&lt;/p&gt;

&lt;p&gt;A configuration edge case deserves its own regression. In the pinned &lt;a href="https://github.com/ERC-3643/ERC-3643/blob/2f0704dee9658ad9cd5b6ed82e7427da74c28345/contracts/registry/implementation/IdentityRegistry.sol" rel="noopener noreferrer"&gt;IdentityRegistry implementation&lt;/a&gt;, a registered identity passes verification when the required-topic list is empty. Our fixture first removes a claim and observes rejection. It then removes the sole required topic and observes acceptance. That is configuration behavior, not evidence of a newly discovered vulnerability.&lt;/p&gt;

&lt;p&gt;Consequently, checking that every wallet has an identity contract is insufficient for a deployment that requires a particular credential. The release receipt should record the required topics, trusted issuers and each issuer's permitted topics. A reviewer should be able to explain why an empty list is acceptable or show that the deployment rejects that configuration before activation.&lt;/p&gt;

&lt;p&gt;Topic &lt;code&gt;7&lt;/code&gt; is a synthetic identifier in this fixture, not a universal KYC topic. Its issuer stub always accepts the supplied claim, and its identity stub exposes unrestricted setters for test setup. Neither stub belongs in a production deployment. Real signature validation, claim expiry and issuer-specific revocation behavior require a separate integration suite using the chosen identity implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Implement one recipient-country module
&lt;/h2&gt;

&lt;p&gt;Keep this first rule narrow enough to inspect. A mapping answers whether a particular compliance contract permits a particular country. The outer address key matters because a module can serve more than one compliance context. Without that separation, one deployment's policy update could unintentionally alter another deployment's behavior.&lt;/p&gt;

&lt;p&gt;The module reads the token bound to the supplied compliance address, then reads the recipient's country through that token's identity registry. The allowlist defaults to rejection. Country zero cannot be enabled through the setter, making the example's requirement for an explicitly configured country visible in code. The fixture uses &lt;code&gt;250&lt;/code&gt; and &lt;code&gt;276&lt;/code&gt; as two distinct country values; neither value represents a policy recommendation.&lt;/p&gt;

&lt;p&gt;Save the following as &lt;code&gt;src/RecipientCountryModule.sol&lt;/code&gt;. It extends the pinned &lt;a href="https://github.com/ERC-3643/ERC-3643/blob/2f0704dee9658ad9cd5b6ed82e7427da74c28345/contracts/compliance/modular/modules/AbstractModule.sol" rel="noopener noreferrer"&gt;AbstractModule&lt;/a&gt;. Configuration requires a call from a bound compliance contract. The check also rejects an unbound compliance context. The transfer, mint and burn action hooks remain access-controlled even though this stateless rule has nothing to update.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.17;

import "erc3643/compliance/modular/modules/AbstractModule.sol";
import "erc3643/compliance/modular/IModularCompliance.sol";
import "erc3643/token/IToken.sol";

contract RecipientCountryModule is AbstractModule {
    mapping(address =&amp;gt; mapping(uint16 =&amp;gt; bool)) public allowed;
    event CountryPermissionSet(address indexed compliance, uint16 country, bool enabled);

    function setCountry(uint16 country, bool enabled) external onlyComplianceCall {
        require(country != 0, "country must be explicit");
        allowed[msg.sender][country] = enabled;
        emit CountryPermissionSet(msg.sender, country, enabled);
    }

    function moduleCheck(address, address to, uint256, address compliance)
        external view override onlyBoundCompliance(compliance) returns (bool)
    {
        IToken token = IToken(IModularCompliance(compliance).getTokenBound());
        uint16 country = token.identityRegistry().investorCountry(to);
        return allowed[compliance][country];
    }

    function moduleTransferAction(address, address, uint256)
        external override onlyComplianceCall {}
    function moduleMintAction(address, uint256)
        external override onlyComplianceCall {}
    function moduleBurnAction(address, uint256)
        external override onlyComplianceCall {}
    function canComplianceBind(address) external pure override returns (bool) { return true; }
    function isPlugAndPlay() external pure override returns (bool) { return true; }
    function name() external pure override returns (string memory) { return "RecipientCountryModule"; }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A rule based only on recipient country does not need a transfer counter. If you extend it to maximum holders or rolling volume limits, the action hooks become part of correctness. Define how minting, burning and exceptional transfers update that state. Test a revert in a later callback and prove that earlier changes roll back with the transaction. This article's empty hooks cannot establish that property for a future stateful module.&lt;/p&gt;

&lt;h2&gt;
  
  
  Route configuration through the compliance owner
&lt;/h2&gt;

&lt;p&gt;An operator should not call &lt;code&gt;setCountry&lt;/code&gt; directly on the module. The configured compliance owner calls &lt;code&gt;callModuleFunction&lt;/code&gt;, which invokes the module so that its &lt;code&gt;msg.sender&lt;/code&gt; is the bound compliance contract. This arrangement gives the module the correct policy namespace while preserving the compliance contract's ownership check.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://github.com/ERC-3643/ERC-3643/blob/2f0704dee9658ad9cd5b6ed82e7427da74c28345/contracts/compliance/modular/ModularCompliance.sol" rel="noopener noreferrer"&gt;ModularCompliance implementation&lt;/a&gt; evaluates its bound modules in &lt;code&gt;canTransfer&lt;/code&gt;. With no modules attached, there is no module-level restriction to reject the transfer. Therefore, a deployment check should assert the intended module set and configured values. A green transaction against an accidentally empty set proves the wrong policy.&lt;/p&gt;

&lt;p&gt;Permissioned issuance requires agreement between investor restrictions and executable rules. Pharos Production describes that mapping, including work with the client's legal team, in its &lt;a href="https://pharosproduction.com/services/tokenization-and-rwa-development/" rel="noopener noreferrer"&gt;tokenization and RWA development process&lt;/a&gt;. The engineering handoff should carry the approved policy into explicit module settings and transaction tests. A country allowlist by itself cannot establish that an instrument or offering meets applicable legal requirements.&lt;/p&gt;

&lt;p&gt;Prepare the configuration receipt before unpausing. Record the token address, registry address, compliance address and module address alongside the owner and agent assignments. Include the transaction that bound the module and the transactions that set permitted countries. Compare the resulting onchain values with the intended configuration, rather than accepting a deployment script's successful exit status as sufficient evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a reproducible local fixture
&lt;/h2&gt;

&lt;p&gt;Use a fresh directory with Git, Node.js, npm and Foundry already installed. The following commands fetch the selected source revision and the pinned dependency versions. Preserve the resulting package lockfile with your test evidence. Review the repository's GPL-3.0 licensing and the licenses of imported dependencies before redistributing or combining the code.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; erc3643-controls/src erc3643-controls/test erc3643-controls/vendor
&lt;span class="nb"&gt;cd &lt;/span&gt;erc3643-controls
git clone https://github.com/ERC-3643/ERC-3643.git vendor/erc3643
git &lt;span class="nt"&gt;-C&lt;/span&gt; vendor/erc3643 checkout 2f0704dee9658ad9cd5b6ed82e7427da74c28345
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--save-exact&lt;/span&gt; &lt;span class="nt"&gt;--ignore-scripts&lt;/span&gt; &lt;span class="nt"&gt;--no-audit&lt;/span&gt; &lt;span class="nt"&gt;--no-fund&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  @openzeppelin/contracts@4.8.3 &lt;span class="se"&gt;\&lt;/span&gt;
  @openzeppelin/contracts-upgradeable@4.8.3 &lt;span class="se"&gt;\&lt;/span&gt;
  @onchain-id/solidity@2.0.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save this configuration as &lt;code&gt;foundry.toml&lt;/code&gt;. The remappings are necessary because the tutorial imports the actual suite rather than recreating token behavior in a simplified mock. The suite's source files remain under &lt;code&gt;vendor/erc3643&lt;/code&gt;; the tutorial's module and tests stay in separate directories.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[profile.default]&lt;/span&gt;
&lt;span class="py"&gt;src&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"src"&lt;/span&gt;
&lt;span class="py"&gt;test&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"test"&lt;/span&gt;
&lt;span class="py"&gt;libs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"vendor"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"node_modules"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;solc_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.8.17"&lt;/span&gt;
&lt;span class="py"&gt;optimizer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;optimizer_runs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="py"&gt;remappings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;["erc3643/&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;vendor/erc&lt;/span&gt;&lt;span class="mi"&gt;3643&lt;/span&gt;&lt;span class="err"&gt;/contracts/&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="py"&gt;@openzeppelin/&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;node_modules/@openzeppelin/&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="py"&gt;@onchain-id/&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;node_modules/@onchain-id/&lt;/span&gt;&lt;span class="s"&gt;"]&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Below, the complete fixture deploys the actual registry storage, claim-topic registry, trusted-issuer registry, identity registry, modular compliance and token. It initializes each instance, binds registry storage and assigns the agent permissions required by the test. The token receives registry-agent authority for the recovery scenario. Alice and Bob start with registered identities in the permitted country.&lt;/p&gt;

&lt;p&gt;Initialization mints 100 indivisible fixture units to Alice and then unpauses ordinary transfers. Using zero decimals makes the balance assertions easy to inspect; production assets need their own denomination and rounding tests. These are directly initialized local contract instances. The fixture does not deploy the suite's proxy factory or validate an implementation-authority upgrade.&lt;/p&gt;

&lt;p&gt;Save the complete file as &lt;code&gt;test/TransferControls.t.sol&lt;/code&gt;. Every test starts from the same fresh setup, so a policy change in one test cannot make another test pass accidentally.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: GPL-3.0
pragma solidity 0.8.17;

import "erc3643/token/Token.sol";
import "erc3643/compliance/modular/ModularCompliance.sol";
import "erc3643/registry/implementation/IdentityRegistry.sol";
import "erc3643/registry/implementation/IdentityRegistryStorage.sol";
import "erc3643/registry/implementation/TrustedIssuersRegistry.sol";
import "erc3643/registry/implementation/ClaimTopicsRegistry.sol";
import "../src/RecipientCountryModule.sol";

interface Vm {
    function prank(address) external;
    function expectRevert() external;
    function expectRevert(bytes calldata) external;
}

// Test doubles: no signature verification, expiry, revocation list or real KYC.
contract IssuerStub {
    function isClaimValid(IIdentity, uint256, bytes calldata, bytes calldata)
        external pure returns (bool) { return true; }
}
contract IdentityStub {
    address public issuer;
    bool public present = true;
    mapping(bytes32 =&amp;gt; bool) public management;
    constructor(address source, address wallet) {
        issuer = source;
        management[keccak256(abi.encode(wallet))] = true;
    }
    function setPresent(bool value) external { present = value; }
    function allowRecovery(address wallet) external {
        management[keccak256(abi.encode(wallet))] = true;
    }
    function keyHasPurpose(bytes32 key, uint256 purpose) external view returns (bool) {
        return purpose == 1 &amp;amp;&amp;amp; management[key];
    }
    function getClaim(bytes32 id) external view
        returns (uint256, uint256, address, bytes memory, bytes memory, string memory)
    {
        if (present &amp;amp;&amp;amp; id == keccak256(abi.encode(issuer, uint256(7))))
            return (7, 1, issuer, hex"01", hex"02", "");
        return (0, 0, address(0), "", "", "");
    }
}

contract TransferControlsTest {
    Vm constant vm = Vm(address(uint160(uint256(keccak256("hevm cheat code")))));
    address constant ALICE = address(0xA11CE);
    address constant BOB = address(0xB0B);
    address constant SPENDER = address(0xCA11);
    address constant NEW_WALLET = address(0xBEEF);
    Token token;
    ModularCompliance compliance;
    RecipientCountryModule policy;
    IdentityRegistry registry;
    ClaimTopicsRegistry topics;
    TrustedIssuersRegistry issuers;
    IssuerStub issuer;
    IdentityStub aliceIdentity;
    IdentityStub bobIdentity;

    function setUp() public {
        topics = new ClaimTopicsRegistry(); topics.init(); topics.addClaimTopic(7);
        issuers = new TrustedIssuersRegistry(); issuers.init();
        issuer = new IssuerStub();
        uint256[] memory required = new uint256[](1); required[0] = 7;
        issuers.addTrustedIssuer(IClaimIssuer(address(issuer)), required);
        IdentityRegistryStorage store = new IdentityRegistryStorage(); store.init();
        registry = new IdentityRegistry();
        registry.init(address(issuers), address(topics), address(store));
        store.bindIdentityRegistry(address(registry)); registry.addAgent(address(this));
        aliceIdentity = new IdentityStub(address(issuer), ALICE);
        bobIdentity = new IdentityStub(address(issuer), BOB);
        registry.registerIdentity(ALICE, IIdentity(address(aliceIdentity)), 250);
        registry.registerIdentity(BOB, IIdentity(address(bobIdentity)), 250);
        compliance = new ModularCompliance(); compliance.init();
        token = new Token();
        token.init(address(registry), address(compliance), "ControlFixture", "CFX", 0, address(0));
        token.addAgent(address(this)); registry.addAgent(address(token));
        policy = new RecipientCountryModule(); compliance.addModule(address(policy));
        setCountry(250, true);
        token.mint(ALICE, 100); token.unpause();
    }
    function setCountry(uint16 country, bool enabled) internal {
        compliance.callModuleFunction(
            abi.encodeCall(RecipientCountryModule.setCountry, (country, enabled)), address(policy));
    }
    function send(uint256 amount) internal {
        vm.prank(ALICE); token.transfer(BOB, amount);
    }
    function deny() internal {
        vm.expectRevert(bytes("Transfer not possible")); send(10);
        require(token.balanceOf(ALICE) == 100 &amp;amp;&amp;amp; token.balanceOf(BOB) == 0, "denial mutated balances");
    }
    function testAllowedTransfer() public {
        send(10); require(token.balanceOf(BOB) == 10 &amp;amp;&amp;amp; token.balanceOf(ALICE) == 90);
    }
    function testDeniedCountry() public { registry.updateCountry(BOB, 276); deny(); }
    function testMissingClaim() public { bobIdentity.setPresent(false); deny(); }
    function testIssuerRemoved() public { issuers.removeTrustedIssuer(IClaimIssuer(address(issuer))); deny(); }
    function testPauseIsOutsideCanTransfer() public {
        token.pause(); require(compliance.canTransfer(ALICE, BOB, 10));
        vm.expectRevert(bytes("Pausable: paused")); send(10);
    }
    function testFrozenSender() public {
        token.setAddressFrozen(ALICE, true); vm.expectRevert(bytes("wallet is frozen")); send(10);
    }
    function testFrozenRecipient() public {
        token.setAddressFrozen(BOB, true); vm.expectRevert(bytes("wallet is frozen")); send(10);
    }
    function testPartialFreezeBoundary() public {
        token.freezePartialTokens(ALICE, 85);
        vm.expectRevert(bytes("Insufficient Balance")); send(16);
        send(15); require(token.balanceOf(ALICE) == 85);
    }
    function testTransferFromAllowance() public {
        vm.prank(ALICE); token.approve(SPENDER, 20);
        vm.prank(SPENDER); token.transferFrom(ALICE, BOB, 10);
        require(token.allowance(ALICE, SPENDER) == 10 &amp;amp;&amp;amp; token.balanceOf(BOB) == 10);
    }
    function testTransferFromDeniedCountry() public {
        registry.updateCountry(BOB, 276);
        vm.prank(ALICE); token.approve(SPENDER, 20);
        vm.expectRevert(bytes("Transfer not possible")); vm.prank(SPENDER);
        token.transferFrom(ALICE, BOB, 10);
        require(token.allowance(ALICE, SPENDER) == 20 &amp;amp;&amp;amp; token.balanceOf(BOB) == 0);
    }
    function testDirectModuleConfigurationDenied() public {
        vm.expectRevert(bytes("only bound compliance can call")); policy.setCountry(276, true);
    }
    function testNonOwnerConfigurationDenied() public {
        vm.expectRevert(bytes("Ownable: caller is not the owner")); vm.prank(BOB);
        compliance.callModuleFunction(abi.encodeCall(RecipientCountryModule.setCountry, (276, true)), address(policy));
    }
    function testForcedTransferBypassesOrdinaryGates() public {
        registry.updateCountry(BOB, 276); token.pause(); token.setAddressFrozen(ALICE, true);
        token.freezePartialTokens(ALICE, 70); token.forcedTransfer(ALICE, BOB, 80);
        require(token.balanceOf(BOB) == 80 &amp;amp;&amp;amp; token.getFrozenTokens(ALICE) == 20);
    }
    function testMintChecksCountry() public {
        registry.updateCountry(BOB, 276);
        vm.expectRevert(bytes("Compliance not followed")); token.mint(BOB, 1);
    }
    function testRecoveryPreservesRestrictions() public {
        aliceIdentity.allowRecovery(NEW_WALLET);
        token.freezePartialTokens(ALICE, 20); token.setAddressFrozen(ALICE, true); token.pause();
        token.recoveryAddress(ALICE, NEW_WALLET, address(aliceIdentity));
        require(token.balanceOf(ALICE) == 0 &amp;amp;&amp;amp; token.balanceOf(NEW_WALLET) == 100);
        require(token.getFrozenTokens(NEW_WALLET) == 20 &amp;amp;&amp;amp; token.isFrozen(NEW_WALLET));
        require(!registry.contains(ALICE) &amp;amp;&amp;amp; registry.investorCountry(NEW_WALLET) == 250);
    }
    function testNonAgentForcedTransferDenied() public {
        vm.expectRevert(); vm.prank(BOB); token.forcedTransfer(ALICE, BOB, 1);
    }
    function testEmptyTopicsAdmitRegisteredIdentity() public {
        bobIdentity.setPresent(false); require(!registry.isVerified(BOB));
        topics.removeClaimTopic(7); require(registry.isVerified(BOB)); send(10);
    }
    function testPolicyChangeInvalidatesPreflight() public {
        require(compliance.canTransfer(ALICE, BOB, 10)); setCountry(250, false); deny();
    }
    function testComplianceContextsAreIsolated() public {
        ModularCompliance second = new ModularCompliance(); second.init();
        second.bindToken(address(token)); second.addModule(address(policy));
        require(compliance.canTransfer(ALICE, BOB, 10));
        require(!second.canTransfer(ALICE, BOB, 10));
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run &lt;code&gt;forge test -vv&lt;/code&gt; from the project directory. The recorded local run on September 30, 2026 compiled the fixture with Solidity 0.8.17 and passed all 19 tests, with no skipped tests. That count describes these named examples. It does not measure exhaustive path coverage, successful mainnet integration or resistance to every adversarial input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove that a denial test can fail
&lt;/h2&gt;

&lt;p&gt;A passing rejection test is more persuasive when it detects a known defect. For the recorded negative run, the module's final check was deliberately replaced with &lt;code&gt;return true&lt;/code&gt;. The country-change regression then failed because the expected revert did not occur. Restoring the mapping lookup made that same test pass, after which the complete suite passed again.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight diff"&gt;&lt;code&gt;&lt;span class="gd"&gt;--- deliberately-permissive/RecipientCountryModule.sol
&lt;/span&gt;&lt;span class="gi"&gt;+++ corrected/RecipientCountryModule.sol
&lt;/span&gt;&lt;span class="p"&gt;@@ -20,7 +20,7 @@&lt;/span&gt;
     {
         IToken token = IToken(IModularCompliance(compliance).getTokenBound());
         uint16 country = token.identityRegistry().investorCountry(to);
&lt;span class="gd"&gt;-        return true; // DELIBERATE TEST MUTATION: ignore the recipient country.
&lt;/span&gt;&lt;span class="gi"&gt;+        return allowed[compliance][country];
&lt;/span&gt;     }
&lt;span class="err"&gt;
&lt;/span&gt;     function moduleTransferAction(address, address, uint256)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a mutation of our tutorial module, not an upstream security finding. Keep the failing output, the correction diff and the passing output together. Otherwise a demonstration can accidentally compare different test selections or lose the source change that explains the result. Both runs used &lt;code&gt;forge test --match-test testDeniedCountry -vv&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F21fo4bcj6gdctoxxuypb.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F21fo4bcj6gdctoxxuypb.gif" alt="Recorded test replay: deliberately allowing every country makes testDeniedCountry fail; restoring the compliance-scoped mapping lookup makes it pass, followed by 19 passing tests." width="799" height="371"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Replay of recorded local test output: deliberately permissive tutorial variant, correction and passing checks. The animation summarizes the retained outputs; it is not a terminal screen recording.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;The denial helper checks unchanged balances after rejected ordinary transfers. The delegated-spending test also checks that a failed &lt;code&gt;transferFrom&lt;/code&gt; leaves its allowance intact. Those assertions connect the error to state, which is more useful than merely observing that some call reverted. Extend this approach to event expectations and downstream accounting when adding stateful modules.&lt;/p&gt;

&lt;p&gt;The context-isolation case deserves a precise reading. It binds the same module to a second compliance instance and confirms that the second instance does not inherit the first instance's country permission. Both compliance instances point to the same token solely to keep this probe small. This is a namespace test, not a rehearsal of two separately deployed instruments with different registries and governance.&lt;/p&gt;

&lt;p&gt;For a production shared-module deployment, create that second instrument explicitly. Change the first instrument's country rule and verify the second instrument's full transfer result remains unchanged. Then reverse the direction of the change. Include distinct owners and registry data so that an accidental cross-reference cannot hide behind identical fixture values. Those additional cases are proposed extensions, not part of the reported 19-test result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review minting and exceptional movement separately
&lt;/h2&gt;

&lt;p&gt;A single assertion that restrictions work hides differences between entry points. The country rule blocks ordinary receipt in the denied country and also blocks minting there in this selected implementation. Our tests exercise both. Forced transfer follows another path: an authorized agent can move tokens while the token is paused and the sender is frozen, without applying this module's ordinary country check.&lt;/p&gt;

&lt;p&gt;The fixture freezes 70 of Alice's 100 units, then forces a transfer of 80. The asserted result is Bob holding 80 and Alice retaining 20 frozen units. This concrete balance transition is the reviewed behavior, not a generic statement that an agent can ignore everything. Recipient identity verification still matters, and the token invokes the transfer callback after moving the balance.&lt;/p&gt;

&lt;p&gt;Our module's callback is empty. A different module may enforce additional behavior there or revert. Test the complete installed module set before describing forced transfer as guaranteed emergency liquidity. Add explicit negative cases for unauthorized agents and invalid recipients, and test your intended handling of an already frozen recipient. The provided suite covers the unauthorized-agent case; it does not claim those additional recipient cases were executed.&lt;/p&gt;

&lt;p&gt;Burning needs its own supply and accounting tests in a production suite. This example implements the required burn hook but does not include a burn scenario. Similarly, the existence of batch entry points does not establish the desired partial-failure semantics for a custody integration. Enumerate the actual entry points the integration exposes and attach an acceptance case to each.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat pause and recovery as separate controls
&lt;/h2&gt;

&lt;p&gt;The ERC-3643 authors, including Joachim Lebrun, make pausing an explicit requirement in the &lt;a href="https://eips.ethereum.org/EIPS/eip-3643#specification" rel="noopener noreferrer"&gt;specification&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;MUST have the possibility to pause the token&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For an operator, the useful follow-up is the scope of that pause. Our test proves that &lt;code&gt;compliance.canTransfer&lt;/code&gt; can return true while an ordinary transfer reverts because the token is paused. The forced-transfer test proves that an agent operation can still move a balance in that state. An incident runbook must identify which operations remain available after the pause transaction confirms.&lt;/p&gt;

&lt;p&gt;Recovery is another privileged workflow. The fixture authorizes the replacement wallet through the identity stub's management-key check, then exercises the actual token recovery function. It asserts movement of the full balance, preservation of a 20-unit partial freeze and the wallet's frozen flag, removal of the old registry association and retention of country &lt;code&gt;250&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That result does not rotate credentials across every connected service or remove every old key from the identity contract. A real recovery procedure needs separate authorization evidence and updates to offchain custody records. It also needs a negative suite for an unauthorized replacement wallet. The identity stub deliberately makes the positive management-key condition easy to arrange; it cannot establish the security of a real key-management implementation.&lt;/p&gt;

&lt;p&gt;A recovery review should also trace permissions across contract boundaries. The local setup grants the token permission to change the identity registry because the recovery operation needs that interaction. Checking only the caller's token-agent role would miss this dependency. Remove the supporting registry authority in a separate deployment rehearsal and confirm that recovery fails without leaving a partially moved balance or a stranded registry entry.&lt;/p&gt;

&lt;p&gt;Before restoring service after an incident, compare both balance state and identity state with the approved recovery request. Confirm the replacement address through the organization's established process, then reconcile downstream systems against the final transaction receipt. Reusing a previously approved wallet address from an old support ticket is not sufficient evidence that the current request is authorized.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give preflight results an honest lifetime
&lt;/h2&gt;

&lt;p&gt;A frontend can ask the compliance contract whether its modules would permit a movement. It should label the result accordingly. The check does not establish adequate allowance, an unfrozen wallet or an unpaused token. Our paused-token test makes that limitation visible without depending on an RPC provider or a race between transactions.&lt;/p&gt;

&lt;p&gt;The policy-change test establishes another boundary. It observes a positive compliance check, disables the previously permitted country and then confirms that the ordinary transfer fails. In a deployed system, the state used for a preflight call can change before transaction execution. Avoid turning a simulated result into a promise that the eventual transaction will succeed.&lt;/p&gt;

&lt;p&gt;For a transaction preview, simulate the exact entry point with the intended sender, calldata and value against a documented block context. Show the time or block associated with the result, then handle an execution failure as an expected possibility. Preserve the transaction hash and inspect the mined receipt before updating balances as settled. These are integration recommendations; the local fixture does not test a remote provider or mempool ordering.&lt;/p&gt;

&lt;p&gt;Failure messages should also preserve the distinction between identity, policy and token state. The pinned token combines some rejection conditions under one revert string. An application may need separate read-only diagnostics to explain a failed simulation, but it should not claim a uniquely established cause when several conditions are false. Keep sensitive identity material out of public error messages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make policy changes visible to reviewers
&lt;/h2&gt;

&lt;p&gt;Changing a trusted issuer, removing a required topic or replacing the compliance contract can alter admissible transfers without changing the token's visible name. Treat those operations as changes to the running system's policy. Maintain an inventory of who can perform each one, including any authority that can upgrade the code implementing those permissions.&lt;/p&gt;

&lt;p&gt;The custom module emits &lt;code&gt;CountryPermissionSet&lt;/code&gt; with the compliance address, country and new value. Retain that context in operational records so that a shared module's event is associated with the right token deployment. Compare observed configuration with an approved baseline after changes. An event stream is useful evidence, but a missed subscription message should not prevent reconciliation from current state and historical logs.&lt;/p&gt;

&lt;p&gt;Choose an approval mechanism appropriate to each authority. A multisignature owner may reduce dependence on one key; a timelock may create review time for a configuration change. Neither control automatically governs an agent that retains a separate immediate operation. Draw the complete authority map and test the exceptional route, including who can remove or replace its operator.&lt;/p&gt;

&lt;p&gt;Treat changing the allowlist as a release even when no Solidity file changes. Save a before-and-after view, simulate a permitted and a denied movement under the new configuration, and state whether existing holders are affected. Assign responsibility for reconciliation if an operator discovers that a registry country value was wrong. This preserves the connection between the administrative decision and the transfers it actually governs.&lt;/p&gt;

&lt;p&gt;The final release receipt should identify the exact source commit, compiler settings, dependency lockfile and deployed bytecode. Attach the configured identities and policy addresses, required topics, trusted issuers, permitted countries and role assignments. Then attach the executed test selection and its limitations. Reviewers need enough information to reproduce the result and enough precision to see what remains untested.&lt;/p&gt;

&lt;p&gt;An ERC-3643 implementation is ready for the next engineering review when each permitted movement and each expected rejection has an explicit explanation tied to selected code and configuration. This fixture supplies that explanation for one recipient-country rule and its tested interactions. Expand it around the instrument's real identity system, deployment architecture and authorized operations before treating it as production acceptance evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bulbapp.io/p/e2580ef7-324b-442d-b818-92b8c8442b3d/five-smart-contract-upgrade-patterns-compared" rel="noopener noreferrer"&gt;Five Smart Contract Upgrade Patterns Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-2-40942c055d79" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.medium.com/upgradeable-solidity-smart-contracts-part-1-versioning-7e6e97cafc28" rel="noopener noreferrer"&gt;Upgradeable Solidity Smart Contracts. Part 1 — Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-1-222fe44ee14c" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/web3-smart-contracts-oracles-part-1-3905b127c01d" rel="noopener noreferrer"&gt;Web3. Smart Contracts. Oracles. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wo81ttokb6meiuexdko.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7wo81ttokb6meiuexdko.jpg" width="112" height="112" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>testing</category>
    </item>
    <item>
      <title>Verify an Indexer Can Recover from a Chain Reorganization</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Sat, 26 Sep 2026 19:57:43 +0000</pubDate>
      <link>https://dev.to/pharos_production/verify-an-indexer-can-recover-from-a-chain-reorganization-1gn0</link>
      <guid>https://dev.to/pharos_production/verify-an-indexer-can-recover-from-a-chain-reorganization-1gn0</guid>
      <description>&lt;p&gt;An indexer can reach the latest block and still serve data from an abandoned chain. Its checkpoint advances, its health endpoint stays green and its token balance includes a transfer that no longer exists in canonical history. Recovery needs a stronger acceptance test: after replacing a branch, every indexed result must match an independent rebuild of the selected canonical history.&lt;/p&gt;

&lt;p&gt;This tutorial turns that requirement into an executable rehearsal. A small Python and SQLite model indexes signed amounts, replaces an orphaned suffix and checks its state against a separate pure replay function. The fixture finishes at block &lt;code&gt;B4&lt;/code&gt; with a total of &lt;code&gt;24&lt;/code&gt;. Repeating the same input changes nothing. Injecting an exception during recovery leaves the previously committed state intact.&lt;/p&gt;

&lt;p&gt;The example is deliberately bounded. It uses synthetic block identifiers and complete, already selected branches. It does not implement Ethereum consensus, validate block hashes or connect to a node. The database represents one chain and one aggregate. Those limits make the assertions easy to inspect before adapting them to a production indexer with multiple projections and concurrent readers.&lt;/p&gt;

&lt;p&gt;The acceptance condition is useful beyond this model. A wallet requires correct ownership and balances; an analytics product requires correct event history and aggregates. Decide which results users depend on before choosing the recovery mechanism.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the state that must recover
&lt;/h2&gt;

&lt;p&gt;A checkpoint containing only a height cannot identify a chain. Two competing blocks can occupy the same position. Record the block number together with its hash, then retain enough parent relationships to establish which stored blocks belong to the replacement branch. Treat the checkpoint as a claim about fully committed application state, not merely the most recent RPC response.&lt;/p&gt;

&lt;p&gt;For an Ethereum log pipeline, distinguish an observed event from the transaction that produced it. A useful occurrence key contains the chain identity, block hash, transaction hash and log index. The chain identity can live in the database namespace for a strictly single-chain deployment. A transaction hash alone cannot distinguish its appearances across competing histories.&lt;/p&gt;

&lt;p&gt;The Geth documentation makes the delivery consequence explicit:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“a subscription can emit logs for the same transaction multiple times.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Geth documentation, &lt;a href="https://geth.ethereum.org/docs/interacting-with-geth/rpc/pubsub" rel="noopener noreferrer"&gt;Real-time Events&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That statement concerns notification behavior, not a promise that every disconnected consumer will receive every correction. Design storage so repeated observations cannot duplicate an occurrence, while a transaction appearing in a different block can be represented accurately. Its execution context may have changed; do not carry its old derived output into the replacement block without decoding the new receipt.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;blockchain software engineering scope at Pharos Production&lt;/a&gt; includes applications whose product behavior depends on off-chain data. A reorganization therefore belongs in the application acceptance criteria: the frontend can display a wrong balance even when every smart contract executed correctly. This tutorial supplies a concrete database exercise for that boundary; it does not claim a measured customer outcome.&lt;/p&gt;

&lt;p&gt;Write the recovery contract in terms of visible state. Canonical block membership, active event rows, materialized aggregates and the checkpoint must agree at a committed boundary. If historical orphan records are retained for audit, mark them explicitly and exclude them from current product queries. Deleting them is only one storage policy.&lt;/p&gt;

&lt;p&gt;Also specify what a reader can observe during recovery. A single database transaction can present an old committed state followed by a new committed state, subject to the database's isolation behavior. An asynchronous projection pipeline needs a visible generation or watermark instead. Otherwise an API can combine the new event table with yesterday's aggregate and return a result that belongs to neither branch.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a fork with a result you can calculate
&lt;/h2&gt;

&lt;p&gt;Use a fixture small enough to audit without trusting the implementation. The shared prefix is &lt;code&gt;G → A1&lt;/code&gt;. The old suffix is &lt;code&gt;A2 → A3&lt;/code&gt;; the replacement suffix is &lt;code&gt;B2 → B3 → B4&lt;/code&gt;. The caller selects the replacement branch. The model never decides that a branch wins merely because it has more blocks.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Block&lt;/th&gt;
&lt;th&gt;Parent&lt;/th&gt;
&lt;th&gt;Event&lt;/th&gt;
&lt;th&gt;Signed units&lt;/th&gt;
&lt;th&gt;Role&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;none&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;Trusted fixture origin&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A1&lt;/td&gt;
&lt;td&gt;G&lt;/td&gt;
&lt;td&gt;deposit&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Shared prefix&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A2&lt;/td&gt;
&lt;td&gt;A1&lt;/td&gt;
&lt;td&gt;shared-tx&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Old occurrence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;A3&lt;/td&gt;
&lt;td&gt;A2&lt;/td&gt;
&lt;td&gt;orphan-tx&lt;/td&gt;
&lt;td&gt;-2&lt;/td&gt;
&lt;td&gt;Orphan-only event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B2&lt;/td&gt;
&lt;td&gt;A1&lt;/td&gt;
&lt;td&gt;shared-tx&lt;/td&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Replacement occurrence&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B3&lt;/td&gt;
&lt;td&gt;B2&lt;/td&gt;
&lt;td&gt;credit&lt;/td&gt;
&lt;td&gt;11&lt;/td&gt;
&lt;td&gt;New event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;B4&lt;/td&gt;
&lt;td&gt;B3&lt;/td&gt;
&lt;td&gt;debit&lt;/td&gt;
&lt;td&gt;-4&lt;/td&gt;
&lt;td&gt;Replacement tip&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The old total is &lt;code&gt;10 + 7 - 2 = 15&lt;/code&gt;. Undoing its suffix returns the aggregate to &lt;code&gt;10&lt;/code&gt;. Applying the replacement produces &lt;code&gt;10 + 7 + 11 - 4 = 24&lt;/code&gt;. Keeping the old negative event would leave an incorrect result. Keeping both occurrences of the shared transaction would also fail, even if the checkpoint looked correct.&lt;/p&gt;

&lt;p&gt;These amounts describe a synthetic signed counter. They are not an ERC-20 balance implementation: there are no addresses, decimals, fees or contract-specific event semantics. Use integer quantities in the model so arithmetic noise cannot obscure a branch identity defect. A real token indexer must define how its decoder maps each event into the relevant accounts and units.&lt;/p&gt;

&lt;p&gt;Make the oracle independent of the repair path. The example folds the chosen branch directly into the expected block rows, event rows and total. It never reads the database or calls the rollback code. This catches an indexer that agrees with its own checkpoint while retaining wrong rows. It does not validate a faulty decoder shared by both paths; production replay needs separate golden receipt fixtures for that risk.&lt;/p&gt;

&lt;p&gt;An empty block matters too. It advances chain continuity without changing the aggregate. An indexer that checkpoints only blocks containing matching logs has discarded evidence it needs when locating a common ancestor. Keep headers or equivalent ancestry records for the entire retained recovery interval.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make replacement and checkpoint advancement atomic
&lt;/h2&gt;

&lt;p&gt;The replacement operation first checks that its input is a connected branch from the trusted origin. Each block must extend the previous hash and increment the height by one. It then compares stored block identities with the candidate branch until their shared prefix ends. Everything after that point is subject to replacement.&lt;/p&gt;

&lt;p&gt;Bound the amount of history the automatic path may undo. A rollback window is an operational limit, not a claim that deeper reorganizations are impossible. If recovery needs records outside the retained window, stop the normal writer and preserve evidence. Continuing from a guessed ancestor can make a damaged projection look current.&lt;/p&gt;

&lt;p&gt;Inside one transaction, remove the losing suffix in reverse block order, reverse its aggregate contributions and append the replacement suffix in forward order. Advance the checkpoint only after the new rows and aggregate updates succeed. Committing those changes together is what makes the checkpoint meaningful. Writing it last without a shared transaction is insufficient when earlier writes can persist independently.&lt;/p&gt;

&lt;p&gt;Inverse arithmetic works for this additive projection. It does not automatically work for every business object. Reversing a maximum value, an ownership transition with side effects or an order-dependent state machine may require before-images, versioned entities or a replay from a saved boundary. Test the actual projection algorithm rather than assuming every update has a safe subtraction.&lt;/p&gt;

&lt;p&gt;The code uses explicit SQL transaction commands and disables Python's implicit transaction opening with &lt;code&gt;isolation_level=None&lt;/code&gt;. See the &lt;a href="https://docs.python.org/3/library/sqlite3.html#transaction-control" rel="noopener noreferrer"&gt;Python sqlite3 transaction documentation&lt;/a&gt; for the connection behavior. This choice keeps the demonstrated boundary visible. It is not a database configuration recommendation for every deployment.&lt;/p&gt;

&lt;p&gt;There is one writer in this fixture. A production worker pool needs a fence that prevents an older worker from committing after a newer recovery generation takes ownership. A database lock can serialize writers, but lock acquisition alone does not prove that the worker's fetched branch is still acceptable. Validate the generation or expected checkpoint at the commit boundary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the storage model
&lt;/h2&gt;

&lt;p&gt;Save the following as &lt;code&gt;reorg_harness.py&lt;/code&gt;. Block hashes are readable fixture labels; the input contract assumes immutable content for each label. There is no remote I/O inside the transaction. The &lt;code&gt;finalized&lt;/code&gt; argument, when supplied, represents an externally verified anchor that the selected branch must contain.&lt;/p&gt;

&lt;p&gt;The dApp delivery process described by Pharos Production includes indexer strategy during discovery and indexer deployment during production readiness. The &lt;a href="https://pharosproduction.com/services/dapp-development-company/" rel="noopener noreferrer"&gt;dApp development and indexer delivery process&lt;/a&gt; provides a place to assign this rehearsal to a release owner. Passing the local example establishes only the behavior demonstrated below; the deployed storage and RPC adapter still need their own evidence.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# (transaction hash, log index, signed units)
&lt;/span&gt;
&lt;span class="n"&gt;G&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;G&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;A1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;G&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;deposit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),))&lt;/span&gt;
&lt;span class="n"&gt;A2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared-tx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),))&lt;/span&gt;
&lt;span class="n"&gt;A3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;orphan-tx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),))&lt;/span&gt;
&lt;span class="n"&gt;B2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared-tx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;),))&lt;/span&gt;
&lt;span class="n"&gt;B3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;credit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;),))&lt;/span&gt;
&lt;span class="n"&gt;B4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;debit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),))&lt;/span&gt;
&lt;span class="n"&gt;OLD&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;NEW&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;B2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;B3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;B4&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;isolation_level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executescript&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;
        CREATE TABLE IF NOT EXISTS blocks(
          n INTEGER PRIMARY KEY, hash TEXT UNIQUE, parent TEXT);
        CREATE TABLE IF NOT EXISTS events(
          block_hash TEXT, tx TEXT, idx INTEGER, units INTEGER,
          PRIMARY KEY(block_hash, tx, idx));
        CREATE TABLE IF NOT EXISTS state(
          id INTEGER PRIMARY KEY CHECK(id=1),
          n INTEGER, hash TEXT, total INTEGER);
        INSERT OR IGNORE INTO state VALUES(1,-1,&lt;/span&gt;&lt;span class="sh"&gt;''&lt;/span&gt;&lt;span class="s"&gt;,0);
        &lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM blocks ORDER BY n&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT * FROM events ORDER BY 1,2,3&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchall&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT n,hash,total FROM state&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_rewind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;finalized&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fail_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# Input is an already selected, immutable full branch from G.
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong trusted genesis&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;prev&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;broken lineage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;finalized&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;finalized&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finalized anchor conflict&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;
        &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;BEGIN IMMEDIATE&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;expected_tip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt; &lt;span class="nf"&gt;else &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;""&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="n"&gt;expected_tip&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkpoint ahead or inconsistent&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;common&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;
                &lt;span class="n"&gt;common&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;common&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;max_rewind&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rewind limit exceeded&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;common&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]):&lt;/span&gt;
                &lt;span class="n"&gt;removed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                    &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;SELECT COALESCE(SUM(units),0) FROM events WHERE block_hash=?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt;
                &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fetchone&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE state SET total=total-?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;removed&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
                &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE FROM events WHERE block_hash=?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;h&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
                &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DELETE FROM blocks WHERE n=?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fail_at&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;after_undo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;injected after undo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;common&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:]:&lt;/span&gt;
                &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO blocks VALUES(?,?,?)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                           &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;INSERT INTO events VALUES(?,?,?,?)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                               &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;block&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                    &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE state SET total=total+?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;,))&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fail_at&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;before_checkpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;injected before checkpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;tip&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE state SET n=?,hash=?&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tip&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;COMMIT&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;BaseException&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ROLLBACK&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;oracle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Independent fold: no rollback logic and no database reads.
&lt;/span&gt;    &lt;span class="n"&gt;blocks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;events&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;tx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;state&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="n"&gt;height&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nb"&gt;hash&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
             &lt;span class="nf"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;units&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;units&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;blocks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;state&lt;/code&gt; row deliberately combines checkpoint identity with the materialized total. A wrong total cannot hide behind a correct height in the expected snapshot. The event table has a composite primary key, so a malformed fixture containing the same occurrence twice aborts the transaction instead of silently applying its amount twice.&lt;/p&gt;

&lt;p&gt;Repeated delivery is tested at the branch synchronization boundary. Synchronizing an already committed branch skips its existing prefix and leaves the state unchanged. This is different from accepting inconsistent duplicate payloads. A duplicate occurrence inside a newly supplied block is rejected, because accepting conflicting representations of immutable history would conceal an upstream defect.&lt;/p&gt;

&lt;p&gt;A full history input keeps the demonstration compact. Production recovery normally fetches only the required headers and suffix, starting from a trusted snapshot or retained boundary. Do not copy the full-history interface into a large deployment without changing its memory, retention and fetch behavior. Preserve the same acceptance conditions when replacing the fixture adapter with a bounded branch reader.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;oracle&lt;/code&gt; function is short enough to inspect separately from &lt;code&gt;sync&lt;/code&gt;. Its event ordering is deterministic, and it derives the total from the selected input.&lt;/p&gt;

&lt;p&gt;Comparing only the final amount would be weaker: unrelated mistakes can cancel numerically. The full snapshot catches a correct total accompanied by stale event identities or an incorrect parent chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exercise recovery, refusal and restart
&lt;/h2&gt;

&lt;p&gt;Save this second file as &lt;code&gt;test_reorg_harness.py&lt;/code&gt; beside the first and run &lt;code&gt;python3 -m unittest -v test_reorg_harness.py&lt;/code&gt;. It uses the standard library and a temporary database. The ten test methods passed in the local rehearsal for this article. That result is evidence about these fixtures, not a production throughput or durability measurement.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;unittest&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;reorg_harness&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;oracle&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RecoveryTests&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TestCase&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;setUp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;TemporaryDirectory&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;index.db&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;tearDown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tmp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cleanup&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_switch_matches_independent_fold&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;oracle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B4&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_repeated_delivery_is_idempotent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;once&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;once&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_reincluded_transaction_has_new_block_identity&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared-tx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;B2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;shared-tx&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_restart_after_each_injected_failure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;after_undo&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;before_checkpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;subTest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                    &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fail_at&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;point&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;oracle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;OLD&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_deep_reorg_refuses_without_mutation&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaisesRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;rewind limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_rewind&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_finalized_anchor_conflict_refuses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaisesRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;finalized anchor&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;finalized&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A2&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_bad_checkpoint_refuses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;UPDATE state SET n=99&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaisesRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;checkpoint&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NEW&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_broken_parent_refuses&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaisesRegex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;lineage&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;X&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;wrong&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_shorter_selected_branch_and_empty_block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;branch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;EMPTY&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nf"&gt;oracle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;branch&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;test_duplicate_event_payload_aborts_transaction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;before&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicate&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertRaises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;IntegrityError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;G&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;A1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Block&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;DUP&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;A1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;event&lt;/span&gt;&lt;span class="p"&gt;))])&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;snapshot&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;before&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;__main__&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;unittest&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;verbosity&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recovery test checks all modeled state against the independent fold. The re-inclusion test isolates the transaction identity problem: only the occurrence under &lt;code&gt;B2&lt;/code&gt; remains active. The duplicate-delivery test verifies idempotency after a successful commit. These failures deserve separate names because a single final-total assertion would make diagnosis harder.&lt;/p&gt;

&lt;p&gt;The refusal tests establish a second kind of correctness. When the requested rollback exceeds the configured limit, the indexer must leave its committed state unchanged. The same applies to a conflicting finalized anchor or a broken parent relationship. Refusal is useful only if it is observable to the operator and prevents the worker from advertising itself as caught up.&lt;/p&gt;

&lt;p&gt;A corruption fixture changes the saved height to &lt;code&gt;99&lt;/code&gt; before attempting recovery. The operation rejects that inconsistent starting state instead of treating the number as authority. A production corruption check needs to cover more than the tip: missing earlier rows or a damaged aggregate can survive a superficial checkpoint comparison. Periodic reconciliation and storage integrity checks serve that separate purpose.&lt;/p&gt;

&lt;p&gt;A shorter-branch fixture prevents an accidental assumption that height must always increase. It also includes a block with no events. The caller remains responsible for deciding which branch is valid. This test establishes that the storage mechanism can install a supplied connected history, including a shorter one, within its configured rollback limit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Distinguish an exception from a process crash
&lt;/h2&gt;

&lt;p&gt;Two injected failures interrupt the transaction: one after undoing the old suffix, another after applying replacement events but before advancing the checkpoint. Each raises an exception, triggers a rollback and closes the connection. Reopening the database must reveal the original committed state. A subsequent recovery must then reach the oracle result.&lt;/p&gt;

&lt;p&gt;That rehearsal verifies application transaction boundaries. It does not simulate an operating-system kill, a machine restart or loss of durable storage. Claiming crash safety from an exception test would skip precisely the behavior that the database and deployment environment contribute. Keep those evidence categories separate in the release record.&lt;/p&gt;

&lt;p&gt;For a process-level extension, run the real worker against an isolated test database and terminate it at instrumented boundaries. Reopen through the normal startup path, then compare its state with the saved canonical fixture. Include termination immediately after commit but before the worker acknowledges success. Retrying that work must not duplicate rows or external deliveries.&lt;/p&gt;

&lt;p&gt;Use deterministic barriers rather than timing guesses. A test coordinator can wait until a worker reports that it has reached a specified boundary, then trigger the failure. Record whether the signal occurs before a write, after a write or after commit. A sleep followed by termination rarely establishes which transaction state was actually exercised.&lt;/p&gt;

&lt;p&gt;Repeat this with the production database engine, transaction isolation and deployment configuration. If projections live in separate stores, one local transaction cannot provide atomicity across all of them. A versioned publication barrier or an explicitly reconciled workflow must cover that gap. Test readers as well as writers: the API should expose a consistent generation or an honest recovery status while downstream projections catch up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile the provider before trusting the next event
&lt;/h2&gt;

&lt;p&gt;An event subscription is a useful notification channel, but it should not be the only recovery record. Geth documents connection-bound subscriptions and the absence of historical event delivery. After reconnecting, reconcile the saved checkpoint against the node's current canonical chain and backfill the required interval. Receiving a new notification proves that the connection works; it does not prove that the gap was empty.&lt;/p&gt;

&lt;p&gt;Ethereum's &lt;a href="https://ethereum.org/developers/docs/apis/json-rpc/" rel="noopener noreferrer"&gt;JSON-RPC reference&lt;/a&gt; exposes block identity and parent identity, along with log fields and block-based lookup methods. An adapter should preserve those identities throughout acquisition. A numeric range fetched while the head changes can otherwise mix observations from different histories. Test the adapter with deliberately inconsistent responses, including a receipt whose block identity does not match the expected header.&lt;/p&gt;

&lt;p&gt;One practical acquisition contract is to capture a target block identity, walk its parent-linked history and verify each returned object against the expected hash. Revalidate the target against the provider's canonical view before publishing the recovered generation. If the target changed, abandon or restart that attempt according to the adapter's bounded retry policy. The chain can change again later, so repeated reconciliation remains necessary.&lt;/p&gt;

&lt;p&gt;Where the provider supports log queries restricted to a block hash, use that identity to reduce ambiguity. Where it only supplies ranges, validate each result and recheck the associated headers. An empty result is not automatically proof of complete history: the provider may have limited retention, truncated a response or failed a request. Preserve explicit completion evidence for the fetch contract your provider actually offers.&lt;/p&gt;

&lt;p&gt;Add a second fork during recovery to the adapter suite. Also test reconnects, duplicate notifications and switching to a provider with a different observed head. The desired result is either a coherent committed generation or a bounded refusal. A mixture assembled from two provider views should never pass merely because all requested heights were returned.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give finality and external effects separate policies
&lt;/h2&gt;

&lt;p&gt;Confirmation depth and protocol finality answer different questions. A configurable number of later blocks is an application risk policy; it is not a universal finality guarantee. Ethereum's &lt;a href="https://ethereum.org/developers/docs/consensus-mechanisms/pos/gasper/" rel="noopener noreferrer"&gt;Gasper explanation&lt;/a&gt; describes how justified and finalized checkpoints relate to fork choice. Map your application's publication boundary to the actual chain and provider semantics you operate.&lt;/p&gt;

&lt;p&gt;The JSON-RPC API includes &lt;code&gt;safe&lt;/code&gt; and &lt;code&gt;finalized&lt;/code&gt; block tags, but a product spanning multiple networks must verify support and meaning for each network. In particular, a rollup's observed execution head and its settlement conditions require network-specific treatment. Do not reuse an Ethereum execution-layer assumption as proof of settlement on another system.&lt;/p&gt;

&lt;p&gt;The model's finalized-anchor check is intentionally conservative. If the selected branch conflicts with the externally supplied anchor, it refuses mutation. An operator then investigates the provider, chain identity and saved anchor. The model does not explain how to resolve a consensus failure or choose between conflicting trusted sources.&lt;/p&gt;

&lt;p&gt;A database rollback also cannot retract an email already delivered or automatically reverse an action in another system. Store outbound work with its originating event identity and recovery generation. Decide which effects wait for the relevant finality policy, which can be canceled before dispatch and which require a separately authorized compensation process after dispatch.&lt;/p&gt;

&lt;p&gt;Test the dispatcher race explicitly. A queued event can become orphaned between selection and delivery. A local validity check narrows that window but cannot make a remote action atomic with chain consensus. For consequential effects, the acceptance contract must state the remaining exposure and the permitted response. Avoid promising exactly-once real-world behavior from a database uniqueness constraint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retain the evidence needed for the next rollback
&lt;/h2&gt;

&lt;p&gt;History retention sets a practical limit on recovery. If the database keeps only current entity values, it may lack the information required to reverse an old transition. Define the retained header interval together with the event journal, decoder versions and any projection snapshots. Keeping headers longer than the data needed to rebuild their effects does not extend the usable recovery window.&lt;/p&gt;

&lt;p&gt;Rehearse pruning as part of the lifecycle. Build a snapshot at a verified boundary, retain the required suffix and prove that replay from that snapshot produces the same state as the full fixture. Then present a fork whose ancestor lies before the retained boundary. The expected result is an explicit recovery-plan failure, not a partial rewind followed by a successful health check.&lt;/p&gt;

&lt;p&gt;Keep operational watermarks separate from retention policy. A consumer finishing a block does not necessarily mean every dependent projection or outbound queue has finished using its evidence. Document which components must acknowledge a generation before its journal becomes eligible for pruning. Otherwise a routine cleanup can remove the very rows an incident runbook assumes are available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep a release receipt that can be rerun
&lt;/h2&gt;

&lt;p&gt;A useful recovery receipt identifies the software revision, database configuration and fixture hashes. It records the old checkpoint, common ancestor and selected target, then stores the expected and observed projection snapshots. Save a structured diff when they disagree. A log line announcing completion cannot replace that comparison.&lt;/p&gt;

&lt;p&gt;Measure operational behavior separately from logical correctness. Record the rollback depth, recovery duration, backlog and time until each public projection reaches the recovered generation. Set budgets from the product's requirements and actual workload. The tiny fixture here supplies no defensible latency target for a deployed indexer.&lt;/p&gt;

&lt;p&gt;Before release, run the same acceptance contract through the real decoder, storage adapter and API read path. Include a replay from a retained snapshot so missing historical data becomes visible before an incident. Confirm that an operator can recognize a refusal, preserve evidence and resume from an approved boundary without editing the checkpoint by hand.&lt;/p&gt;

&lt;p&gt;The release gate is concrete: the supported reorganization cases converge to independently expected state, repeated work has no additional effect and interrupted work resumes without exposing mixed generations. Unsupported depth or conflicting finality evidence produces an explicit stop. Keep the failed fixtures as regression tests; they describe the exact circumstances under which a green health check once failed to tell the whole story.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bulbapp.io/p/e2580ef7-324b-442d-b818-92b8c8442b3d/five-smart-contract-upgrade-patterns-compared" rel="noopener noreferrer"&gt;Five Smart Contract Upgrade Patterns Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.medium.com/upgradeable-solidity-smart-contracts-part-1-versioning-7e6e97cafc28" rel="noopener noreferrer"&gt;Upgradeable Solidity Smart Contracts. Part 1 — Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/web3-smart-contracts-oracles-part-1-3905b127c01d" rel="noopener noreferrer"&gt;Web3. Smart Contracts. Oracles. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-1-222fe44ee14c" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-2-40942c055d79" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F10rbqvs1846bo9ibwb4j.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F10rbqvs1846bo9ibwb4j.jpg" width="112" height="112" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>blockchain</category>
      <category>testing</category>
      <category>database</category>
      <category>web3</category>
    </item>
    <item>
      <title>Healthcare RAG: Test the Claim-to-Source Contract</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Sat, 26 Sep 2026 08:18:29 +0000</pubDate>
      <link>https://dev.to/pharos_production/healthcare-rag-test-the-claim-to-source-contract-2036</link>
      <guid>https://dev.to/pharos_production/healthcare-rag-test-the-claim-to-source-contract-2036</guid>
      <description>&lt;p&gt;A healthcare RAG answer can carry a working citation link and still fail review. The page exists, the passage loads and the answer sounds reasonable. Yet the passage may support only half the sentence, belong to an older edition or describe a different population. A link check cannot resolve those failures.&lt;/p&gt;

&lt;p&gt;Before accepting a retrieval-augmented generation feature, specify a claim-to-source contract: what the product retains about each assertion, which checks it performs and what the user sees when a check fails. Treat that contract as a deliverable shared by ingestion, retrieval, answer rendering and review. The schema and acceptance cases below describe a proposed implementation for an educational healthcare knowledge product. They are not a clinical validation or a report of a deployed system.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with an assertion that can fail
&lt;/h2&gt;

&lt;p&gt;Consider a fictional learning assistant whose approved handbook describes how a training module is organized. A generated answer says that the module has an introductory lesson and an assessment. The cited passage describes the lesson but says nothing about assessment. Every component can appear healthy: ingestion succeeded, retrieval returned a relevant passage and the link opens the right page.&lt;/p&gt;

&lt;p&gt;The unsupported addition is still visible to the learner.&lt;/p&gt;

&lt;p&gt;Your first acceptance rule should therefore operate on assertions, rather than on paragraphs that happen to end with a reference. Divide independently checkable statements into separate claims. Retain which passage supports each one, including any qualification needed to keep its meaning intact.&lt;/p&gt;

&lt;p&gt;Do not assume one sentence equals one claim. A sentence can combine a population, a benefit and a comparison. Conversely, one claim may require two passages read together. Let the representation support a set of evidence references and explain why that set is sufficient. A mechanical requirement for exactly one reference would force the content into the wrong shape.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://arxiv.org/abs/2305.14627" rel="noopener noreferrer"&gt;ALCE research paper&lt;/a&gt; evaluates fluency, correctness and citation quality as separate dimensions. That distinction is useful for an acceptance plan: polished output and correct links are insufficient evidence of supported claims. The practical design decision here is to retain those checks separately so an attractive answer cannot hide a failed evidence check.&lt;/p&gt;

&lt;p&gt;For teams commissioning this work, the boundary between a prototype and an implementation should also appear in the statement of work. The &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;software product development scope&lt;/a&gt; should identify which evidence interactions are demonstrated and which are backed by working retrieval. A reviewer needs to know whether an opened passage was handpicked for the demo or selected by the system for that request.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep evidence identity separate from its location
&lt;/h2&gt;

&lt;p&gt;A source URL is a locator.&lt;/p&gt;

&lt;p&gt;It is not enough to identify the exact material behind a saved answer. The same address can serve a corrected document tomorrow. A source record should distinguish the publisher's identifier, the retrieved version and the particular passage used in an answer.&lt;/p&gt;

&lt;p&gt;For a document corpus, retain a content fingerprint with the ingestion run. Bind extracted passages to that fingerprint and to an extraction version. A new extraction can change reading order or table structure even when the original file has not changed. Without both identities, a reviewer may be comparing an old answer against new extracted text without realizing it.&lt;/p&gt;

&lt;p&gt;Page labels need similar care. A viewer's page index and the number printed on a book page can differ. Front matter can use a different numbering system. Store each independently and verify their mapping during ingestion. A fixed subtraction is appropriate only when the source actually has a verified fixed offset. Otherwise use an explicit page map, including pages without printed labels.&lt;/p&gt;

&lt;p&gt;The passage should retain enough context to inspect its qualifications. Extracting a sentence while dropping a table heading can remove the population or unit that makes it meaningful. Preserve the relevant parent heading, table headers and footnotes with a reference to the original location. If extraction cannot capture the context, mark that passage for review before using it as answer evidence.&lt;/p&gt;

&lt;p&gt;Here is a compact record shape. Values are synthetic and the identifiers intentionally describe test fixtures. This is a data contract, not a ready-made clinical schema:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"answer_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fixture-answer-01"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"answer_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"corpus_snapshot_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fixture-corpus-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"policy_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"education-only-v1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"claims"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"claim_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"c1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"The module contains an introductory lesson."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"evidence_refs"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"p1"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"support_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"pending_review"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"passages"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"passage_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"p1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fixture-handbook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fixture-edition-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"content_fingerprint"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fixture-fingerprint-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"extraction_version"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"fixture-extractor-a"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"source_class"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"handbook"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"locator"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"viewer_page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"printed_page"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"3"&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"This module begins with an introductory lesson."&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"review"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In an implementation, validate the fingerprint's real algorithm and format; the fixture string above is only a readable stand-in. Likewise, a support label is a decision to justify. It does not become true because a generator emitted the field. Store the verifier or reviewer decision separately and bind it to the answer version it actually examined.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give structural checks a narrow job
&lt;/h2&gt;

&lt;p&gt;Deterministic validation can establish whether referenced records exist, whether identifiers are unique and whether a page mapping resolves within the identified document. It can reject an answer with a dangling passage reference before the renderer builds a citation marker. Those are useful guarantees because they have precise failure conditions.&lt;/p&gt;

&lt;p&gt;This Python function implements only the reference-integrity portion, assuming the record has already passed type and required-field validation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;reference_errors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;claims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claims&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;passages&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;record&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;passages&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;claim_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;claim_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;passage_ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;passage_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;passages&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_ids&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;claim_ids&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicate_claim_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;passage_ids&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;passage_ids&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;duplicate_passage_id&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;known&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;passage_ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;claim&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;claims&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;refs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;claim&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;evidence_refs&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;refs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;missing_evidence_ref&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ref&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;known&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ref&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;refs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;dangling_evidence_ref&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;sorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I checked this function locally against the synthetic receipt and four mutations: a duplicated claim identifier, a duplicated passage identifier, an empty reference list and an unknown reference. The control returned an empty list; each mutation returned its corresponding error. Those five checks establish the illustrated reference behavior only. They do not verify source authenticity, passage meaning, access policy or page mapping. An empty claim list also needs a separate product rule if the caller expects an answer.&lt;/p&gt;

&lt;p&gt;String matching cannot establish that a clinical statement is justified. Exact copying can omit a negation immediately before the excerpt. A correct paraphrase can share few words with its source. Keep a distinct semantic review stage with a written rubric for support, contradiction and unresolved cases.&lt;/p&gt;

&lt;p&gt;For the fictional fixture, a structural check should resolve &lt;code&gt;c1&lt;/code&gt; to &lt;code&gt;p1&lt;/code&gt;, confirm the passage belongs to the declared corpus snapshot and verify that its locator opens the intended source version. A semantic review should compare the claim with the complete passage context. Adding an assessment to the claim should fail that review while leaving the structural result unchanged.&lt;/p&gt;

&lt;p&gt;This separation makes failures diagnosable. If the source cannot be opened, investigate source availability or access. If the wrong page opens, investigate the locator. If the correct page opens but does not support the claim, investigate generation or the support decision. Routing every problem into one hallucination counter discards information the team needs to repair it. Define the review unit before calculating any percentage. Citation completeness asks whether claims that need evidence have it. Support correctness asks whether the attached evidence supports those claims. A dashboard should name the denominator and preserve unresolved cases rather than silently removing them. Otherwise a system can improve its reported score by making difficult claims disappear from the evaluation set.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make source authority an explicit constraint
&lt;/h2&gt;

&lt;p&gt;A handbook explanation, a research finding and a regulatory record answer different questions. A product should preserve those source classes through extraction and rendering, instead of reducing them to interchangeable links. The reviewer must be able to tell what kind of evidence is attached before interpreting what it establishes.&lt;/p&gt;

&lt;p&gt;An article about a device cannot stand in for a regulator's record of that device's status. A regulatory record cannot, by itself, support every claim about comparative effectiveness. In your source policy, define which class may support which assertion type and require review when an answer crosses that boundary. Keep jurisdiction and retrieval date visible where they affect interpretation.&lt;/p&gt;

&lt;p&gt;This distinction appeared during Pharos Production's healthcare AI design work. Our published case reports that an audit found 6 of 7 demo citations wrong, including 4 invented citations. These were findings about our own mockups, not measurements of a deployed model. The response included verified source passages and separate presentation of handbook, research and regulatory evidence.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://pharosproduction.com/insights/healthcare/ai-citation-ux-aesthetic-medicine/?utm_source=devto&amp;amp;utm_medium=referral&amp;amp;utm_campaign=healthcare_citation_ux_20260926&amp;amp;utm_content=claim_source_contract" rel="noopener noreferrer"&gt;healthcare AI citation UX case study&lt;/a&gt; shows the source audit and illustrative screens behind those decisions, including the work still outstanding. Use it when reviewing an implementation proposal: compare the proposed evidence panel with the concrete design problems the case documents. The engagement remains in the design phase; external retrieval connectors shown in the demo are planned, not live.&lt;/p&gt;

&lt;p&gt;For an engineering team, the next step is to turn a chosen source policy into fixtures. Create a claim that uses the right passage under the wrong source class. The acceptance result must expose that mismatch even if the text appears relevant. Source authority should survive a rendering refactor and a retrieval-provider change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate support from permission to answer
&lt;/h2&gt;

&lt;p&gt;A supported statement can still be inappropriate for the product's intended use. An educational assistant should not drift into patient-specific treatment instructions because a passage contains information that looks relevant. Keep the source-support decision separate from the policy decision governing what this product may present.&lt;/p&gt;

&lt;p&gt;That separation needs visible states. A missing source, an unavailable source service and a request outside the product's permitted scope are different situations. The user needs a different explanation and next action for each. Internally, retaining separate reasons prevents the team from trying to fix a policy refusal by increasing the number of retrieved passages.&lt;/p&gt;

&lt;p&gt;The FDA's January 29, 2026, guidance discusses enabling a healthcare professional to "independently review the basis" for recommendations. That phrase concerns one part of the US non-device clinical decision support criteria; a citation interface alone does not establish a product's regulatory classification. The &lt;a href="https://www.fda.gov/media/109618/download" rel="noopener noreferrer"&gt;current FDA guidance&lt;/a&gt; is the source for that boundary, rather than a certification supplied by this design pattern.&lt;/p&gt;

&lt;p&gt;For the proposed educational product, define a policy result alongside the support result. A fully supported explanation can proceed only when its use is allowed. An unresolved support decision should remain visibly unresolved. A denied use should select the intended boundary response even if retrieval found relevant material. Have the responsible domain owner approve that behavior before teams encode it in templates.&lt;/p&gt;

&lt;p&gt;The priority between states also matters. Suppose retrieval times out on a request the product must not fulfill. Retrying the request automatically could produce prohibited output when the service recovers. Evaluate the intended-use boundary independently and avoid making an infrastructure error the only reason the answer was withheld.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build a small fixture suite with explicit oracles
&lt;/h2&gt;

&lt;p&gt;Start with a bounded corpus you are entitled to use and questions whose expected evidence can be inspected. Each fixture needs a reason it belongs in the suite, an expected user-visible outcome and an owner for disagreements. A collection of convenient questions without answerability labels will not reveal whether the product refuses too often.&lt;/p&gt;

&lt;p&gt;Keep synthetic examples visibly synthetic.&lt;/p&gt;

&lt;p&gt;Use neutral learning-content statements for plumbing tests so a malformed fixture cannot be mistaken for medical advice. Domain-specific acceptance cases need appropriately qualified review and controlled source material. Do not copy sensitive patient information into a shared test repository simply because a scenario would look more realistic.&lt;/p&gt;

&lt;p&gt;The following matrix is a proposed starting point. Its rows represent different failure classes, not a measured test result or a completeness claim:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Fixture&lt;/th&gt;
&lt;th&gt;Deliberate condition&lt;/th&gt;
&lt;th&gt;Required observable result&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Supported control&lt;/td&gt;
&lt;td&gt;Approved learning claim with an exact supporting passage&lt;/td&gt;
&lt;td&gt;Claim displays and its marker opens the correct version and location&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unsupported addition&lt;/td&gt;
&lt;td&gt;Add an assertion absent from the otherwise relevant passage&lt;/td&gt;
&lt;td&gt;Added assertion is withheld or enters review; the supported part remains distinguishable&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Locator mismatch&lt;/td&gt;
&lt;td&gt;Keep the passage text but point to another printed page&lt;/td&gt;
&lt;td&gt;Citation validation fails; the interface does not present the location as verified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authority mismatch&lt;/td&gt;
&lt;td&gt;Attach an assertion to a source class that cannot establish it&lt;/td&gt;
&lt;td&gt;Source-policy failure is visible and retained in the receipt&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Source unavailable&lt;/td&gt;
&lt;td&gt;Make the evidence service unavailable for the request&lt;/td&gt;
&lt;td&gt;An availability state appears; no fabricated passage fills the gap&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Covered question missed&lt;/td&gt;
&lt;td&gt;A gold passage exists but the retriever misses it&lt;/td&gt;
&lt;td&gt;Evaluation records a retrieval miss rather than treating the refusal as success&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intended-use boundary&lt;/td&gt;
&lt;td&gt;Ask the educational product for a patient-specific directive&lt;/td&gt;
&lt;td&gt;Approved boundary response appears, independent of source availability&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Changed source&lt;/td&gt;
&lt;td&gt;Replace a source version behind a saved answer&lt;/td&gt;
&lt;td&gt;The saved evidence remains identifiable and its review status is reconsidered&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Run the supported control alongside negative cases. A system that suppresses every answer can pass many refusal tests while failing its main purpose. The covered-question fixture provides a second safeguard: the absence of retrieved evidence does not prove the absence of evidence in the corpus.&lt;/p&gt;

&lt;p&gt;For each failure, check both the visible result and the retained record. A correct warning with a misleading success status in the receipt is still a defect. A correct internal rejection with an unsupported claim left on screen is also a defect. The acceptance oracle spans the service boundary and the rendered answer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Exercise the contract through the user interface
&lt;/h2&gt;

&lt;p&gt;Component tests should prove that a claim marker opens its bound passage, rather than whichever passage happens to occupy the same array position. Reorder the evidence list while preserving its identifiers. The marker must still lead to the same evidence. This catches a simple implementation mistake with consequences that a visually unchanged snapshot may miss.&lt;/p&gt;

&lt;p&gt;Also test the path back to the answer. After inspecting a passage, a reader should retain the context of the claim they were checking. If the interface opens a large document at its beginning, the citation may be technically reachable but practically difficult to review. Record that as a usability finding instead of converting it into an invented accuracy metric.&lt;/p&gt;

&lt;p&gt;Use representative intended users for that review. Ask them to locate the evidence, explain what it supports and identify a qualification. Observe where they mistake source provenance for review approval or assume a familiar publisher validates the whole answer. The product owner should decide what failure in that task blocks acceptance; a developer should not infer the threshold from a click-through rate.&lt;/p&gt;

&lt;p&gt;Mobile layouts deserve the same evidence identity checks. A compact popover may omit the source edition that appears in a desktop rail. A downloaded or copied answer may lose the marker entirely. Specify which export formats preserve traceability, then verify them. If a format cannot preserve it, the exported artifact should make that limitation apparent to its reader.&lt;/p&gt;

&lt;p&gt;Accessibility belongs in the acceptance scope too. A visual connection drawn by color alone does not tell every reader which claim is selected. Give markers descriptive names, support keyboard navigation and verify focus behavior when an evidence panel opens and closes. These are proposed product requirements that need testing in the actual implementation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Invalidate review when its inputs change
&lt;/h2&gt;

&lt;p&gt;A review decision should refer to an answer version, its evidence versions and the policy under which it was accepted. Changing any of those inputs can invalidate the decision. Avoid a permanent approved flag attached only to the conversation, because later answers can inherit approval for content nobody reviewed.&lt;/p&gt;

&lt;p&gt;For example, a reviewer accepts the synthetic lesson claim against edition A. An editor publishes edition B and moves the passage. Keep the old answer and its original evidence identity available under the applicable retention policy. Create a new review event for the changed source relationship. Do not silently rewrite the old receipt until it appears to describe edition B all along.&lt;/p&gt;

&lt;p&gt;Some changes require semantic review; others need narrower checks. A viewer-coordinate correction with unchanged passage content may need locator verification. A changed qualification requires reassessment of support. A new intended-use policy requires reassessment of permission even if the text is identical. Write these invalidation rules down so routine maintenance has predictable scope.&lt;/p&gt;

&lt;p&gt;The same rule applies to model and retrieval changes. Passing the contract tests on a new configuration establishes only the tested behavior for that configuration and fixture set. Preserve the model identifier and relevant retrieval settings with the run. If a provider cannot guarantee identical future behavior behind an identifier, record that reproducibility limit rather than promising exact replay.&lt;/p&gt;

&lt;h2&gt;
  
  
  Budget for the review work the schema cannot perform
&lt;/h2&gt;

&lt;p&gt;Mechanical checks are cheap to repeat once the representation is stable. Semantic review has a different cost and requires an agreed rubric. An automated verifier can help prioritize disagreements, but its output needs evaluation against the domain reviewers' judgments. Agreement on familiar examples does not establish reliability on unsupported, ambiguous or contradictory ones.&lt;/p&gt;

&lt;p&gt;Assign ownership at the point where a decision can become stuck. The ingestion owner resolves missing context. The product owner decides whether the intended user can complete the evidence task. The domain reviewer judges whether the claim is supported within its scope. Engineering preserves those decisions and makes the software follow them. One undifferentiated review queue makes these responsibilities harder to see. Set an unresolved-case policy before measuring performance. If reviewers disagree about whether a passage supports a claim, keep that uncertainty in the fixture metadata. It can be a useful test of the escalation route. Forcing consensus merely to obtain a tidy score removes the scenario that needs the clearest product behavior.&lt;/p&gt;

&lt;p&gt;Measure operational burden as well as answer quality. Count cases awaiting review and record why they remain open. Track whether the evidence panel gives reviewers enough context to decide without searching an unrelated document store. Use those observations to improve the workflow; do not describe shorter handling time as proof of safer clinical decisions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Retain evidence without retaining everything
&lt;/h2&gt;

&lt;p&gt;An auditable answer does not require copying every input into an unrestricted log. Decide which records belong in the evidence store, which belong in a restricted operational system and which should not be retained. The educational fixture can be public because it contains invented learning content. A real request may carry information that changes those decisions.&lt;/p&gt;

&lt;p&gt;Give the receipt stable references to protected records where appropriate. Apply access checks when the evidence panel resolves them. A citation should not become an alternate route around the permissions that protect the underlying document.&lt;/p&gt;

&lt;p&gt;Test this with an authorized reader and a reader who cannot access the same source; the second reader must not receive the passage through a cached answer.&lt;/p&gt;

&lt;p&gt;Retention also creates an acceptance edge case. If the supporting document is no longer available under the applicable policy, the product cannot honestly present the saved answer as fully inspectable. Preserve the reason for that limitation without inventing a replacement passage. The owner needs to decide whether the answer remains visible with a warning, becomes restricted or is removed through the established retention process.&lt;/p&gt;

&lt;p&gt;Agree on that behavior before promising reproducibility in a delivery contract. Exact replay may depend on source licenses, retained snapshots and provider behavior outside the application's control. A useful receipt states what can still be verified today and what was verified at the original review. Those are different claims, and a buyer should be able to distinguish them without investigating your storage architecture.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put the evidence contract in the delivery packet
&lt;/h2&gt;

&lt;p&gt;For a healthcare AI development proposal, request the schema, the source-version policy and the fixture matrix together. Require a sample answer receipt that can be followed from the rendered claim to its source passage. Ask for both a passing control and a failure that remains visible to the user. Those artifacts make an implementation scope concrete enough to inspect.&lt;/p&gt;

&lt;p&gt;The packet should also name what has not been demonstrated. A clickable prototype, a working ingestion job and a tested retrieval path are different deliverables. Record which one exists, which sources it uses and who accepted its limits. That gives the buyer a basis for deciding what the next development milestone must prove.&lt;/p&gt;

&lt;p&gt;Before commissioning the next phase, bring an authorized sample corpus, a representative learning task and a named review owner to the technical discussion. Use the case linked above to examine the discovery work, then use this contract to ask how the implementation will preserve it. A credible proposal should identify the first claim it will prove, the fixture that can break it and the person responsible for resolving the failure.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://hackmd.io/@dmytro-nasyrov/rag-citation-audit-claim-source-evidence" rel="noopener noreferrer"&gt;Make RAG Citations Auditable, One Claim at a Time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pharosengineeringnotes.wordpress.com/2026/09/19/choose-rag-supplier-retrieval-evaluation-evidence/" rel="noopener noreferrer"&gt;How to Choose a RAG Supplier by Retrieval Evaluation Evidence&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://pharosengineeringnotes.wordpress.com/2026/09/18/rag-vendor-retrieval-evaluation-deliverables/" rel="noopener noreferrer"&gt;How to Specify Retrieval Evaluation Deliverables for a RAG Vendor&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.substack.com/p/enterprise-rag-procurement-brief" rel="noopener noreferrer"&gt;How to Write an Enterprise RAG Procurement Brief&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.substack.com/p/choose-rag-development-partner-before-paid-pilot" rel="noopener noreferrer"&gt;How to Choose a RAG Development Partner Before a Paid Pilot&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feaenhy6t8vj9yop1fx91.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Feaenhy6t8vj9yop1fx91.jpg" width="112" height="112" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>rag</category>
      <category>testing</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Smart Contract Engineering in 2026: Development, Audits, Upgrades and Release Evidence</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Tue, 22 Sep 2026 21:52:33 +0000</pubDate>
      <link>https://dev.to/pharos_production/smart-contract-engineering-in-2026-development-audits-upgrades-and-release-evidence-27jn</link>
      <guid>https://dev.to/pharos_production/smart-contract-engineering-in-2026-development-audits-upgrades-and-release-evidence-27jn</guid>
      <description>&lt;p&gt;A smart contract can pass its tests, receive an audit and still reach production with the wrong upgrade authority or an unreviewed initialization call. Each document may be accurate while describing a different version of the system. The release decision fails at the joins.&lt;/p&gt;

&lt;p&gt;Smart contract engineering in 2026 needs a traceable connection between intended behavior, reviewed source, executable artifacts, administrative powers and observed deployment. This guide provides that connection: a route through six published technical articles, an evidence acceptance table and a release receipt you can adapt to your repository. The scope is Solidity and EVM systems. The year identifies this reading route, checked on September 23, 2026. It does not imply an industry survey or a universal security standard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choose the guide that answers your release question
&lt;/h2&gt;

&lt;p&gt;Start with the decision you cannot currently defend. The following articles each contain a different working artifact. Reading them in publication order is unnecessary.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Your question&lt;/th&gt;
&lt;th&gt;Guide and its specific job&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;What should a repository handoff contain?&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/pharos_production/how-to-choose-a-smart-contract-development-company-8-repository-checks-9m"&gt;Smart Contract Release Audit: 8 Repository Artifacts&lt;/a&gt; identifies the records to request before accepting delivery.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do automated checks block a bad candidate?&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/pharos_production/smart-contract-cicd-foundry-slither-and-audit-gates-4ael"&gt;Smart Contract CI/CD: Foundry, Slither, and Audit Gates&lt;/a&gt; connects build and analysis jobs to release gates.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Who can change the deployed system?&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/pharos_production/smart-contract-upgrade-authority-timelocks-multisigs-and-emergency-controls-p6o"&gt;Smart Contract Upgrade Authority: Timelocks, Multisigs, and Emergency Controls&lt;/a&gt; maps normal and emergency administrative paths.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;What happens if the new implementation fails?&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/pharos_production/how-to-rehearse-smart-contract-rollback-before-calling-a-system-upgradeable-3532"&gt;How to Rehearse Smart-Contract Rollback Before Calling a System Upgradeable&lt;/a&gt; defines recovery exercises and the limits of code reversal.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How should I evaluate a development partner?&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/pharos_production/10-smart-contract-development-companies-compared-by-repository-and-release-evidence-in-2026-m59"&gt;10 Smart Contract Development Companies Compared by Repository and Release Evidence in 2026&lt;/a&gt; separates public evidence from project-specific delivery questions.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;How do I test behavior across an upgrade?&lt;/td&gt;
&lt;td&gt;
&lt;a href="https://dev.to/pharos_production/property-based-testing-for-upgradeable-smart-contracts-a-stateful-invariant-harness-45j3"&gt;Property-Based Testing for Upgradeable Smart Contracts: A Stateful Invariant Harness&lt;/a&gt; supplies a bounded teaching harness with handlers, ghost state and a deliberately broken upgrade.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use the vendor comparison during selection, then move to repository acceptance when a team delivers your code. Public engineering activity can support a shortlist. It cannot establish that the people assigned to your project reviewed its economics, reproduced its build or rehearsed its deployment.&lt;/p&gt;

&lt;p&gt;For a delivery organization offering &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;blockchain software development&lt;/a&gt;, such as Pharos Production, this distinction matters at the handoff: service scope identifies the work offered, while the candidate's artifacts establish what was actually completed. Apply the same acceptance questions to an internal team and an external supplier.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give every piece of evidence a subject
&lt;/h2&gt;

&lt;p&gt;A filename such as &lt;code&gt;final-audit.pdf&lt;/code&gt; is insufficient identification. Define the release subject using the repository and full commit, build configuration, dependency revisions, target chain and intended contract addresses. An upgrade also needs the currently deployed implementation and the candidate implementation. Keep intended values separate from observations read from the network.&lt;/p&gt;

&lt;p&gt;The identity must extend beyond Solidity files. A deployment script can pass the wrong administrator without changing the implementation. Compiler settings can alter bytecode. An oracle address can change system behavior while every repository test remains unchanged. Include these inputs in the candidate and explain which checks cover them.&lt;/p&gt;

&lt;p&gt;The following acceptance table is a proposed engineering policy. Adapt its owners and stop conditions to the actual system. It is not a report of completed checks.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Evidence&lt;/th&gt;
&lt;th&gt;Acceptance question&lt;/th&gt;
&lt;th&gt;What invalidates it?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Build record&lt;/td&gt;
&lt;td&gt;Can the recorded inputs reproduce the intended artifact?&lt;/td&gt;
&lt;td&gt;Source, compiler, dependencies or settings change.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Behavior results&lt;/td&gt;
&lt;td&gt;Were the relevant properties exercised under declared assumptions?&lt;/td&gt;
&lt;td&gt;Properties, handlers, configuration or relevant code change.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit disposition&lt;/td&gt;
&lt;td&gt;Is the candidate inside the reviewed boundary?&lt;/td&gt;
&lt;td&gt;An unclassified change crosses that boundary.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Authority snapshot&lt;/td&gt;
&lt;td&gt;Do the actual accounts and roles match the approved model?&lt;/td&gt;
&lt;td&gt;Owners, thresholds, modules, roles or delays change.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Recovery rehearsal&lt;/td&gt;
&lt;td&gt;Does the procedure preserve the required state and access?&lt;/td&gt;
&lt;td&gt;Migration logic, starting state or recovery powers change.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment observation&lt;/td&gt;
&lt;td&gt;Did the intended code and configuration reach this chain?&lt;/td&gt;
&lt;td&gt;Wrong target, bytecode mismatch or insufficient confirmation.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Operations handoff&lt;/td&gt;
&lt;td&gt;Can an assigned operator detect and handle the specified failure?&lt;/td&gt;
&lt;td&gt;The monitored system or response ownership changes.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgxf5qk87k5pkw54fn1oe.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgxf5qk87k5pkw54fn1oe.png" alt="Release evidence diagram: candidate build, behavior tests, audit and recovery records meet observed chain code and authority at one identity check; a mismatch blocks release, and changes invalidate affected evidence." width="800" height="480"&gt;&lt;/a&gt;&lt;br&gt;
&lt;small&gt;Release evidence joins candidate records to a chain observation. A changed input reopens its affected checks.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;Do not replace this table with a weighted readiness score. A strong test suite cannot compensate for an unknown upgrade administrator. A documented exception needs its own scope, reason and authorized owner. An arithmetic average cannot accept it on their behalf.&lt;/p&gt;
&lt;h2&gt;
  
  
  Develop the behavior contract before the implementation
&lt;/h2&gt;

&lt;p&gt;Write properties in business terms first. Which balances represent customer claims? When is a request irrevocably accepted? Who may mint, withdraw, cancel or change a limit? Which external data must be fresh? These answers define observable obligations that an implementation and its tests can share.&lt;/p&gt;

&lt;p&gt;Consider a withdrawal queue. A useful specification distinguishes a requested withdrawal from a claimable one and a paid one. It says whether pausing new requests must preserve existing claims. It defines how rounding is allocated and whether a later parameter change applies to pending requests. Without these decisions, two correct-looking functions can disagree about the same liability.&lt;/p&gt;

&lt;p&gt;Translate those obligations into contract boundaries, explicit permissions and testable state transitions. Keep an inventory of external assumptions: token transfer behavior, oracle updates, keeper availability and chain-specific execution conditions. An assumption belongs beside the property that depends on it, so a reviewer can see what a passing assertion leaves unresolved.&lt;/p&gt;

&lt;p&gt;Pin the compiler and dependencies, and retain the settings used for the release. Check the selected compiler against Solidity's &lt;a href="https://docs.soliditylang.org/en/latest/bugs.html" rel="noopener noreferrer"&gt;known-bug list&lt;/a&gt;, including the conditions under which a listed bug applies. A compiler version number alone cannot answer whether your settings and code encounter the issue. Record the review outcome with the build evidence.&lt;/p&gt;

&lt;p&gt;Upgradeable contracts add initialization obligations. OpenZeppelin states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Do not leave an implementation contract uninitialized.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OpenZeppelin, &lt;a href="https://docs.openzeppelin.com/upgrades-plugins/writing-upgradeable#initializing-the-implementation-contract" rel="noopener noreferrer"&gt;Writing Upgradeable Contracts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The same guide describes locking the implementation with &lt;code&gt;_disableInitializers&lt;/code&gt; and treating proxy initialization separately. Turn that distinction into explicit checks for each deployed address. The intended owner, initialized version and repeat-call behavior belong in the deployment review, not in an operator's memory.&lt;/p&gt;
&lt;h2&gt;
  
  
  Read test results as bounded evidence
&lt;/h2&gt;

&lt;p&gt;A unit test checks a particular behavior. Fuzzing explores inputs within its configured domain. Stateful invariant testing explores sequences and asks whether a property survives them. These methods answer related questions, but their results should retain enough context to distinguish what actually ran.&lt;/p&gt;

&lt;p&gt;For an upgrade, include behavior before the transition, the transition itself and behavior afterward. A fresh deployment of version two can pass every test while a populated version-one proxy fails during migration. Likewise, a successful upgrade transaction says little about whether old withdrawal claims remain redeemable.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://getfoundry.sh/forge/invariant-testing" rel="noopener noreferrer"&gt;Foundry invariant-testing documentation&lt;/a&gt; explains sequence campaigns, handlers and configuration. Retain the tool version, selected profile, targeted operations, campaign bounds and failure reproduction data. Inspect successful calls and reverts as well as the final result. A handler that filters every difficult input may make an invariant easy to satisfy without exercising its intended risk.&lt;/p&gt;

&lt;p&gt;Use a negative control when the property is important enough to gate release. Introduce a deliberate defect in an isolated test fixture and verify that the relevant assertion detects it. For example, a migration that resets an aggregate should fail the conservation property that compares aggregate and per-account obligations. Record that result as evidence about the test's sensitivity, not as proof that all migration defects are covered.&lt;/p&gt;

&lt;p&gt;The linked stateful harness is a teaching example, not a production asset protocol. Adapting its structure requires replacing the model, actions and assumptions with your system's behavior. Copying its campaign settings or green result does not transfer its evidence to your release.&lt;/p&gt;

&lt;p&gt;Keep static analysis alongside these tests. Preserve findings, reviewed suppressions and the analysis configuration. When a result disappears, determine whether code was fixed, coverage changed or a detector was disabled. The release owner needs the disposition, not just an empty findings panel.&lt;/p&gt;

&lt;p&gt;Assign a question to every retained result. A conservation test might compare claims against assets under a defined token model. An authorization test should show that a forbidden actor cannot reach the protected state change. A liveness exercise should demonstrate progress under its stated assumptions about keepers and available liquidity. These are different obligations, so combining their totals into one test count hides information the reviewer needs.&lt;/p&gt;

&lt;p&gt;When a test uses a fork, distinguish captured state from simulated future behavior. Record the fork block, dependency addresses and any substituted responses. A simulated oracle update or signer impersonation can be useful for isolating contract behavior, but it removes part of the operational problem. List that substitution so the release decision can require separate evidence for the real dependency or signing procedure.&lt;/p&gt;
&lt;h2&gt;
  
  
  Bind the audit to the release candidate
&lt;/h2&gt;

&lt;p&gt;A smart contract audit has a scope: repository, revision, files, assumptions, exclusions and review method. Ask for those identifiers before treating its conclusions as relevant to a pending deployment. A report about the project can be authentic and still omit the migration you intend to execute tomorrow.&lt;/p&gt;

&lt;p&gt;Create a finding-to-fix record. Each relevant finding should point to the remediation change, a regression check where appropriate, the follow-up review and any residual limitation. A team marking its own ticket closed is different evidence from the reviewer confirming the fix. Preserve that difference in the receipt.&lt;/p&gt;

&lt;p&gt;Compare the audited revision with the candidate. Classify changes by affected behavior instead of accepting them because the diff is small. One address replacement can redirect authority. A single arithmetic adjustment can change claims. Documentation-only changes may need no new security review, but the classification itself should be visible and attributable.&lt;/p&gt;

&lt;p&gt;This is also where delivery scope must become concrete. When development and audit handoffs lose the relationship between fixes and the deployable candidate, Pharos Production's &lt;a href="https://pharosproduction.com/services/smart-contracts-development/" rel="noopener noreferrer"&gt;smart contract development and audit preparation&lt;/a&gt; provides a relevant service scope spanning implementation, testing and security-review preparation. The acceptance record should still require evidence from the particular engagement. The service description does not establish an independent audit result.&lt;/p&gt;

&lt;p&gt;Record accepted risks with the condition that made acceptance reasonable. If a limitation was accepted because withdrawals were capped, raising the cap should reopen the decision. Otherwise, a valid historical exception becomes an undocumented expansion of risk.&lt;/p&gt;
&lt;h2&gt;
  
  
  Review the complete upgrade authority path
&lt;/h2&gt;

&lt;p&gt;Start at the function that can replace the implementation and work outward to every account or contract that can authorize it. Include powers that can change those permissions. A diagram showing a multisig at the top is incomplete if another role can replace the controlling administrator.&lt;/p&gt;

&lt;p&gt;OpenZeppelin's &lt;a href="https://docs.openzeppelin.com/contracts/5.x/access-control" rel="noopener noreferrer"&gt;access-control documentation&lt;/a&gt; describes ownership, role-based controls and timelocks. A threshold wallet distributes approval among signers. A timelock introduces delay for scheduled operations. Neither mechanism, by itself, establishes that the approved payload preserves user obligations. The control model must describe proposal, execution, cancellation and administration together.&lt;/p&gt;

&lt;p&gt;Record the current threshold, owners, relevant modules and the roles on each controlled contract. Separate accounts allowed to propose a change from those able to execute or cancel it. Include changes to the delay itself. Read these values from the intended network at a recorded block. Deployment notes may describe an earlier configuration.&lt;/p&gt;

&lt;p&gt;Then examine emergency powers. A pause role may block deposits while withdrawals remain available, or it may freeze both. An emergency upgrade path may bypass the ordinary delay. State the consequence explicitly and test unauthorized calls through each reachable entry point. A bypass justified for an incident remains a live administrative capability afterward.&lt;/p&gt;

&lt;p&gt;Rehearse absence as well as compromise. Determine whether the required signers can actually act, whether a cancellation can arrive in time and who owns the response when one signer is unavailable. Do not choose a universal delay from a blog post. Its usefulness depends on detection, decision time, transaction inclusion and the actions users can take during the window.&lt;/p&gt;
&lt;h2&gt;
  
  
  Separate storage compatibility from recovery
&lt;/h2&gt;

&lt;p&gt;A compatible storage layout is necessary for many proxy upgrades, but it cannot establish that the old implementation understands state written by the new one. A field can retain its type and slot while changing units, meaning or permitted values. Recovery must account for those semantic changes.&lt;/p&gt;

&lt;p&gt;For example, suppose version one records a claim in whole units and version two migrates it into scaled units. Reverting the implementation pointer may succeed technically while causing version one to interpret the scaled value incorrectly. A rollback rehearsal must execute meaningful reads and transactions after reversal, rather than stopping at a successful administrative call.&lt;/p&gt;

&lt;p&gt;OpenZeppelin's &lt;a href="https://docs.openzeppelin.com/upgrades-plugins/foundry/api/upgrades" rel="noopener noreferrer"&gt;Foundry upgrade-validation API&lt;/a&gt; compares a new implementation against a declared reference. Preserve the reference identity and validation settings. A comparison against the wrong predecessor provides no useful answer about the proxy you operate.&lt;/p&gt;

&lt;p&gt;Define the recovery strategy before approving migration: return to the old code where its state assumptions still hold, deploy a reviewed corrective implementation, or follow a bounded migration procedure. Document conditions that make each option unavailable. A pause can buy time while retaining state, but it does not reverse transfers or restore a previous storage snapshot.&lt;/p&gt;

&lt;p&gt;Use representative populated state and retain the rehearsal's starting block or fixture. Include pending claims, active permissions and operations already in flight. Verify the obligations after recovery and the operator's ability to complete the next required action. If the procedure leaves the system permanently paused, explain how users regain access before calling the rehearsal successful.&lt;/p&gt;
&lt;h2&gt;
  
  
  Join the approved artifact to an observed deployment
&lt;/h2&gt;

&lt;p&gt;Before signing, compare the full intended operation with the reviewed one: chain, target, value, calldata and administrative path. Decode parameters for the reviewer while retaining their exact encoded form. A familiar function name is insufficient when its arguments choose a different implementation or initialization recipient.&lt;/p&gt;

&lt;p&gt;After execution, retain the transaction receipt and apply the confirmation or finality policy appropriate to the chain. Record the block number and block hash used for verification. A transaction accepted by a node is an earlier observation than a deployment accepted under that policy. Give these states separate names.&lt;/p&gt;

&lt;p&gt;Compare deployed runtime code with the expected deployment artifact, accounting explicitly for linked libraries, immutable values and compiler metadata where relevant. Do not silently strip differences to obtain a match. Preserve the comparison method so another engineer can reproduce the result from the same build inputs.&lt;/p&gt;

&lt;p&gt;For relevant proxy types, &lt;a href="https://eips.ethereum.org/EIPS/eip-1967" rel="noopener noreferrer"&gt;ERC-1967&lt;/a&gt; specifies implementation, beacon and administrator storage slots and associated events. Inspect the mechanism the system actually uses. A beacon proxy adds a dependency through the beacon. A nonstandard proxy requires its own documented inspection path. An explorer badge does not replace this identification work.&lt;/p&gt;

&lt;p&gt;Finally, check the initialized application state and authority configuration. Confirm the intended implementation, roles, limits and external dependencies. Where a check would create an irreversible effect, use a suitable simulation or a separately authorized bounded operation. Verification should not invent an unreviewed transaction merely to demonstrate that the deployment is alive.&lt;/p&gt;
&lt;h2&gt;
  
  
  Keep one receipt with explicit evidence states
&lt;/h2&gt;

&lt;p&gt;The following YAML is an unfilled template, not an executable validator or a completed release. Every check begins at &lt;code&gt;NOT_RUN&lt;/code&gt;. Null identifiers must be replaced with observed values. Copying the file cannot authorize a deployment.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;schema&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;release-evidence-example/v1&lt;/span&gt;
&lt;span class="na"&gt;candidate&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;repository&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;source_commit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;build_input_sha256&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;compiler_and_settings_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;dependency_lock_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="na"&gt;target&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;chain_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;proxy_address&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;current_implementation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;candidate_implementation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;reviewed_operation_sha256&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="na"&gt;evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NOT_RUN&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;artifact_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;null&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;behavior&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NOT_RUN&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;artifact_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;null&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;audit_scope_and_fixes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NOT_RUN&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;artifact_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;null&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;authority&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NOT_RUN&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;artifact_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;null&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;recovery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NOT_RUN&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;artifact_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;null&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;deployment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NOT_RUN&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;artifact_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;null&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
  &lt;span class="na"&gt;operations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;{&lt;/span&gt;&lt;span class="nv"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;NOT_RUN&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;artifact_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="nv"&gt;null&lt;/span&gt;&lt;span class="pi"&gt;}&lt;/span&gt;
&lt;span class="na"&gt;observation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;transaction_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;block_number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;block_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;finality_policy_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NOT_READY&lt;/span&gt;
  &lt;span class="na"&gt;reviewer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
  &lt;span class="na"&gt;accepted_exceptions_ref&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each referenced artifact should contain its subject identifiers, producer, observation time, result and limitations. Define a small state vocabulary: not run, passed, failed, invalidated and not applicable with a reason. Keep release authorization distinct from both a technical check and a transaction submission.&lt;/p&gt;

&lt;p&gt;Retain a pre-submission decision and a separate post-deployment acceptance record. Deployment observations cannot exist before deployment, so an all-fields-passed rule would be circular. The first decision authorizes an exact operation after the required preparatory evidence is accepted. The second confirms what happened and whether operational ownership can transfer.&lt;/p&gt;

&lt;p&gt;Hashing records helps detect changed bytes when a trusted comparison value exists. It does not prove that their claims are true or that their signer was authorized. Preserve review identity and artifact provenance alongside integrity values. Keep secrets and private signing material outside the evidence packet.&lt;/p&gt;

&lt;p&gt;Make artifact references durable enough for an incident investigation. A temporary CI download link may expire before the next upgrade. Retain the relevant build inputs, reports and replay instructions in an access-controlled archive with the release identifier. Test whether the receiving engineer can retrieve the packet using their own access, rather than relying on the original author's workstation or browser session.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat configuration changes as releases with their own scope
&lt;/h2&gt;

&lt;p&gt;A new implementation is only one way to alter a smart contract system. An oracle replacement, a fee adjustment or a role transfer can change user outcomes without changing runtime bytecode. Give such changes an exact subject and acceptance path instead of exempting them because no compiler ran.&lt;/p&gt;

&lt;p&gt;For a parameter update, record its old and proposed values, permitted bounds and affected obligations. Determine how it applies to already accepted work. A fee update may be valid for new requests while breaking the economics of a pending queue if applied retroactively. The required evidence should follow that behavior change. Rebuilding unchanged code is not a substitute for reviewing the parameter's effect.&lt;/p&gt;

&lt;p&gt;For a dependency replacement, establish the interface and failure behavior your application relies on. The same function signature does not guarantee equivalent units, freshness, permissions or revert behavior. Preserve the chosen address and the assumptions checked against it, then update monitoring to observe the replacement. Treat a change to those assumptions as a reason to revisit the dependent properties.&lt;/p&gt;

&lt;p&gt;For an authority transfer, verify that the recipient can exercise its intended role and that the previous principal has only the powers the plan allows. Some transitions require an acceptance step. The receipt should distinguish transfer requested from transfer completed and should identify the observation that resolves the distinction.&lt;/p&gt;

&lt;p&gt;Scope the work proportionately. A configuration-only release can reuse unchanged build evidence while requiring new parameter, authority or operational checks. A code upgrade may require the whole path. The useful question is which dependency changed and which conclusion relied on it. Write that relationship down before selecting the checks, then preserve the rejected and superseded records so the history explains why a new decision was necessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walk a candidate through rejection and acceptance
&lt;/h2&gt;

&lt;p&gt;Imagine a hypothetical upgrade with passing behavior tests and an audit covering its source commit. During deployment preparation, the operator changes the migration recipient. The source tests remain green, but the reviewed operation no longer matches. The receipt should invalidate the operation review and any rehearsal dependent on that recipient.&lt;/p&gt;

&lt;p&gt;The fix is to review the changed parameter, rerun affected checks and issue a new decision for the exact payload. Unaffected evidence can remain valid if its dependencies are unchanged and that conclusion is recorded. This avoids both careless reuse and an expensive ritual of rerunning everything without asking what changed.&lt;/p&gt;

&lt;p&gt;Now suppose deployment succeeds but the observed administrator differs from the approved configuration. Record the actual state and keep operational acceptance blocked. Diagnose the mismatch and use the authorized incident or correction procedure. Never edit the expected value after execution merely to make the comparison pass.&lt;/p&gt;

&lt;p&gt;Operations should receive alerts tied to the same model: implementation or role changes, violated accounting properties, stale dependencies and failures of required off-chain services. Every alert needs a destination, an accountable responder and an available action. A notification without authority to respond leaves the handoff unfinished.&lt;/p&gt;

&lt;p&gt;The release is ready for operational acceptance when the approved candidate, observed chain state and assigned response process agree within their documented limits. Keep the receipt with the release, reopen affected decisions when inputs change, and use the linked specialist guide for the next missing piece of evidence.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bulbapp.io/p/e2580ef7-324b-442d-b818-92b8c8442b3d/five-smart-contract-upgrade-patterns-compared" rel="noopener noreferrer"&gt;Five Smart Contract Upgrade Patterns Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.medium.com/upgradeable-solidity-smart-contracts-part-1-versioning-7e6e97cafc28" rel="noopener noreferrer"&gt;Upgradeable Solidity Smart Contracts. Part 1 — Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/web3-smart-contracts-oracles-part-1-3905b127c01d" rel="noopener noreferrer"&gt;Web3. Smart Contracts. Oracles. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-2-40942c055d79" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-1-222fe44ee14c" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3y3gv5cvg1m0yfn97ofb.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3y3gv5cvg1m0yfn97ofb.jpg" width="112" height="112" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;br&gt;
&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>web3</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Property-Based Testing for Upgradeable Smart Contracts: A Stateful Invariant Harness</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Wed, 16 Sep 2026 07:04:05 +0000</pubDate>
      <link>https://dev.to/pharos_production/property-based-testing-for-upgradeable-smart-contracts-a-stateful-invariant-harness-45j3</link>
      <guid>https://dev.to/pharos_production/property-based-testing-for-upgradeable-smart-contracts-a-stateful-invariant-harness-45j3</guid>
      <description>&lt;p&gt;&lt;small&gt;Cover illustration: independent state checking across a change of contract logic.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;An upgrade test can pass while the upgraded contract has already lost its accounting history. The test may check that the implementation address changed, that a new method returns the expected value, or that deployment completed. None of those checks establishes that earlier users still have the same rights after the transition.&lt;/p&gt;

&lt;p&gt;A stateful invariant harness makes a stronger, bounded claim: after each generated action, the contract must still agree with an independently maintained model. The upgrade itself becomes one action in that sequence. Grants, spending, pauses and rejected calls happen on both sides of it.&lt;/p&gt;

&lt;p&gt;The example below runs against an actual UUPS proxy. Its correct candidate passed a seeded campaign of 128 sequences with 64 handler calls each. A deliberately broken migration failed, and Foundry reduced its failing sequence to &lt;code&gt;pause(true)&lt;/code&gt; followed by &lt;code&gt;upgrade()&lt;/code&gt;. These are results from a teaching fixture, not evidence that an unrelated protocol is safe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Define the state that must survive
&lt;/h2&gt;

&lt;p&gt;Consider a small credit ledger. An administrator grants integer credits to registered actors. Each actor can spend its own credits. Pausing blocks grants and spending. Version two adds a migration marker while retaining existing balances and ownership.&lt;/p&gt;

&lt;p&gt;There are no deposits, token transfers, exchange rates or redeemable assets. Removing those features keeps the example focused on upgrade continuity. A vault handling money needs additional properties for custody, withdrawal behavior and external calls; copying this ledger would not provide them.&lt;/p&gt;

&lt;p&gt;For an upgrade project, the acceptance scope of &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;Pharos Production blockchain development services&lt;/a&gt; should specify which existing rights must survive a release. A statement that the proxy is upgradeable leaves that question open. The useful deliverable is a property that a reviewer can run against the proposed implementation.&lt;/p&gt;

&lt;p&gt;Write the properties before constructing the handler. Otherwise, it is easy to model whatever the implementation happens to do and call that behavior correct.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Independent expectation&lt;/th&gt;
&lt;th&gt;Where the example checks it&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Historical credits survive&lt;/td&gt;
&lt;td&gt;Each tracked actor retains credits minus successful spending&lt;/td&gt;
&lt;td&gt;Model invariant after generated calls&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Accounting stays consistent&lt;/td&gt;
&lt;td&gt;Contract total equals modeled actor balances and the model accumulator&lt;/td&gt;
&lt;td&gt;Same invariant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Upgrade authority stays fixed&lt;/td&gt;
&lt;td&gt;Only the handler acting as administrator can upgrade&lt;/td&gt;
&lt;td&gt;Rejection probe and owner comparison&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pause behavior survives&lt;/td&gt;
&lt;td&gt;A paused actor cannot spend; resuming permits the selected lifecycle to continue&lt;/td&gt;
&lt;td&gt;Exact revert check and deterministic lifecycle&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Initialization cannot repeat&lt;/td&gt;
&lt;td&gt;Proxy and implementation initialization reject a second or direct attempt&lt;/td&gt;
&lt;td&gt;Rejection probe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Candidate activation is observable&lt;/td&gt;
&lt;td&gt;Implementation slot, version and migration marker match the intended state&lt;/td&gt;
&lt;td&gt;Same invariant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Failed migration is atomic&lt;/td&gt;
&lt;td&gt;An unpaused migration attempt leaves the original observable state intact&lt;/td&gt;
&lt;td&gt;Separate deterministic test&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The third column matters. A property named in a document but absent from executable assertions is still an assumption. Conversely, a test that checks only a total can miss a redistribution between users. The example compares individual accounts as well as the aggregate.&lt;/p&gt;

&lt;p&gt;A migration does not always preserve every value literally. A system might replace one unit with another or split an account into separate records. In that case, define a compatibility relation before testing: which economic entitlement stays equivalent, which metadata may change and what conversion rule determines the expected result. Equality is appropriate for this fixture because its upgrade promises no accounting conversion.&lt;/p&gt;

&lt;p&gt;Write down who can change each expected value. Spending changes one actor and the aggregate. A grant changes the recipient and the aggregate. Pausing changes only the operational flag. The intended upgrade changes implementation metadata and the migration marker. This compact transition contract helps reviewers notice an assertion that accidentally permits migration to rewrite unrelated balances.&lt;/p&gt;

&lt;p&gt;The model can still be wrong. Adding the same mistaken fee formula to both implementation and handler would produce agreement without correctness. Review the expected transition against product requirements, accounting rules and representative examples before relying on randomized exploration. Independence is a design choice, not a consequence of storing variables in a different contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Pin a small, reproducible environment
&lt;/h2&gt;

&lt;p&gt;This run used Forge 1.5.1, Solidity 0.8.28, forge-std v1.9.7 and OpenZeppelin Contracts v5.4.0. Those are reproduction pins, not a claim that they are the newest versions available. The EVM target is Cancun and the optimizer uses 200 runs.&lt;/p&gt;

&lt;p&gt;Start in a new directory. The dependency revisions below are the exact commits used for the example. Install Forge 1.5.1 through the official Foundry distribution before running the commands. No RPC endpoint, wallet or funded account is required.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;upgrade-invariants
&lt;span class="nb"&gt;cd &lt;/span&gt;upgrade-invariants
&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; src &lt;span class="nb"&gt;test &lt;/span&gt;lib
git clone &lt;span class="nt"&gt;--branch&lt;/span&gt; v1.9.7 &lt;span class="nt"&gt;--depth&lt;/span&gt; 1   https://github.com/foundry-rs/forge-std.git lib/forge-std
git clone &lt;span class="nt"&gt;--branch&lt;/span&gt; v5.4.0 &lt;span class="nt"&gt;--depth&lt;/span&gt; 1   https://github.com/OpenZeppelin/openzeppelin-contracts.git lib/openzeppelin-contracts
&lt;span class="c"&gt;# Verify dependency revisions:&lt;/span&gt;
git &lt;span class="nt"&gt;-C&lt;/span&gt; lib/forge-std rev-parse HEAD
&lt;span class="c"&gt;# 77041d2ce690e692d6e03cc812b57d1ddaa4d505&lt;/span&gt;
git &lt;span class="nt"&gt;-C&lt;/span&gt; lib/openzeppelin-contracts rev-parse HEAD
&lt;span class="c"&gt;# c64a1edb67b6e3f4a15cca8909c9482ad33a02b0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save this as &lt;code&gt;foundry.toml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight toml"&gt;&lt;code&gt;&lt;span class="nn"&gt;[profile.default]&lt;/span&gt;
&lt;span class="py"&gt;src&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"src"&lt;/span&gt;
&lt;span class="py"&gt;test&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"test"&lt;/span&gt;
&lt;span class="py"&gt;libs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s"&gt;"lib"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="py"&gt;solc_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"0.8.28"&lt;/span&gt;
&lt;span class="py"&gt;evm_version&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"cancun"&lt;/span&gt;
&lt;span class="py"&gt;optimizer&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;optimizer_runs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;
&lt;span class="py"&gt;remappings&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="py"&gt;["forge-std/&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;lib/forge-std/src/&lt;/span&gt;&lt;span class="s"&gt;", "&lt;/span&gt;&lt;span class="py"&gt;@openzeppelin/contracts/&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="err"&gt;lib/openzeppelin-contracts/contracts/&lt;/span&gt;&lt;span class="s"&gt;"]&lt;/span&gt;&lt;span class="err"&gt;
&lt;/span&gt;
&lt;span class="nn"&gt;[profile.default.invariant]&lt;/span&gt;
&lt;span class="py"&gt;runs&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;128&lt;/span&gt;
&lt;span class="py"&gt;depth&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;64&lt;/span&gt;
&lt;span class="py"&gt;fail_on_revert&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="py"&gt;show_metrics&lt;/span&gt; &lt;span class="p"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The configuration makes unexpected handler reverts fail the campaign. Expected denials are asserted inside the handler, so they do not appear as failed handler calls. That distinction will explain the zero-revert metric later.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://getfoundry.sh/forge/invariant-testing?highlight=invariant" rel="noopener noreferrer"&gt;official Foundry invariant-testing guide&lt;/a&gt; documents handler targeting and ghost variables. This article applies those mechanisms to one specific upgrade transition. It does not require increasing a fuzz budget until an attractive result appears.&lt;/p&gt;

&lt;h2&gt;
  
  
  Keep the implementation intentionally small
&lt;/h2&gt;

&lt;p&gt;Save the following as &lt;code&gt;src/Ledger.sol&lt;/code&gt;. The first implementation stores credits and their total. The second adds one field and a versioned migration. The final contract is an intentionally defective candidate used only to check that the test can fail.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {UUPSUpgradeable} from "@openzeppelin/contracts/proxy/utils/UUPSUpgradeable.sol";

// Teaching fixture: credits are integers, not redeemable assets.
contract LedgerV1 is Initializable, UUPSUpgradeable {
    address public owner;
    bool public paused;
    mapping(address =&amp;gt; uint256) public credit;
    uint256 public total;
    error Unauthorized();
    error Paused();
    error Insufficient();
    error MustPause();

    constructor() { _disableInitializers(); }
    modifier onlyOwner() {
        if (msg.sender != owner) revert Unauthorized();
        _;
    }
    function initialize(address admin) external initializer { owner = admin; }
    function setPaused(bool value) external onlyOwner { paused = value; }
    function grant(address user, uint256 amount) external onlyOwner {
        if (paused) revert Paused();
        credit[user] += amount;
        total += amount;
    }
    function spend(uint256 amount) external {
        if (paused) revert Paused();
        if (credit[msg.sender] &amp;lt; amount) revert Insufficient();
        credit[msg.sender] -= amount;
        total -= amount;
    }
    function version() external pure virtual returns (uint256) { return 1; }
    function _authorizeUpgrade(address) internal override onlyOwner {}
}

contract LedgerV2 is LedgerV1 {
    uint256 public migrationMarker;
    function initializeV2() public virtual reinitializer(2) onlyOwner {
        if (!paused) revert MustPause();
        migrationMarker = 2;
    }
    function version() external pure override returns (uint256) { return 2; }
}

// Deliberate negative control. Never deploy this candidate.
contract BrokenV2 is LedgerV2 {
    function initializeV2() public override reinitializer(2) onlyOwner {
        if (!paused) revert MustPause();
        migrationMarker = 2;
        total = 0;
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proxy receives encoded initializer data during construction. Its owner becomes the handler, which will represent the authorized administrator. Each implementation locks its own initializer in its constructor. These are different storage contexts: initializing the proxy does not make direct initialization of an implementation a useful or safe operation.&lt;/p&gt;

&lt;p&gt;OpenZeppelin gives the relevant warning directly:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Do not leave an implementation contract uninitialized.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;OpenZeppelin, &lt;a href="https://docs.openzeppelin.com/upgrades-plugins/writing-upgradeable#initializing-the-implementation-contract" rel="noopener noreferrer"&gt;Writing Upgradeable Contracts&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The example inherits the pinned UUPS implementation and defines its authorization hook with &lt;code&gt;onlyOwner&lt;/code&gt;. Inspect the &lt;a href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v5.4.0/contracts/proxy/utils/UUPSUpgradeable.sol" rel="noopener noreferrer"&gt;v5.4.0 UUPS source&lt;/a&gt; when reproducing this behavior with a different dependency version. A method named &lt;code&gt;upgradeToAndCall&lt;/code&gt; alone does not establish who may execute it.&lt;/p&gt;

&lt;p&gt;Version two deliberately preserves the base declaration order and appends its marker. This is a simple fixture, not a general storage-layout approval procedure. Real changes involving inheritance, packed values or namespaced storage require their own compatibility analysis.&lt;/p&gt;

&lt;p&gt;The pause requirement belongs to &lt;code&gt;initializeV2&lt;/code&gt;, not to every possible upgrade call. An authorized owner could choose another candidate or omit migration data. The positive campaign models the intended release procedure; it does not prove that a malicious administrator cannot bypass that procedure. If pausing before every upgrade is a production requirement, encode and test that requirement at the authorization boundary.&lt;/p&gt;

&lt;p&gt;The broken candidate resets &lt;code&gt;total&lt;/code&gt; without changing account credits. It is intentionally obvious. Its purpose is to establish that the oracle notices a state discontinuity at migration time, before anyone needs to spend afterward.&lt;/p&gt;

&lt;h2&gt;
  
  
  Model history outside the proxy
&lt;/h2&gt;

&lt;p&gt;The handler keeps a separate expected balance for each of three actors. It also maintains an expected total, pause state and implementation address. None of those expectations is copied from the proxy after an upgrade.&lt;/p&gt;

&lt;p&gt;That separation is the heart of the test. If the handler read the migrated total and adopted it as the new expected value, the broken migration would teach the test to accept its own corruption. The model must describe the intended effect of an accepted action, not merely repeat the result returned by the system.&lt;/p&gt;

&lt;p&gt;Seed each actor with 100 credits before fuzzing. An empty ledger would make a reset-to-zero mutation invisible until later activity created a difference. A small nonzero history makes this defect observable even when the generated sequence contains only a pause and an upgrade.&lt;/p&gt;

&lt;p&gt;The actor set is deliberately closed. Grants target only these addresses, which makes their sum meaningful. In a real protocol, maintain a registry of every account the campaign can credit or debit, including fee recipients and custody contracts. Otherwise, an aggregate comparison can exclude balances that the test created itself.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;bound&lt;/code&gt; limits grants to positive amounts and successful spending to the actor's modeled balance. It helps the campaign reach useful states, but it also narrows the tested domain. Overspending, zero-value behavior and arithmetic extremes are outside that success path and deserve targeted tests when they matter to the actual contract.&lt;/p&gt;

&lt;p&gt;The model changes only after a successful call. A paused spending attempt expects the precise &lt;code&gt;Paused&lt;/code&gt; error and leaves expected balances unchanged. Catching every exception and continuing would obscure both an unexpected denial and a defect in the handler.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F54gkm7i5d58ratjn2qnh.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F54gkm7i5d58ratjn2qnh.png" alt="Stateful invariant harness with a fuzzer feeding five handler actions, a proxy whose implementation changes from V1 to V2, and an independent model compared after every action." width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;The handler drives the proxy. The model records intended effects independently; assertions compare the two after each generated action.&lt;/small&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the allowed actions explicit
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;test/UpgradeInvariant.t.sol&lt;/code&gt; with the next two code blocks in order. The first contains its imports and the handler. The second contains the test contract in the same file.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// SPDX-License-Identifier: MIT
pragma solidity 0.8.28;

import {Test} from "forge-std/Test.sol";
import {StdInvariant} from "forge-std/StdInvariant.sol";
import {Initializable} from "@openzeppelin/contracts/proxy/utils/Initializable.sol";
import {ERC1967Proxy} from "@openzeppelin/contracts/proxy/ERC1967/ERC1967Proxy.sol";
import {LedgerV1, LedgerV2, BrokenV2} from "../src/Ledger.sol";

contract Handler is Test {
    LedgerV1 public ledger;
    address public immutable first;
    address public immutable next;
    address public expectedImplementation;
    uint256[3] public expected;
    uint256 public expectedTotal;
    bool public expectedPaused;
    bool public upgraded;
    uint256 public grants;
    uint256 public spends;
    uint256 public blockedSpends;
    uint256 public probes;
    uint256 public upgrades;
    uint256 public postUpgradeSpends;

    constructor(address candidate) {
        next = candidate;
        first = address(new LedgerV1());
        ledger = LedgerV1(address(new ERC1967Proxy(
            first, abi.encodeCall(LedgerV1.initialize, (address(this)))
        )));
        expectedImplementation = first;
        for (uint256 i; i &amp;lt; 3; ++i) {
            ledger.grant(actor(i), 100);
            expected[i] = 100;
            expectedTotal += 100;
        }
    }

    function actor(uint256 i) public pure returns (address) {
        return address(uint160(0x100 + i));
    }

    function grant(uint256 who, uint256 raw) external {
        uint256 i = who % 3;
        uint256 amount = bound(raw, 1, 10_000);
        if (expectedPaused) {
            vm.expectRevert(LedgerV1.Paused.selector);
            ledger.grant(actor(i), amount);
            return;
        }
        ledger.grant(actor(i), amount);
        expected[i] += amount;
        expectedTotal += amount;
        grants++;
    }

    function spend(uint256 who, uint256 raw) external {
        uint256 i = who % 3;
        if (expectedPaused) {
            vm.expectRevert(LedgerV1.Paused.selector);
            vm.prank(actor(i));
            ledger.spend(1);
            blockedSpends++;
            return;
        }
        if (expected[i] == 0) return;
        uint256 amount = bound(raw, 1, expected[i]);
        vm.prank(actor(i));
        ledger.spend(amount);
        expected[i] -= amount;
        expectedTotal -= amount;
        spends++;
        if (upgraded) postUpgradeSpends++;
    }

    function pause(bool value) external {
        ledger.setPaused(value);
        expectedPaused = value;
    }

    function upgrade() external {
        if (upgraded || !expectedPaused) return;
        ledger.upgradeToAndCall(next, abi.encodeCall(LedgerV2.initializeV2, ()));
        expectedImplementation = next;
        upgraded = true;
        upgrades++;
    }

    function probe() external {
        vm.expectRevert(LedgerV1.Unauthorized.selector);
        vm.prank(actor(0));
        ledger.upgradeToAndCall(next, "");
        vm.expectRevert(LedgerV1.Unauthorized.selector);
        vm.prank(actor(0));
        ledger.setPaused(!expectedPaused);
        vm.expectRevert(Initializable.InvalidInitialization.selector);
        ledger.initialize(actor(0));
        vm.expectRevert(Initializable.InvalidInitialization.selector);
        LedgerV1(first).initialize(actor(0));
        vm.expectRevert(Initializable.InvalidInitialization.selector);
        LedgerV1(next).initialize(actor(0));
        if (upgraded) {
            vm.expectRevert(Initializable.InvalidInitialization.selector);
            LedgerV2(address(ledger)).initializeV2();
        }
        probes++;
    }
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;probe&lt;/code&gt; groups rejected operations whose expected outcome is unchanged state. It attempts an unauthorized upgrade and pause change, repeats proxy initialization and tries to initialize both implementation contracts directly. After a successful upgrade, it also repeats the version-two initializer.&lt;/p&gt;

&lt;p&gt;The probe uses specific errors. An unrelated revert is not acceptable evidence that access control worked. The model invariant then checks the owner and implementation slot as well as accounting state. Keeping those checks together helps expose a control-plane change even when user balances still look correct.&lt;/p&gt;

&lt;p&gt;The testing methods listed in &lt;a href="https://pharosproduction.com/services/smart-contracts-development/" rel="noopener noreferrer"&gt;smart contract development services&lt;/a&gt; at Pharos Production include Foundry fuzz testing. For an upgrade release, that activity becomes reviewable when its handover includes the actual handler selectors, independent model and failing counterexample. The service description supplies context for the method; the executable fixture supplies this article's evidence.&lt;/p&gt;

&lt;p&gt;There are five fuzz targets: grant, spend, pause, upgrade and probe. Public getters and inherited test helpers are excluded from the selector list. Targeting the handler contract explicitly also prevents automatically discovered implementation contracts from becoming unintended campaign targets.&lt;/p&gt;

&lt;p&gt;The outer caller chosen by the fuzzer is not a modeled end user. Authorized calls originate from the handler, while spending uses &lt;code&gt;vm.prank&lt;/code&gt; to represent one of the fixed actors. That keeps the authority model understandable. Expanding the sender population without defining roles would change the experiment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the whole state, then force a complete lifecycle
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;contract UpgradeInvariantTest is StdInvariant, Test {
    Handler internal handler;
    bytes32 internal constant IMPLEMENTATION_SLOT =
        0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc;

    function setUp() public {
        address candidate = vm.envOr("BROKEN", false)
            ? address(new BrokenV2()) : address(new LedgerV2());
        handler = new Handler(candidate);
        bytes4[] memory selectors = new bytes4[](5);
        selectors[0] = Handler.grant.selector;
        selectors[1] = Handler.spend.selector;
        selectors[2] = Handler.pause.selector;
        selectors[3] = Handler.upgrade.selector;
        selectors[4] = Handler.probe.selector;
        targetContract(address(handler));
        targetSelector(FuzzSelector(address(handler), selectors));
    }

    function invariant_modelMatchesProxy() public view {
        LedgerV1 v = handler.ledger();
        uint256 sum;
        for (uint256 i; i &amp;lt; 3; ++i) {
            assertEq(v.credit(handler.actor(i)), handler.expected(i), "actor credit");
            sum += handler.expected(i);
        }
        assertEq(v.total(), sum, "total vs model");
        assertEq(v.total(), handler.expectedTotal(), "model accumulator");
        assertEq(v.owner(), address(handler), "owner");
        assertEq(v.paused(), handler.expectedPaused(), "pause state");
        assertEq(address(uint160(uint256(vm.load(address(v), IMPLEMENTATION_SLOT)))),
            handler.expectedImplementation(), "implementation");
        assertEq(v.version(), handler.upgraded() ? 2 : 1, "version");
        if (handler.upgraded()) {
            assertEq(LedgerV2(address(v)).migrationMarker(), 2, "migration");
        }
    }

    function test_failedMigrationIsAtomic() public {
        LedgerV1 v = handler.ledger();
        address candidate = handler.next();
        vm.expectRevert(LedgerV1.MustPause.selector);
        vm.prank(address(handler));
        v.upgradeToAndCall(candidate, abi.encodeCall(LedgerV2.initializeV2, ()));
        invariant_modelMatchesProxy();
    }

    function test_requiredLifecycle() public {
        handler.grant(0, 17);
        handler.spend(1, 9);
        handler.pause(true);
        handler.spend(0, 1);
        handler.probe();
        handler.upgrade();
        invariant_modelMatchesProxy();
        handler.probe();
        handler.pause(false);
        handler.spend(0, 1);
        invariant_modelMatchesProxy();
        assertGt(handler.grants(), 0);
        assertGt(handler.spends(), 0);
        assertGt(handler.blockedSpends(), 0);
        assertEq(handler.upgrades(), 1);
        assertGt(handler.postUpgradeSpends(), 0);
        assertEq(handler.probes(), 2);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;a href="https://eips.ethereum.org/EIPS/eip-1967#logic-contract-address" rel="noopener noreferrer"&gt;ERC-1967 specification&lt;/a&gt; identifies the implementation storage slot. Reading it gives the test an observation independent of &lt;code&gt;version()&lt;/code&gt;. A candidate that simply reports version two must not satisfy an assertion that a particular implementation was installed.&lt;/p&gt;

&lt;p&gt;All persistent state checks live in one invariant function. They therefore inspect the same sequence at each check. The assertions cover the chosen accounts, total, owner, pause state and active implementation, followed by the migration marker when version two should be active.&lt;/p&gt;

&lt;p&gt;A successful call to &lt;code&gt;upgrade&lt;/code&gt; does not reset the model. It changes only expected implementation metadata. Preserving the ghost balances across that boundary is what makes a reset or redistribution visible.&lt;/p&gt;

&lt;p&gt;Random selection does not promise that every sequence completes the entire release lifecycle. &lt;code&gt;upgrade&lt;/code&gt; returns without action before pausing or after an earlier upgrade. &lt;code&gt;spend&lt;/code&gt; can return when its selected account has no credits. Those choices are visible limits on exploration, not evidence that every generated call accomplished useful work.&lt;/p&gt;

&lt;p&gt;The deterministic lifecycle test closes one specific gap. It guarantees a grant and spend before migration, a denied spend while paused, an upgrade, a repeated-initialization probe and successful spending after resumption. Its counter assertions make that path explicit. They do not establish transition coverage for all random campaigns.&lt;/p&gt;

&lt;p&gt;The atomicity test exercises a different branch: migration while unpaused must fail, and the invariant must still describe version one afterward. Notice that &lt;code&gt;handler.next()&lt;/code&gt; is read before arming &lt;code&gt;expectRevert&lt;/code&gt; and &lt;code&gt;prank&lt;/code&gt;. An external getter placed between those cheatcodes and the intended call can consume the next-call expectation and produce a fixture failure unrelated to migration behavior.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run the positive case and the negative control
&lt;/h2&gt;

&lt;p&gt;Run the normal suite with the fixed seed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;FOUNDRY_CACHE_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache-positive   forge &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--fuzz-seed&lt;/span&gt; 0x9162026 &lt;span class="nt"&gt;-vv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The September 16, 2026 run returned three passing tests: the model invariant, failed-migration atomicity and the required lifecycle. The invariant campaign executed 8,192 handler calls across 128 runs at depth 64. No unexpected handler revert or discarded call was reported.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Handler selector&lt;/th&gt;
&lt;th&gt;Calls reported in the passing campaign&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;grant&lt;/td&gt;
&lt;td&gt;1,661&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pause&lt;/td&gt;
&lt;td&gt;1,653&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;probe&lt;/td&gt;
&lt;td&gt;1,591&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;spend&lt;/td&gt;
&lt;td&gt;1,636&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;upgrade&lt;/td&gt;
&lt;td&gt;1,651&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These figures are selector invocations, not successful economic operations. A paused grant follows an expected rejection path. An upgrade invocation can do nothing because its precondition is absent or migration already happened. The explicit success counters in the lifecycle test answer a narrower, separate question.&lt;/p&gt;

&lt;p&gt;Now select the broken candidate without changing the property:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;BROKEN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;true &lt;/span&gt;&lt;span class="nv"&gt;FOUNDRY_CACHE_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;cache-negative   forge &lt;span class="nb"&gt;test&lt;/span&gt; &lt;span class="nt"&gt;--match-test&lt;/span&gt; invariant_modelMatchesProxy   &lt;span class="nt"&gt;--fuzz-seed&lt;/span&gt; 0x9162026 &lt;span class="nt"&gt;-vv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command is expected to exit unsuccessfully. In the observed run, Foundry found an eight-call failing sequence and shrank it to two calls: pause, then upgrade. The initial seeded credits explain why those two calls suffice. Resetting the aggregate violates the model even without a generated grant.&lt;/p&gt;

&lt;p&gt;Separate cache directories keep the negative-control failure corpus apart from the positive campaign. The candidate switch is explicit through &lt;code&gt;BROKEN&lt;/code&gt;; it does not rewrite the invariant or relax an assertion. Do not run the negative control as an ordinary green CI job and then ignore its exit status.&lt;/p&gt;

&lt;p&gt;A passing negative control would block acceptance of this harness. It would mean the intended defect was not reached, the observation was missing, or the oracle had accepted the defect. Raising the run count would not be the first repair; inspect the target path and expected state first.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review the harness as executable code
&lt;/h2&gt;

&lt;p&gt;Inspect every early return. In this handler, an upgrade outside the modeled paused state is intentionally skipped. That helps explore the intended workflow, but it removes unauthorized sequencing from that action's domain. The separate atomicity test covers an unpaused migration attempt; it does not cover every possible authorized upgrade payload. A reviewer should be able to map each exclusion to a separate check or an explicit limitation.&lt;/p&gt;

&lt;p&gt;Inspect every privilege shortcut as well. &lt;code&gt;vm.prank&lt;/code&gt; changes the caller for testing; it does not demonstrate that a deployed administrator can assemble signatures or execute a governance proposal. The fixed actor addresses are identities inside the local test environment. They are not funded production accounts, and the campaign makes no statement about key custody.&lt;/p&gt;

&lt;p&gt;Expected-revert tests need a successful neighboring path. If all spending reverted for an unrelated reason, a pause denial alone could look reassuring. The lifecycle therefore includes spending before pausing and after resumption. This establishes that the tested denial sits between working paths for the selected actor and amount. It remains a concrete example, not a proof of universal withdrawal availability.&lt;/p&gt;

&lt;p&gt;Finally, distinguish state safety from progress. Equality checks can show that an attempted action did not corrupt the observed ledger. They cannot show that a necessary upgrade will eventually be proposed, approved or executed. Operational readiness requires a separate owner and execution procedure. A property called eventual recovery would need a defined time model and assumptions about the actors who must act.&lt;/p&gt;

&lt;h2&gt;
  
  
  Preserve the failure as release evidence
&lt;/h2&gt;

&lt;p&gt;A seed alone is not a portable proof. Keep the source revision with compiler settings and dependency commits. Preserve the generated sequence, the candidate-selection environment and the tool version. A later toolchain can make different generation or shrinking decisions even when the numeric seed matches.&lt;/p&gt;

&lt;p&gt;For a real defect, convert the reduced sequence into a named regression test. Include its required initial state. Here, removing the seeded balances would change the meaning of the two-call reproducer, so a bare list of method names is incomplete evidence.&lt;/p&gt;

&lt;p&gt;Keep the first failing log as well as the minimized trace. Do not treat every number in a failure message as a value recomputed from the shortest sequence; inspect the replay before reporting intermediate balances. The article's reproducible claim is the accounting failure and the reduced action sequence, not a guessed value at an unseen trace step.&lt;/p&gt;

&lt;p&gt;When diagnosing a failed campaign, identify whether the failing assertion belongs to the protocol model, an expected-revert probe or the harness itself. A bad caller, wrong selector or misplaced cheatcode can stop useful exploration. Correct the fixture without weakening the requirement, then rerun the affected campaign with a retained explanation.&lt;/p&gt;

&lt;p&gt;Release evidence should name the exact candidate it covers. A green result for yesterday's implementation is not automatically evidence for a rebuild, a dependency change or different initializer arguments. Treat those changes as new inputs to the relevant checks.&lt;/p&gt;

&lt;h2&gt;
  
  
  Extend the model to the real release boundary
&lt;/h2&gt;

&lt;p&gt;Replace the integer ledger only after writing the properties of the actual system. For a share-based vault, model the intended relationship between deposits, shares and withdrawals, including permitted rounding. Avoid computing the expected answer by calling the same conversion function that the implementation uses; both sides would inherit the same mistake.&lt;/p&gt;

&lt;p&gt;External assets introduce additional behavior. Fee-on-transfer tokens, callbacks and unusual return values can invalidate a handler that assumes a simple transfer. Those cases require explicit fixtures and observations. This example neither executes external transfers nor tests reentrancy.&lt;/p&gt;

&lt;p&gt;A governance-controlled system needs its real authorization route. Represent proposal creation, delay and execution through the timelock or multisig rather than using a privileged prank for every update. Keep expected denials for unauthorized callers. A mock owner proves only the owner-based boundary implemented here.&lt;/p&gt;

&lt;p&gt;Migration can be incremental. If users are converted lazily, the model needs pre-migration and post-migration account states with rules for crossing between them. A single global version flag cannot describe users whose data is at different stages. Define which operations remain valid at each stage and which historical quantities must still reconcile.&lt;/p&gt;

&lt;p&gt;Events need their own observations when off-chain consumers depend on them. This harness reads contract state and does not assert event contents or ordering. An indexer could therefore receive an incorrect migration event while every property here passes. Add an event assertion or integration test where an external consumer relies on that message to change its interpretation of stored data.&lt;/p&gt;

&lt;p&gt;Similarly, an authorization probe for upgrading does not establish authorization on every administrator method. The example exercises a particular set of denied calls. A production review should enumerate privileged entry points and identify the test responsible for each one, including newly introduced methods in the candidate implementation.&lt;/p&gt;

&lt;p&gt;A production pause policy may permit withdrawals while blocking deposits. Encode that asymmetry directly instead of copying this fixture's blanket pause on grants and spending. Likewise, distinguish safe resumption from a claim that reverting implementation code restores all earlier state. An invariant campaign does not undo transactions or establish rollback feasibility.&lt;/p&gt;

&lt;p&gt;When adding another property, identify the defect it would catch that existing assertions miss. A second assertion of the same total under another name adds little. A check for an account that was previously absent from the actor registry changes coverage. A check for an external recipient changes the observation boundary. Keep that distinction visible in code review so the suite grows with the system's behavior rather than with a checklist of reassuring names.&lt;/p&gt;

&lt;p&gt;Retain layout validation alongside behavioral tests. This fixture checks three known accounts and a few public fields; it cannot inspect every possible mapping key or prove that all storage interpretations are compatible. Static compatibility checks, deployment validation and a fork rehearsal each answer questions the local campaign leaves open.&lt;/p&gt;

&lt;p&gt;The practical release decision is specific: the reviewed candidate agrees with the stated model over the executed paths, the required lifecycle completes, and a controlled violation causes failure. List the untested paths beside that result. That gives the next engineer something to reproduce, challenge and extend before the upgrade reaches users.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bulbapp.io/p/e2580ef7-324b-442d-b818-92b8c8442b3d/five-smart-contract-upgrade-patterns-compared" rel="noopener noreferrer"&gt;Five Smart Contract Upgrade Patterns Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.medium.com/upgradeable-solidity-smart-contracts-part-1-versioning-7e6e97cafc28" rel="noopener noreferrer"&gt;Upgradeable Solidity Smart Contracts. Part 1 — Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-1-222fe44ee14c" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-2-40942c055d79" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/web3-smart-contracts-oracles-part-1-3905b127c01d" rel="noopener noreferrer"&gt;Web3. Smart Contracts. Oracles. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzoazi4278bedr68mju8l.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fzoazi4278bedr68mju8l.jpg" width="112" height="112" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>web3</category>
      <category>testing</category>
      <category>security</category>
    </item>
    <item>
      <title>10 Smart Contract Development Companies Compared by Repository and Release Evidence in 2026</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Tue, 15 Sep 2026 08:05:41 +0000</pubDate>
      <link>https://dev.to/pharos_production/10-smart-contract-development-companies-compared-by-repository-and-release-evidence-in-2026-m59</link>
      <guid>https://dev.to/pharos_production/10-smart-contract-development-companies-compared-by-repository-and-release-evidence-in-2026-m59</guid>
      <description>&lt;p&gt;A smart contract development company should be able to connect the code it proposes to ship with the tests, review decisions and deployment records that justify shipping it. That connection is the basis of this comparison. A service page establishes what a company offers; a repository can expose implementation artifacts; a release record must explain which artifacts reached which environment.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;Pharos Production's smart contract development team&lt;/a&gt; prepared this list and selected its order. The positions are editorial, with the publishing company first. They are not security scores or a claim that the first company has the strongest public repository.&lt;/p&gt;

&lt;p&gt;The review cutoff is &lt;strong&gt;September 15, 2026&lt;/strong&gt;. All ten companies have relevant published service descriptions. For four, this bounded review also inspected attributable public repository metadata and file trees. No builds were executed, no companies were contacted, and no client audit-to-deployment chain was independently verified. The comparison therefore separates observed artifacts from the release evidence still needed before a hiring decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the comparison can establish
&lt;/h2&gt;

&lt;p&gt;Each company receives the same four fields: published scope, repository observation, release evidence to request, and a limitation. A missing repository observation means this review has no attributable sample for that company. It does not mean the company has no repositories or cannot provide confidential evidence.&lt;/p&gt;

&lt;p&gt;For the inspected samples, a commit identifier fixes the file-tree observation. A test directory proves that files exist at that commit; it does not prove that the tests run or detect meaningful failures. A package release proves that a release was published; it does not establish that a customer's contracts were audited or deployed from it.&lt;/p&gt;

&lt;p&gt;Use the company profiles to decide what to inspect next. Use the common release packet and hard stops below to decide whether the evidence is sufficient. Keep those decisions separate from price, staffing availability and contractual terms, which this review did not assess.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Pharos Production: Smart Contract Development Company
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; The smart contract service description covers Solidity engineering, architecture, automated testing and deployment pipelines. For a buyer facing a gap between tested code and deployment, those services provide a relevant scope to discuss; delivery quality still requires artifacts from the proposed engagement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; The company-linked GitHub organization includes the &lt;code&gt;openzeppelin-solidity&lt;/code&gt; fork. At commit &lt;code&gt;1238d8f&lt;/code&gt;, its tree contains contracts, tests, a dependency lock and audit material inherited within the repository. The inspected repository's releases endpoint returned no releases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; Ask for an attributable delivery sample, the team's changes, test execution records and a deployment manifest tied to the reviewed commit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; An upstream library fork and its audit files do not establish the company's own audit coverage, client release history or current delivery-team competence.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. ScienceSoft
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; Its smart contract development page describes consulting, implementation, testing, blockchain deployment and oracle integration with external systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; The inspected service page did not supply an attributable repository sample for this review. Its statements about testing and audits remain published service claims, rather than independently reproduced release evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; For an oracle-dependent contract, request the release commit, external-data configuration and tests covering stale observations, invalid values and unavailable providers. The deployment record should identify the actual data source and the authority allowed to replace it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; The public scope does not establish how a proposed team handles those failure cases. A successful integration example would also need its operational assumptions and excluded dependencies before it could support a release decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. PixelPlex
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; Its smart contract page describes requirements discovery, architecture, implementation, security testing, controlled deployment and subsequent support.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; No attributable project repository was selected from that service-page inspection. The described sequence supplies useful questions for a release review, but it does not provide a commit, reproducible result or deployment receipt.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; Ask the proposed lead to trace one contract change through implementation, review, tests, deployment configuration and post-launch checks. For a token or NFT system, include minting permissions, transfer restrictions and the handling of administrative changes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; A published development process does not establish that every engagement follows it. Review the actual release package and responsibility split, especially when the buyer or a separate auditor owns part of the delivery process.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. SoluLab
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; The current dApp development page covers smart contracts alongside application development, testing and deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; Its verified GitHub organization contains an Ethereum boilerplate fork and a separate public &lt;code&gt;Internal-ChatBids-SmartContract&lt;/code&gt; repository. The latter's tree at &lt;code&gt;6cef7c7&lt;/code&gt; includes program sources, lockfiles, tests and a deployment migration. Its releases endpoint returned no releases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; Request a recent project on the intended chain, including the application's contract interface, deployment configuration and a trace of a failed transaction through the user interface and backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; The inspected sample establishes file availability, not test success, production use or current support. Its Rust program structure should not be treated as evidence of equivalent Solidity delivery without a relevant EVM sample.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Boosty Labs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; The company describes blockchain engineering and provides a direct link to its GitHub organization. Its engagement scope includes development capacity that can participate in a wider delivery team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; The public &lt;code&gt;ultimatedivision-smartcontracts&lt;/code&gt; tree at &lt;code&gt;93b1abf&lt;/code&gt; contains Solidity contracts, tests, deployment migrations and a dependency lock. The commit is dated September 6, 2022; the inspected releases endpoint returned no releases.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; Ask for a current comparable sample and identify who owns code review, audit remediation, deployment approval and operational handover when engineers join the buyer's team.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; This older repository cannot establish current release practices or the capability of engineers assigned in 2026. Repository push metadata and the date of the inspected commit are different facts and should not be substituted for one another.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Unicsoft
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; Its smart contract service material discusses development, external-system dependencies and concerns including synchronization and performance.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; The inspected service page did not provide an attributable repository sample for this review. Its scope is relevant to an integration-heavy project, while its implementation and release history remain unresolved here.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; Request one versioned interface contract covering off-chain inputs, on-chain state transitions and recovery from interrupted synchronization. Then ask for the tests and deployment settings that enforce those assumptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; Describing integration risks does not prove that a particular delivery team has implemented the required controls. A migration or synchronization demonstration must specify its starting state and the data that cannot be recovered automatically after an interruption.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Antier
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; Its smart contract page lists development, auditing and optimization, with deployment and testing in the described process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; The service-page review did not establish a project repository, release commit or independently checked audit trail. The scope is a starting point for requesting those artifacts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; For a value-handling contract, ask for the accounting properties, tests of privileged actions and the exact code covered by security review. If optimization is proposed, compare behavior before and after the change under the same compiler and workload assumptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; An optimization claim is insufficient without its baseline and correctness checks. An audit service description also leaves open who performed a particular review, what it excluded and whether later changes received further assessment.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Vention
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; Its smart contract offering includes design, development, audits, application integration and automated testing. It also describes team augmentation and other delivery models.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; No attributable repository sample was established from the inspected service page. The page describes access controls and multisignature arrangements, without proving the authority configuration of a proposed deployment.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; Ask for a release responsibility map alongside the code: who approves changes, controls deployment credentials, resolves findings and accepts residual risk. Require a versioned authority manifest and evidence that the deployed controllers match it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; Staffing arrangements can place these responsibilities on different organizations. A technically capable contributor does not, by that fact alone, own the complete release process or the buyer's production approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Cheesecake Labs
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; Its blockchain offering includes smart contracts across Stellar, Solana, Ethereum and Sui, plus tokenization, wallets and DeFi systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; Its linked GitHub organization publishes &lt;code&gt;stellar-plus&lt;/code&gt;. The inspected development-branch tree at &lt;code&gt;e3a44fb&lt;/code&gt; includes unit tests and test-coverage and package-publishing workflows. The releases list includes &lt;code&gt;v0.14.4&lt;/code&gt;, published August 7, 2025, targeting &lt;code&gt;main&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; Resolve the selected release tag to its commit, then inspect its build and test records. For a proposed application, separately request the contract deployment manifest and audit scope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; The inspected branch commit must not be assumed to be the release commit. This SDK offers inspectable engineering artifacts, but it does not establish a customer's audited contract deployment or equivalent expertise across every advertised chain.&lt;/p&gt;

&lt;h2&gt;
  
  
  10. Hyperlink InfoSystem
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Published scope.&lt;/strong&gt; Its smart contract material presents requirements, development, testing and deployment within a broader application delivery process.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repository observation.&lt;/strong&gt; The inspected service page did not establish an attributable contract repository or a release packet. Consequently, implementation, test execution and production reconciliation remain unverified in this comparison.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Release evidence to request.&lt;/strong&gt; Ask for a project that connects a wallet-facing application to contract execution. Trace one successful transaction and one rejected transaction through request creation, signing, submission, confirmation and the application's displayed state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Limitation.&lt;/strong&gt; General application testing does not establish correct handling of chain-specific failure modes. The proposed team must identify which failures its contract tests cover and which require integration or operational checks outside the contract repository.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read the evidence matrix before making a shortlist
&lt;/h2&gt;

&lt;p&gt;The same contract-to-release gap motivates the &lt;a href="https://pharosproduction.com/services/smart-contracts-development/" rel="noopener noreferrer"&gt;smart contract testing and deployment services&lt;/a&gt; described in the publishing company's service scope. Treat that description as a statement of offered work. Apply the artifact requirements below to the publisher and every other candidate before crediting a delivery claim.&lt;/p&gt;

&lt;p&gt;In this matrix, &lt;strong&gt;unverified&lt;/strong&gt; means no complete client audit-to-deployment relation was established during this review. It is a review status, not a verdict on the company's work. The commit prefixes identify snapshots inspected for this article; buyers should retain complete hashes in their own records.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Company&lt;/th&gt;
&lt;th&gt;Inspected public artifact&lt;/th&gt;
&lt;th&gt;Snapshot or release observation&lt;/th&gt;
&lt;th&gt;Client audit-to-deployment relation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Pharos Production&lt;/td&gt;
&lt;td&gt;Upstream smart contract library fork&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;1238d8f&lt;/code&gt;; files present; no repository releases returned&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ScienceSoft&lt;/td&gt;
&lt;td&gt;Service description&lt;/td&gt;
&lt;td&gt;No repository sample established&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;PixelPlex&lt;/td&gt;
&lt;td&gt;Service description&lt;/td&gt;
&lt;td&gt;No repository sample established&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SoluLab&lt;/td&gt;
&lt;td&gt;Contract program repository&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;6cef7c7&lt;/code&gt;; tests and migration present; no releases returned&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Boosty Labs&lt;/td&gt;
&lt;td&gt;Solidity contract repository&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;93b1abf&lt;/code&gt;; tests and migrations present; no releases returned&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unicsoft&lt;/td&gt;
&lt;td&gt;Service description&lt;/td&gt;
&lt;td&gt;No repository sample established&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Antier&lt;/td&gt;
&lt;td&gt;Service description&lt;/td&gt;
&lt;td&gt;No repository sample established&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Vention&lt;/td&gt;
&lt;td&gt;Service description&lt;/td&gt;
&lt;td&gt;No repository sample established&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cheesecake Labs&lt;/td&gt;
&lt;td&gt;Stellar SDK repository&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;e3a44fb&lt;/code&gt; tree; separate &lt;code&gt;v0.14.4&lt;/code&gt; release observed&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Hyperlink InfoSystem&lt;/td&gt;
&lt;td&gt;Service description&lt;/td&gt;
&lt;td&gt;No repository sample established&lt;/td&gt;
&lt;td&gt;Unverified&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These differences change the next review step. An attributable repository lets a reviewer ask about specific files immediately. A service-only candidate first needs to supply a suitable sample. Neither route skips release verification. A private demonstration can be stronger evidence than an unrelated public repository, provided the reviewer can inspect the relevant artifacts and record what was demonstrated.&lt;/p&gt;

&lt;h3&gt;
  
  
  Compare answers without inventing a numerical ranking
&lt;/h3&gt;

&lt;p&gt;Suppose two candidates provide different evidence rooms. One supplies a recent public repository with a polished README but cannot identify the deployed configuration. The other supplies a supervised private demonstration that resolves the build, audit changes and deployment manifest. For the release decision, the second demonstration answers more of the required questions. Public visibility remains useful, but it is not an acceptance criterion by itself.&lt;/p&gt;

&lt;p&gt;Write down the question each artifact answers. A source tree can answer what files were present. A reproducible build can answer how an output was created. A review report can answer what someone examined. A deployment receipt can answer where a transaction executed. None automatically answers the neighboring question. This keeps the comparison tied to evidence rather than presentation quality.&lt;/p&gt;

&lt;p&gt;Use a small set of descriptive outcomes: demonstrated for the agreed scope, partially demonstrated with a named gap, or not demonstrated. Give every gap an owner and a follow-up requirement. Keep a material failure separate from minor documentation cleanup; an unresolved upgrade controller should not disappear inside an average score.&lt;/p&gt;

&lt;p&gt;The proposed delivery team should participate in the review. A sample produced by another team may illustrate an organizational process, but the buyer still needs to know who can maintain it. Ask the assigned lead to explain one design trade-off and locate the corresponding implementation and test. Record the answer's scope without turning the meeting into an unpaid production exercise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Request one release packet from every shortlisted company
&lt;/h2&gt;

&lt;p&gt;Select a system close to the intended chain, asset flow and authority model. Give every candidate the same request and acceptance criteria. Do not let one present a simple token while another must explain a lending protocol, unless the difference is explicitly part of the scope decision.&lt;/p&gt;

&lt;p&gt;The packet should identify the repository and complete commit hash, compiler version and settings, dependency locks, build instructions, test configuration and execution records. It should also contain the security-review scope, finding dispositions, deployment manifest and operational owners. Record unavailable fields as unavailable, rather than accepting a slide deck as an equivalent substitute.&lt;/p&gt;

&lt;p&gt;This is an application of build provenance to the release decision. &lt;a href="https://slsa.dev/spec/v1.2/build-provenance" rel="noopener noreferrer"&gt;SLSA's provenance specification&lt;/a&gt; describes recording how an artifact was produced, including its build definition and execution details. A buyer can use that principle without claiming SLSA compliance: identify the inputs, the builder and the output being approved.&lt;/p&gt;

&lt;p&gt;For confidential work, agree a controlled review format. A sanitized repository or supervised demonstration can protect client material while showing the process. Record whether the sample represents a production engagement, a reference implementation or a training exercise. Each can answer useful questions, but only within its stated scope.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwlm73voqnenjxkgdbuz.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fgwlm73voqnenjxkgdbuz.png" alt="A release evidence chain connects a source commit to build and test records, audit scope, deployment, and reconciliation; any changed input requires review of affected evidence." width="800" height="410"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Release evidence must remain connected when code or configuration changes. Diagram by the author.&lt;/small&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the repository against the claim being made
&lt;/h2&gt;

&lt;p&gt;Begin with a clean, isolated review environment and the documented build procedure. Record the toolchain and dependency resolution used. The expected output needs an identity, such as a digest and build metadata, that can be compared with the release artifact. A successful local build is only one observation; preserve the associated logs and configuration.&lt;/p&gt;

&lt;p&gt;Next, inspect a small number of important properties. For an escrow, a buyer might require that only the authorized party releases funds and that recorded liabilities remain consistent with held assets under the defined token model. For a minting system, examine issuance authority and supply constraints. The properties must follow the actual design, including fees, rounding and external-token behavior.&lt;/p&gt;

&lt;p&gt;A useful demonstration introduces a reversible defect in an isolated copy and shows the relevant test fail. This checks whether the assertion detects the selected failure. It does not measure the whole team's ability or justify claims about all possible attacks. Record the defect, expected failure and restoration of the original commit.&lt;/p&gt;

&lt;p&gt;Inspect review exceptions as carefully as passing checks. A suppressed finding, excluded directory or accepted risk should identify its scope, rationale and owner. A test result becomes harder to interpret when the reviewer cannot tell which code paths or configuration it omitted. This is why repository structure and green badges are insufficient on their own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reconcile the audit, deployment and authority
&lt;/h2&gt;

&lt;p&gt;An audit report needs a scope commit or an equally precise source boundary. Trace findings to fixes and retest decisions. Then compare that boundary with the candidate release. Changes after the audit require a disposition: reviewed, assessed as outside the relevant scope, or still unresolved. A later branch name cannot substitute for that accounting.&lt;/p&gt;

&lt;p&gt;For an EVM deployment, preserve the chain identifier, transaction receipt, contract address, compiler settings, linked libraries and constructor or initialization inputs. Where proxies are involved, distinguish the proxy from its implementation and identify the authority that can change either relevant configuration or implementation selection.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://ethereum.org/en/developers/docs/smart-contracts/verifying/" rel="noopener noreferrer"&gt;Ethereum's contract verification guidance&lt;/a&gt; explains the role of checking source code against deployed bytecode. That relationship helps establish what is running. It does not prove that the business logic is correct, that the audit covered it or that administrators cannot change its behavior later.&lt;/p&gt;

&lt;p&gt;Consider a hypothetical release: an auditor reviewed commit A, the team fixed a finding in B, and deployment used C after an administrator change. Tests passing on B do not resolve C's new authority behavior. The buyer needs the B-to-C difference, its review disposition and confirmation of the deployed controllers before approving that release.&lt;/p&gt;

&lt;p&gt;The same reasoning applies when source stays unchanged but configuration moves. Replacing an oracle, changing an initializer argument or assigning a different controller can alter the system's behavior. An approval should therefore identify the code and the relevant deployment configuration together. Reconcile actual state against the manifest after deployment, rather than assuming the script's intended inputs became the final state.&lt;/p&gt;

&lt;h3&gt;
  
  
  Preserve the decision when a release changes
&lt;/h3&gt;

&lt;p&gt;Give the acceptance record a stable release identifier and retain the evidence it references. A later code or configuration change should create a new review entry with a link to the previous decision. Describe the affected assumptions and the checks repeated. This makes a small change reviewable without pretending that the earlier approval covers every future state.&lt;/p&gt;

&lt;p&gt;For example, changing an administrator address may leave bytecode untouched while changing who can authorize an upgrade. The follow-up check should inspect the controller's configured authority and the transfer outcome. Re-running an unrelated unit suite would not answer that question. Conversely, a documentation correction need not trigger a complete technical rehearsal if it changes no approved input or assumption.&lt;/p&gt;

&lt;p&gt;Have the receiving operator confirm that the handover is usable. They should be able to identify the deployed release, locate its unresolved risks and find the approved containment procedure. Record where the supplier's responsibility ends and the operator's begins. This final check turns a collection of documents into something the buyer can use after the engagement ends.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard stops that override an attractive proposal
&lt;/h2&gt;

&lt;p&gt;Pause acceptance when the team cannot identify the reviewed commit, reproduce the agreed build, connect material findings to their dispositions or explain deployed privileged authority. Also pause when the demonstrated artifact differs from the one proposed for release and nobody can account for the difference.&lt;/p&gt;

&lt;p&gt;Missing public code alone is not a hard stop. Refusing every reasonable way to demonstrate the claimed delivery process is. Likewise, an older sample may explain engineering decisions, but it should not silently stand in for evidence that the currently assigned team can operate the current toolchain.&lt;/p&gt;

&lt;p&gt;Vitalik Buterin described the need for layered security in his 2016 essay, &lt;a href="https://blog.ethereum.org/2016/06/19/thinking-smart-contract-security" rel="noopener noreferrer"&gt;Thinking About Smart Contract Security&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;There will be further bugs, and we will learn further lessons; there will not be a single magic technology that solves everything.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The procurement consequence is practical: no audit badge, public repository or named tool should carry the whole decision. Require connected evidence, identify its limits and assign responsibility for what remains unresolved.&lt;/p&gt;

&lt;p&gt;End the review with a short acceptance record: the company and proposed team, demonstrated system, exact release boundary, artifacts inspected, checks performed, unresolved items and decision owner. State what must be supplied before the next stage. Preserve that record when the release changes so the team can see which earlier conclusions still apply.&lt;/p&gt;

&lt;p&gt;This comparison gives ten starting points and a common way to evaluate them. The strongest next step is to ask a shortlisted team to explain one release through its actual artifacts. Choose on the evidence it can demonstrate for the work you need, with every gap visible before deployment approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bulbapp.io/p/e2580ef7-324b-442d-b818-92b8c8442b3d/five-smart-contract-upgrade-patterns-compared" rel="noopener noreferrer"&gt;Five Smart Contract Upgrade Patterns Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.medium.com/upgradeable-solidity-smart-contracts-part-1-versioning-7e6e97cafc28" rel="noopener noreferrer"&gt;Upgradeable Solidity Smart Contracts. Part 1 — Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/web3-smart-contracts-oracles-part-1-3905b127c01d" rel="noopener noreferrer"&gt;Web3. Smart Contracts. Oracles. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-1-222fe44ee14c" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-2-40942c055d79" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 2&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff87rh3rqhcbkjdpamosn.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff87rh3rqhcbkjdpamosn.jpg" width="800" height="800" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>web3</category>
      <category>testing</category>
      <category>security</category>
    </item>
    <item>
      <title>How to Rehearse Smart-Contract Rollback Before Calling a System Upgradeable</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Sat, 12 Sep 2026 07:06:15 +0000</pubDate>
      <link>https://dev.to/pharos_production/how-to-rehearse-smart-contract-rollback-before-calling-a-system-upgradeable-3532</link>
      <guid>https://dev.to/pharos_production/how-to-rehearse-smart-contract-rollback-before-calling-a-system-upgradeable-3532</guid>
      <description>&lt;p&gt;A smart contract can accept a new implementation and still have no safe route back. The upgrade transaction may succeed, the old bytecode may remain available, and an administrator may retain permission to install it. None of those facts proves that the old code can interpret the state users have created since the upgrade.&lt;/p&gt;

&lt;p&gt;Rehearse recovery against those later states before describing a system as operationally upgradeable. The useful result is a manifest that identifies the exact deployment, the checkpoint tested, the recovery action permitted there and the evidence that user rights survive it. A successful pointer change is only one observation in that record.&lt;/p&gt;

&lt;p&gt;This guide develops that manifest for a hypothetical EVM vault. Its scenarios are proposed tests, not results from a deployed protocol. Adapt the accounting, dependencies and authority model to the system under review.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. Define the recovery promise before choosing a command
&lt;/h2&gt;

&lt;p&gt;Use three separate terms in the runbook. An implementation rollback reinstalls an earlier implementation through the system's supported upgrade mechanism. A state repair transforms particular stored values under a reviewed procedure. A service recovery restores an acceptable user operation, possibly through a forward fix or a controlled migration. One incident may require all three, but each needs its own success criteria.&lt;/p&gt;

&lt;p&gt;When upgrade acceptance stops at deployment success, the missing deliverable is evidence of recovery. &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;Pharos Production&lt;/a&gt; documents a blockchain delivery process that includes contract testing, security review and staged deployment. A release review can attach the rehearsal described here to those activities and make the recovery assumptions explicit. That is a proposed acceptance artifact, not a claim that every contract has a reversible migration.&lt;/p&gt;

&lt;p&gt;For the example vault, define the promise as follows: an existing user retains the same valid withdrawal entitlement after recovery, subject only to documented fees and rounding; a pending withdrawal remains identifiable and cannot be paid twice; operators can resume only the functions whose invariants have passed. Specify how each condition will be measured before executing any recovery transaction.&lt;/p&gt;

&lt;p&gt;Also write down what the promise excludes. Restoring one contract's implementation cannot by itself reclaim a payment already received by another party, erase a message already executed on another chain or reverse a decision made by an external service. Those effects require separate authority and a separate reconciliation procedure.&lt;/p&gt;

&lt;p&gt;Give the promise an explicit scope: one vault, its asset contract, the withdrawal queue and any settlement adapter. A statement about the vault alone should not silently become a claim about the entire protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Freeze the deployed system you intend to rehearse
&lt;/h2&gt;

&lt;p&gt;Start from the actual deployment inventory. Record the chain identity, fork block number and block hash, proxy addresses, active implementation addresses and runtime bytecode hashes. Include the source commit, compiler version and settings, dependency lockfile and storage-layout artifacts used to explain that bytecode. A repository branch name is insufficient because it can move.&lt;/p&gt;

&lt;p&gt;Resolve the upgrade topology before selecting the recovery transaction. OpenZeppelin's &lt;a href="https://docs.openzeppelin.com/contracts/5.x/api/proxy" rel="noopener noreferrer"&gt;proxy reference&lt;/a&gt; distinguishes transparent proxies, UUPS implementations and beacon-based deployments. Their upgrade logic and control points differ. In a beacon system, enumerate every proxy that follows the affected beacon; testing one instance does not establish compatibility for instances with different initialization histories.&lt;/p&gt;

&lt;p&gt;For a UUPS deployment, establish that the currently installed implementation still exposes a usable authorized upgrade route. Do not assume that a route present in the previous release remains callable. Record any compatibility restriction that prevents reinstalling a particular historical version. The recovery target must be admissible through the deployed mechanism, not merely available in an artifact directory.&lt;/p&gt;

&lt;p&gt;Build the local fork at a fixed block. &lt;a href="https://www.getfoundry.sh/anvil/index.html" rel="noopener noreferrer"&gt;Anvil's official documentation&lt;/a&gt; describes local forking, controlled mining, state management and account impersonation. These capabilities support a rehearsal, but each convenience changes what the exercise proves. Pin the tool version and relevant chain configuration, verify the starting block hash against the recorded source chain, and keep the transaction destination confined to the local test environment.&lt;/p&gt;

&lt;p&gt;Document injected assumptions alongside the fixture: extra test balances, impersonated actors, mocked oracle responses and altered timestamps. Use dedicated test credentials. Production signing material is unnecessary for testing contract authorization rules, and its presence makes a local exercise harder to keep isolated.&lt;/p&gt;

&lt;p&gt;Finally, choose representative existing positions. Include a long-lived depositor, an account with a pending withdrawal, an empty account and any privileged account with special accounting treatment. Record why each position matters. A fork containing real storage is still a weak fixture if every test touches only a newly created user.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Test the meaning of storage in both directions
&lt;/h2&gt;

&lt;p&gt;A forward storage-layout check asks whether the new implementation can interpret the earlier layout. Recovery introduces another question: can the old implementation interpret every relevant state the new release is allowed to produce?&lt;/p&gt;

&lt;p&gt;OpenZeppelin makes the persistence issue concrete in &lt;a href="https://docs.openzeppelin.com/upgrades-plugins/writing-upgradeable" rel="noopener noreferrer"&gt;Writing Upgradeable Contracts&lt;/a&gt;:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;And if you remove a variable from the end of the contract, note that the storage will not be cleared.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The quotation concerns removing a variable from a contract definition. Its relevance to recovery is that changing code does not erase historical storage. The same documentation warns against incompatible changes to variable ordering and types. A layout validation therefore belongs in the release evidence, while the rehearsal must also address the meaning of the values already written.&lt;/p&gt;

&lt;p&gt;Consider a hypothetical vault whose first implementation stores withdrawal requests in asset units. A later version migrates those requests into shares while retaining a similarly shaped numeric field. The old implementation might read a perfectly ordinary integer after reinstallation and interpret it using the wrong unit. Successful reads and unchanged slot locations would not establish correct entitlements.&lt;/p&gt;

&lt;p&gt;Make a state-meaning table for every changed field: previous interpretation, new interpretation, transition that writes the new form and behavior if old code reads it. Include enumerations, sentinel values, rounding conventions, timestamps and identifiers. Mark the first transition that makes a direct return invalid. That boundary can occur during initialization, the first deposit or a later maintenance transaction.&lt;/p&gt;

&lt;p&gt;Use a concrete accounting fixture to expose the difference. Suppose the vault holds 1,000 asset units against 500 shares, with no fees or rounding in this example. A request for ten asset units becomes a request for five shares during migration. If old code later treats the stored five as asset units, the user receives only half the original entitlement. A test that merely confirms the request still exists would pass. A test that settles the request and compares the payment with its expected ten asset units would fail. Preserve both the stored representation and the economic expectation in the fixture so the assertion does not accidentally reuse the faulty conversion logic.&lt;/p&gt;

&lt;p&gt;Treat initializers and migrations as state transitions with their own preconditions. Determine whether a recovery requires additional initialization, whether a version guard prevents it and whether replaying a migration could duplicate an allocation. The answer must come from the specific contract and reviewed payload. Avoid a generic instruction to call the initializer again.&lt;/p&gt;

&lt;p&gt;If the reverse interpretation is undefined, record direct rollback as prohibited at that checkpoint. That finding is useful before release. Hiding it behind a green deployment test would turn a known architectural constraint into an incident-time surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Branch the rehearsal at three checkpoints
&lt;/h2&gt;

&lt;p&gt;Run independent branches from the pinned baseline. In each branch, apply the exact upgrade payload, advance to its named checkpoint and execute the recovery action against that checkpoint's state. Preserve transaction order and the arguments for every intervening operation.&lt;/p&gt;

&lt;p&gt;The first checkpoint is immediately after the upgrade transaction. If installation and migration occur atomically, this checkpoint already includes that migration; there is no accessible production state between the two. Do not manufacture an intermediate recovery window that the actual transaction never exposes.&lt;/p&gt;

&lt;p&gt;The second checkpoint is after any separate migration or initialization work. The third is after representative user activity and external interactions. These checkpoints describe progressively different conditions, not a guarantee that recovery becomes harder in a predictable numerical way.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftm9dtq4cem7pvbdb5zwl.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ftm9dtq4cem7pvbdb5zwl.png" alt="Three independent rehearsal branches start at the same pinned baseline and test recovery after installation, migration and user activity. Each branch checks invariants before allowing resume or requiring repair." width="800" height="500"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Rehearsal branches for the hypothetical vault. A local snapshot resets the test fixture; recovery acts on the state produced within a branch.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Use a small scenario matrix tied to the release's actual changes:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Branch&lt;/th&gt;
&lt;th&gt;State reached&lt;/th&gt;
&lt;th&gt;Recovery attempt&lt;/th&gt;
&lt;th&gt;Required observation&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Installation&lt;/td&gt;
&lt;td&gt;Upgrade payload completed&lt;/td&gt;
&lt;td&gt;Supported return to prior code&lt;/td&gt;
&lt;td&gt;Existing positions still behave correctly&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Migration&lt;/td&gt;
&lt;td&gt;Changed records converted&lt;/td&gt;
&lt;td&gt;Approved repair or forward fix&lt;/td&gt;
&lt;td&gt;Record meaning and ownership reconcile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;New activity&lt;/td&gt;
&lt;td&gt;Deposit and withdrawal requested&lt;/td&gt;
&lt;td&gt;Checkpoint-specific recovery&lt;/td&gt;
&lt;td&gt;Claims remain payable exactly once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;External effect&lt;/td&gt;
&lt;td&gt;Settlement adapter acted&lt;/td&gt;
&lt;td&gt;Containment and reconciliation&lt;/td&gt;
&lt;td&gt;External obligations remain accounted for&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Interrupted operation&lt;/td&gt;
&lt;td&gt;One step pending or reverted&lt;/td&gt;
&lt;td&gt;Resume the recorded procedure&lt;/td&gt;
&lt;td&gt;No duplicated action or lost request&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Use local snapshots to create repeatable starting conditions, then distinguish their resets from the action being tested. A test that upgrades, restores the baseline snapshot and calls an old function demonstrates the old fixture. It says nothing about old code running against the post-upgrade state. Keep the checkpoint evidence before any fixture reset.&lt;/p&gt;

&lt;p&gt;Order matters within a branch. A withdrawal requested before migration can exercise a different path from one requested afterward. A deposit followed by a withdrawal may expose a unit conversion that either operation alone misses. Select sequences from the changed behavior and known invariants, rather than expanding into a large arbitrary matrix.&lt;/p&gt;

&lt;p&gt;Include at least one deliberate incompatibility in the fixture or expectations. The harness should reject a recovery action that violates the declared unit or ownership rule. A suite that passes both the intended case and a clearly invalid case cannot support the release decision.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Exercise the authority path and the waiting period
&lt;/h2&gt;

&lt;p&gt;Rehearse through the actual control contracts. If the production route requires a multisig proposal followed by a timelock, the local sequence should exercise the corresponding proposal, authorization and execution checks. Calling an implementation directly as an impersonated administrator bypasses the part of the system that determines whether recovery is available.&lt;/p&gt;

&lt;p&gt;Maintain a clear distinction between simulated authority and operational availability. Impersonation can test behavior for a particular caller. It does not prove that enough people can access their signing devices during an incident. Advancing the local clock can test a delay condition. It does not measure detection time, approval time, network congestion or the time needed to inspect the result.&lt;/p&gt;

&lt;p&gt;Record those durations separately. Use measured operational evidence where it exists and label unmeasured estimates. The contract waiting period, signing process and detection path together determine what the system can do while affected operations remain available. Rehearse that exposure interval with realistic allowed actions, including a transaction already queued before a pause.&lt;/p&gt;

&lt;p&gt;Recovery belongs in the same release conversation as audit remediation. The &lt;a href="https://pharosproduction.com/services/how-we-build-blockchain-solutions/" rel="noopener noreferrer"&gt;blockchain delivery process documented by Pharos Production&lt;/a&gt; includes testing, internal review, external audit coordination and staged deployment. Attach the checkpoint manifest to that process so a reviewer can identify which recovery payload was assessed and which subsequent writes invalidate it. This adds an inspectable condition to delivery without turning an audit into a guarantee of reversibility.&lt;/p&gt;

&lt;p&gt;Test the pause boundary precisely. Identify the functions a guardian can stop, functions that remain callable and the authority required to resume them. A pause that prevents deposits but leaves an unsafe settlement path open does not provide the containment assumed by the runbook. A pause that blocks every exit may also change the recovery obligations to users.&lt;/p&gt;

&lt;p&gt;Include a stale operation in the scenario. After choosing a recovery path, verify what happens to an already scheduled upgrade or maintenance transaction. Cancel it where the governance design permits cancellation, or demonstrate that its preconditions prevent execution against the recovered state. Leave no unexplained pending payload capable of undoing the repair.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Verify user outcomes after the recovery transaction
&lt;/h2&gt;

&lt;p&gt;A successful receipt establishes that a transaction executed without reverting. It does not establish that balances, claims and permissions are correct. Capture observations before the upgrade, at the failed checkpoint and after recovery, then reconcile the expected changes between them.&lt;/p&gt;

&lt;p&gt;For the hypothetical vault, start with ownership and accounting. Every sampled withdrawal request should retain its owner, status and entitlement under the declared accounting model. A completed payment must not remain claimable. A pending payment must not disappear merely because the previous implementation ignores a field introduced by the upgrade.&lt;/p&gt;

&lt;p&gt;Define conservation using the system's actual assets and liabilities. Track deposits, withdrawals, fees and any permitted gain or loss in consistent units. Explain rounding tolerances and their maximum aggregate effect. A bare comparison between the vault's token balance and total shares is usually not a complete accounting rule because the quantities may have different meanings.&lt;/p&gt;

&lt;p&gt;Check behavior as well as getters. Have a representative user finish a withdrawal, create a new permitted request and encounter the intended restriction on an invalid request. Verify allowances, role membership, pause settings and request identifiers where the release can affect them. A readable position is not necessarily a usable position.&lt;/p&gt;

&lt;p&gt;Record the population covered by the checks. Sampling representative accounts is useful for scenario design, but it does not prove that every mapping entry survived a migration. If the migration touches a bounded set of records, reconcile that complete set. If the population is large, define the mechanism that establishes coverage, such as an enumerated migration input with totals and per-record checks. State the residual uncertainty rather than presenting a sample as exhaustive evidence.&lt;/p&gt;

&lt;p&gt;Then examine consumers outside the upgraded contract. An indexer may have processed events emitted before recovery. A keeper may hold a pending job. An adapter may have accepted an identifier that the old code no longer understands. Record how these components rebuild, reject stale work or reconcile their records. The chain's current storage is only part of the service state.&lt;/p&gt;

&lt;p&gt;Make every assertion report expected and actual values. When one fails, preserve the smallest reproducible sequence that reaches the discrepancy. This gives a reviewer an explanation of the boundary that broke instead of a screenshot containing a red test name.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. Select recovery from the state that actually exists
&lt;/h2&gt;

&lt;p&gt;Choose a direct implementation rollback only when the deployed mechanism permits it and the relevant post-upgrade states remain compatible with the earlier code. Rehearse the exact historical bytecode and the actual return payload. Recompiling an old source tag under different settings creates a different artifact to review.&lt;/p&gt;

&lt;p&gt;If a reversible migration is part of the design, test its inverse as a separate operation. Identify the information needed to restore the earlier representation, the records it will touch and the transaction boundaries. If the forward migration discarded information, the inverse requires another trustworthy source for it; calling the procedure a rollback does not supply the missing data.&lt;/p&gt;

&lt;p&gt;For a migration spread across transactions, rehearse a partially processed population. Record which entries use the old representation and which use the new one. Check that a retry cannot convert the same record twice and that recovery does not assume every batch completed. Run the largest supported batch under the intended gas conditions and preserve the boundary at which another transaction is required. Local success with an artificially generous block gas limit would not establish that the same batch is executable on the target chain.&lt;/p&gt;

&lt;p&gt;A forward repair may preserve valid new state while correcting the faulty behavior. Its acceptance test must cover both the original defect and the recovery obligations. Reusing the upgrade route is convenient, but the repair is still new code with new assumptions. Do not substitute confidence in the previous release for review of the repair.&lt;/p&gt;

&lt;p&gt;Some checkpoints should permit containment only. After an external payment or cross-chain execution, the immediate action may be to stop further affected operations and establish a reconciled claim set. Compensation, migration or administrator-assisted processing can require a separate decision. Record the unresolved obligation and its owner before allowing unrelated activity to obscure it.&lt;/p&gt;

&lt;p&gt;Keep the incident decision narrow. The manifest should identify the permitted action for each observed checkpoint and the evidence needed to choose it. An operator should not need to invent a new storage transformation while deciding whether the system is safe to resume.&lt;/p&gt;

&lt;h2&gt;
  
  
  8. Preserve a manifest that another reviewer can reproduce
&lt;/h2&gt;

&lt;p&gt;The manifest is the durable output of the rehearsal. Store it beside the release artifacts and bind its evidence to immutable hashes. Keep confidential endpoint credentials and signing material out of it; a provider label and the required access characteristics are enough to describe the environment.&lt;/p&gt;

&lt;p&gt;The following is a template, not a completed run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;rehearsal_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;vault-release-candidate-01&lt;/span&gt;
&lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;NOT_RUN&lt;/span&gt;
&lt;span class="na"&gt;baseline&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;chain_id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;block_number&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;block_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;deployment_inventory_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
&lt;span class="na"&gt;release&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;source_commit&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;build_and_dependency_manifest_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;implementation_code_hashes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;upgrade_payload_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
&lt;span class="na"&gt;scenario&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;checkpoint&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;ordered_transactions_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;simulated_privileges_and_time_changes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
&lt;span class="na"&gt;recovery&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;permitted_action&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;payload_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;authority_and_delay_evidence&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
&lt;span class="na"&gt;verification&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;invariant_definitions_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;expected_and_actual_results_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;covered_records_and_exclusions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;receipts_traces_and_state_diff_hash&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
&lt;span class="na"&gt;decision&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;reviewer&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;outcome&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;UNDECIDED&lt;/span&gt;
  &lt;span class="na"&gt;unresolved_obligations&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
  &lt;span class="na"&gt;invalidation_conditions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;REQUIRED&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Give every scenario its own record or unambiguous entry. Do not overwrite a failed rehearsal with a successful rerun. Preserve the failed payload and observations, then link the revised run to the change that resolved them. The release decision should identify the exact accepted record rather than whichever file currently has the newest timestamp.&lt;/p&gt;

&lt;p&gt;Retain enough environment information to reproduce the observation: tool version, chain configuration, dependency versions and any mock behavior. An archived result without a usable fixture can help incident analysis, but it is weaker evidence for a future release than a procedure another engineer can execute.&lt;/p&gt;

&lt;p&gt;Keep local transaction receipts clearly labeled as rehearsal evidence. A local receipt is not a production receipt, and a local block height is not evidence that a live operation occurred. Store production approvals and eventual deployment observations in separately identified records linked to the same release.&lt;/p&gt;

&lt;h2&gt;
  
  
  9. Make the release claim expire when its assumptions change
&lt;/h2&gt;

&lt;p&gt;Approve recovery per checkpoint. A release can legitimately support direct rollback before user activity while requiring forward repair afterward. Put that distinction in the operational runbook and the release decision so the team knows when the simpler route stops being valid.&lt;/p&gt;

&lt;p&gt;Define invalidation conditions before deployment: different implementation bytecode, changed migration input, altered authority, a new dependency configuration or a newly enabled operation outside the rehearsed states. A recent rehearsal is not automatically current when the conditions it tested have changed.&lt;/p&gt;

&lt;p&gt;Specify abort conditions as executable preflight checks wherever possible. An unexpected current implementation hash, an unknown migration version or an unresolved settlement record should stop the selected procedure before its first write. Test those rejection paths as well as the accepted path. Record who investigates an abort and how the incident remains contained while the assumption is unresolved. This prevents a prepared recovery payload from becoming an instruction to proceed regardless of the state operators actually find.&lt;/p&gt;

&lt;p&gt;Before the real upgrade, compare the intended payload and deployment inventory with the accepted manifest. Review material state drift since the pinned block and rerun the affected scenarios when it changes their preconditions. After deployment, inspect the actual implementation and resulting state before resuming affected operations. Resolve an uncertain transaction outcome from on-chain evidence before submitting another potentially duplicative action.&lt;/p&gt;

&lt;p&gt;The defensible claim is specific: this release has a demonstrated recovery procedure for these states, with these remaining limitations. That gives operators a usable decision under pressure and gives reviewers evidence they can challenge. Upgradeability becomes an operational property only when the system can reach an acceptable state through an authorized, understood and rehearsed procedure.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://www.bulbapp.io/p/e2580ef7-324b-442d-b818-92b8c8442b3d/five-smart-contract-upgrade-patterns-compared" rel="noopener noreferrer"&gt;Five Smart Contract Upgrade Patterns Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.medium.com/upgradeable-solidity-smart-contracts-part-1-versioning-7e6e97cafc28" rel="noopener noreferrer"&gt;Upgradeable Solidity Smart Contracts. Part 1 — Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-2-40942c055d79" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-1-222fe44ee14c" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/web3-smart-contracts-oracles-part-1-3905b127c01d" rel="noopener noreferrer"&gt;Web3. Smart Contracts. Oracles. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F81i2l07e00u2ekr237vw.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F81i2l07e00u2ekr237vw.jpg" width="800" height="800" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background."&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>web3</category>
      <category>testing</category>
      <category>security</category>
    </item>
    <item>
      <title>Smart Contract Upgrade Authority: Timelocks, Multisigs, and Emergency Controls</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Wed, 09 Sep 2026 06:30:12 +0000</pubDate>
      <link>https://dev.to/pharos_production/smart-contract-upgrade-authority-timelocks-multisigs-and-emergency-controls-p6o</link>
      <guid>https://dev.to/pharos_production/smart-contract-upgrade-authority-timelocks-multisigs-and-emergency-controls-p6o</guid>
      <description>&lt;p&gt;An upgrade timelock protects users only when every implementation change must pass through it. A multisig helps distribute approval, but its threshold says little about an enabled module, an old administrator or an emergency role that can replace the code directly. Start by mapping those paths. Give routine upgrades a reviewed, delayed route and give incident responders a narrower power to stop specific operations. Then prove that neither route can silently acquire the other's authority.&lt;/p&gt;

&lt;p&gt;This guide proposes a control matrix for EVM protocols using upgradeable contracts. Its thresholds and timing examples are hypothetical design inputs, not universal recommendations or reported client results. AI assisted the drafting and the conceptual cover; the acceptance exercises below are proposed tests, not a report of tests run against a deployed protocol.&lt;/p&gt;

&lt;h2&gt;
  
  
  Put every privileged action in one control matrix
&lt;/h2&gt;

&lt;p&gt;Treat authority as a relationship between an actor, a target and a permitted state transition. A role name is insufficient: an emergency operator who can grant an upgrader role possesses indirect upgrade power. A reviewer needs both the direct call and the administrative path that can make the call possible.&lt;/p&gt;

&lt;p&gt;For an example vault, assume a governance multisig proposes routine changes, a timelock owns upgrade authority and a separate guardian can pause new deposits. The application must actually support that pause scope. Unpausing follows the normal governance route. A cancellation role can stop queued operations, subject to the limits discussed below.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Privileged action&lt;/th&gt;
&lt;th&gt;Actor and target&lt;/th&gt;
&lt;th&gt;Delay&lt;/th&gt;
&lt;th&gt;Veto&lt;/th&gt;
&lt;th&gt;Emitted evidence&lt;/th&gt;
&lt;th&gt;Recovery path&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Replace implementation&lt;/td&gt;
&lt;td&gt;Governance proposes; timelock calls upgrade gate&lt;/td&gt;
&lt;td&gt;Configured upgrade delay&lt;/td&gt;
&lt;td&gt;Canceller before execution&lt;/td&gt;
&lt;td&gt;Scheduled operation, execution, implementation event&lt;/td&gt;
&lt;td&gt;Corrected upgrade or tested migration&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Pause deposits&lt;/td&gt;
&lt;td&gt;Guardian calls scoped pause function&lt;/td&gt;
&lt;td&gt;Immediate after authorization&lt;/td&gt;
&lt;td&gt;No pre-execution veto&lt;/td&gt;
&lt;td&gt;Application pause event&lt;/td&gt;
&lt;td&gt;Governed unpause after verification&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unpause deposits&lt;/td&gt;
&lt;td&gt;Timelock calls scoped unpause function&lt;/td&gt;
&lt;td&gt;Chosen recovery delay&lt;/td&gt;
&lt;td&gt;Canceller before execution&lt;/td&gt;
&lt;td&gt;Scheduled operation and unpause event&lt;/td&gt;
&lt;td&gt;Pause again if the fault returns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rotate application roles&lt;/td&gt;
&lt;td&gt;Timelock calls the relevant role administrator&lt;/td&gt;
&lt;td&gt;Administrative delay&lt;/td&gt;
&lt;td&gt;Canceller before execution&lt;/td&gt;
&lt;td&gt;Role grant and revocation events&lt;/td&gt;
&lt;td&gt;Previously documented role-transfer route&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change timelock delay&lt;/td&gt;
&lt;td&gt;Timelock calls itself&lt;/td&gt;
&lt;td&gt;Existing minimum applies to scheduling&lt;/td&gt;
&lt;td&gt;Canceller before execution&lt;/td&gt;
&lt;td&gt;Scheduled operation and delay-change event&lt;/td&gt;
&lt;td&gt;Another delayed configuration change&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Change multisig owners or threshold&lt;/td&gt;
&lt;td&gt;Authorized Safe transaction changes the Safe&lt;/td&gt;
&lt;td&gt;No extra delay assumed&lt;/td&gt;
&lt;td&gt;Only constraints actually installed&lt;/td&gt;
&lt;td&gt;Owner and threshold events&lt;/td&gt;
&lt;td&gt;Tested surviving-owner or recovery route&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Move assets during recovery&lt;/td&gt;
&lt;td&gt;Separately authorized, narrowly scoped recovery function&lt;/td&gt;
&lt;td&gt;Explicit policy for that action&lt;/td&gt;
&lt;td&gt;Depends on implemented route&lt;/td&gt;
&lt;td&gt;Recipient, asset and amount evidence&lt;/td&gt;
&lt;td&gt;Reconciliation; no automatic reversal&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Replace each actor label with a chain-specific address and each delay with the deployed value. Record absent controls as absent. If an asset-recovery function does not exist, its row describes a design question rather than an available escape route. The matrix should expose those differences before a production incident makes them urgent.&lt;/p&gt;

&lt;p&gt;For a team confronting undocumented administrative paths, &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;Pharos Production&lt;/a&gt; provides smart-contract engineering; its published upgrade process includes documented procedures and fork rehearsals. That process is relevant to making an authority map executable. The matrix here is a proposed review artifact, with no claim that a particular customer deployment already satisfies it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvn89tut9t8io5nk7rbns.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fvn89tut9t8io5nk7rbns.png" alt="Illustrative authority paths. Governance multisig schedules a timelock operation that can call the upgrade gate when ready. A canceller can stop a pending operation. A separate guardian can pause new deposits only; unpause returns to the governed route. All deployed admin, module and recovery paths require inspection." width="800" height="485"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Illustrative authority paths. Diagram constructed with code and AI assistance; actual contracts must enforce the separation.&lt;/small&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Trace the upgrade gate to its actual owner
&lt;/h2&gt;

&lt;p&gt;Begin at the address users call. Resolve the implementation and identify the proxy pattern from verified code and deployment evidence. Then follow the authorization check outward until it reaches the accounts or governance mechanism that can satisfy it. Stop only when every branch has an identified controller.&lt;/p&gt;

&lt;p&gt;In OpenZeppelin's transparent pattern, inspect the associated &lt;code&gt;ProxyAdmin&lt;/code&gt; and its owner. For UUPS, inspect the implementation's &lt;code&gt;_authorizeUpgrade&lt;/code&gt; logic and the state that makes that check pass through the proxy. For a beacon arrangement, inspect the beacon's controller and the full set of dependent proxies. The &lt;a href="https://docs.openzeppelin.com/contracts/5.x/api/proxy" rel="noopener noreferrer"&gt;OpenZeppelin proxy reference&lt;/a&gt; documents these distinct mechanisms; a familiar address label does not establish which one a deployment uses.&lt;/p&gt;

&lt;p&gt;Imagine the timelock controls a &lt;code&gt;ProxyAdmin&lt;/code&gt;, while an application administrator can change the contract that authorizes upgrades. The first observation looks reassuring. The second may create another route to the same power. Draw both edges, including any ability to change role administrators, replace a registry or execute arbitrary calls.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://eips.ethereum.org/EIPS/eip-1967" rel="noopener noreferrer"&gt;ERC-1967&lt;/a&gt; specifies implementation, beacon and admin storage slots and recommends events for changes. Use those as inspection aids, then compare them with the actual contract. Custom proxies and version-specific designs require their own interpretation. A storage read is evidence about that slot at one block, not a complete account of future authority.&lt;/p&gt;

&lt;p&gt;For each network, retain the inspected block number and hash. The same application name on two chains can hide different guardians, signer sets or delay settings. An authority diagram without a network and observation point cannot show which deployment the reviewer accepted.&lt;/p&gt;

&lt;p&gt;Also inspect the powers that leave the implementation address unchanged. A controller might replace an oracle, alter a withdrawal recipient or raise a minting allowance. Those actions can change the system's economic behavior without invoking an upgrade function. Put them in an adjacent parameter-authority inventory and decide whether they require the same review window. Otherwise, a narrowly correct upgrade policy can coexist with an immediately callable action that creates comparable harm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the timelock bind a specific operation
&lt;/h2&gt;

&lt;p&gt;A delay has value when observers can identify the exact change awaiting execution. Record the target, value, calldata, predecessor and salt used to derive the operation identifier. Decode nested calls, including the implementation address and any initializer data. Link the readable explanation to those bytes so an edited description cannot silently redefine approval.&lt;/p&gt;

&lt;p&gt;OpenZeppelin's &lt;a href="https://docs.openzeppelin.com/contracts/5.x/api/governance#TimelockController" rel="noopener noreferrer"&gt;TimelockController API&lt;/a&gt; distinguishes scheduling from execution and cancellation. Execution requires a ready operation and, when specified, a completed predecessor. The contract's minimum delay is a floor; an operation can be scheduled with a longer delay. Granting executor access to the zero address allows anyone to execute an eligible operation. It does not authorize them to invent an unscheduled payload.&lt;/p&gt;

&lt;p&gt;The example matrix assumes governed timelock administration. Check the actual admin set, including deployment-time privileges. A separate administrator may be able to change role membership outside the intended governance procedure. OpenZeppelin describes self-administration and its liveness trade-offs in its &lt;a href="https://docs.openzeppelin.com/contracts/5.x/access-control" rel="noopener noreferrer"&gt;access-control guide&lt;/a&gt;. Remove bootstrap authority only after the permanent proposal and execution routes have been verified.&lt;/p&gt;

&lt;p&gt;Suppose a reviewed upgrade is ready but the execution transaction fails because the migration encounters an unexpected account state. A ready timestamp does not make a failing payload correct. Preserve the failure evidence, determine whether the same operation remains executable and cancel it when the plan changes. Replacing calldata requires a newly identified operation and the applicable review cycle.&lt;/p&gt;

&lt;p&gt;Keep a separate operational expiry for review evidence. A queued operation may still be technically executable after its simulation or approval context becomes stale. If the contract has no maximum execution window, an internal document cannot enforce one against another authorized executor. Cancel stale operations on-chain or implement the required expiry constraint before relying on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Size the delay around an available response
&lt;/h2&gt;

&lt;p&gt;Choosing forty-eight hours because another protocol uses it leaves the central question unanswered: what can an affected person accomplish during that interval? Write down who detects a proposal, who interprets it and what meaningful action remains available before execution.&lt;/p&gt;

&lt;p&gt;Consider a hypothetical delay of forty-eight hours. Detection takes six hours, technical review takes twelve and an allowed withdrawal takes thirty-six. A sequential response needs fifty-four hours before any contingency margin. That configuration cannot support the claimed exit window. The arithmetic describes this scenario only; real response paths may overlap or face additional constraints.&lt;/p&gt;

&lt;p&gt;Include weekends, signer availability and the dependencies of the exit transaction. If withdrawals require an operator signature, that operator must be part of the response model. If pausing deposits also blocks withdrawals, the emergency setting changes the meaning of the timelock. Show that consequence in the incident runbook and user-facing control description.&lt;/p&gt;

&lt;p&gt;Different changes can justify different waiting periods. A routine parameter adjustment and a replacement implementation need not share the same risk assessment. However, a policy document cannot create per-action enforcement by itself. Explain which delays the deployed contracts enforce and which longer waits remain a governance commitment.&lt;/p&gt;

&lt;p&gt;An emergency may require stopping exposure faster than an implementation review can finish. A predesigned pause function can provide that response while a correction follows the upgrade path. When the exploit is outside the pause scope, describe the residual exposure explicitly. Making the delay smaller after an incident begins is not a substitute for having designed an effective containment action beforehand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Review the multisig as a system of permissions
&lt;/h2&gt;

&lt;p&gt;A three-of-five threshold means three valid owner approvals are needed for the ordinary owner transaction path. It does not prove that three independent people reviewed the change. Two keys held by one person, a shared recovery account or a common signing workstation can reduce the practical independence of the set.&lt;/p&gt;

&lt;p&gt;Assign each signer a concrete verification task. One can reproduce the target and calldata, another can inspect the migration evidence and another can compare the live authority state with the reviewed snapshot. The assignments do not replace complete understanding, but they make three identical clicks less likely to pass for three distinct checks.&lt;/p&gt;

&lt;p&gt;Safe's &lt;a href="https://docs.safe.global/advanced/smart-account-concepts" rel="noopener noreferrer"&gt;smart-account concepts&lt;/a&gt; describe owner transactions and module transactions as separate paths. Enabled modules can execute through the Safe under their own authorization logic. That makes the module inventory part of the authority review, even when a dashboard prominently displays the owner threshold.&lt;/p&gt;

&lt;p&gt;The Safe documentation states:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;A malicious module can take over a Safe.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Attribution: Safe, &lt;a href="https://docs.safe.global/advanced/smart-account-modules" rel="noopener noreferrer"&gt;Safe Modules&lt;/a&gt;. This warning belongs beside the signer review because adding a module can change what the owner threshold actually protects.&lt;/p&gt;

&lt;p&gt;For every enabled module, identify its controller, permitted targets and ability to change its own policy. Inspect guards and recovery mechanisms as well. Safe's &lt;a href="https://docs.safe.global/advanced/smart-account-guards" rel="noopener noreferrer"&gt;guard documentation&lt;/a&gt; warns that a broken or malicious guard can block transaction execution. Verify which transaction paths a particular guard covers; do not infer that one guard constrains every module route.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://pharosproduction.com/services/smart-contracts-development/" rel="noopener noreferrer"&gt;smart-contract upgrade procedures&lt;/a&gt; described by Pharos Production include timelocks, multisigs and fork rehearsals. For this review, the useful application is to rehearse the complete authority chain, including account extensions, rather than testing only the final upgrade function. No signer count makes that rehearsal unnecessary.&lt;/p&gt;

&lt;h2&gt;
  
  
  Give emergency controls a narrow state transition
&lt;/h2&gt;

&lt;p&gt;Define an emergency action by the exposure it stops. Pausing new deposits can prevent additional funds entering a vulnerable path. Disabling a particular borrowing route can contain a pricing problem. Neither description establishes what happens to existing positions, withdrawals or integrations that call another entry point.&lt;/p&gt;

&lt;p&gt;Write the allowed transition precisely: normal deposits become disabled, while a separately verified withdrawal path remains available. Enumerate the selectors affected by that transition and the paths that remain active. If the contract cannot enforce this distinction, adjust the claim and evaluate the consequences of its actual global pause behavior.&lt;/p&gt;

&lt;p&gt;The guardian should not inherit arbitrary execution, implementation replacement or role-administration power merely because incident response is urgent. A narrow pause role reduces the set of harmful changes that a compromised guardian can make. It can still cause an outage, so the architecture also needs a way to remove or replace it.&lt;/p&gt;

&lt;p&gt;Unpause deserves its own acceptance condition. The team should establish which defect was corrected, which state was inspected and which operations were rehearsed before reopening. A separate unpause route can reduce the chance that the same compromised emergency key immediately restores the vulnerable path.&lt;/p&gt;

&lt;p&gt;Some protocols deliberately give an emergency council upgrade authority. Document that as a second upgrade path, with its own authorization requirements and complete blast radius. Calling it an emergency control does not make its implementation-changing power smaller. Users and reviewers need to know whether it can bypass the routine delay.&lt;/p&gt;

&lt;p&gt;Avoid implying that the pause flag blocks every administrative action. An upgrade function, another contract or an alternate routing path may remain callable. Test the relevant behavior through each public entry point. The operational question is what stops in the deployed system, including the operations the team intended to preserve.&lt;/p&gt;

&lt;p&gt;A pause can also shift risk to another participant. An integration may keep accepting deposits while its downstream vault rejects them, leaving funds waiting in an intermediary. Map that intermediate state, its refund path and the party responsible for communicating the interruption. The contract event reports that a switch changed; it does not prove the surrounding product has stopped offering the affected operation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Separate migration safety from authorization
&lt;/h2&gt;

&lt;p&gt;An authorized upgrade can still corrupt state. Review the change to storage and initialization independently of the decision about who may execute it. OpenZeppelin's &lt;a href="https://docs.openzeppelin.com/upgrades-plugins/writing-upgradeable" rel="noopener noreferrer"&gt;upgradeable-contract guidance&lt;/a&gt; explains storage-layout restrictions and initializer handling. Pin the dependency version used by the project and apply the matching validation workflow.&lt;/p&gt;

&lt;p&gt;Bind the release packet to the implementation address, deployed runtime code and initialization payload. Include the compiler configuration and relevant linked libraries. A source commit is useful provenance, but it cannot alone demonstrate that the address in the scheduled transaction contains the reviewed build.&lt;/p&gt;

&lt;p&gt;Use a rehearsal state that includes awkward accounts. For a vault, that might mean outstanding withdrawal requests, accrued fees and positions near a limit. Check value-preservation and access-control properties after migration. Include at least one user flow through the proxy, since calling the implementation directly does not exercise the same storage context.&lt;/p&gt;

&lt;p&gt;Suppose the new implementation appends a field and initializes it successfully, then later user transactions populate new state. Returning the implementation pointer to the previous version does not reverse those writes. A rollback plan therefore needs a state-compatibility argument and an explicit treatment of activity that occurred after the upgrade.&lt;/p&gt;

&lt;p&gt;When that argument cannot be made, recovery may require a forward fix or a separately authorized migration. Record the decision before execution. An old implementation address in a runbook is only a potential target; it is not evidence that restoring it will preserve balances or restore the intended permissions.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rehearse lost keys and hostile cancellation
&lt;/h2&gt;

&lt;p&gt;Availability failures belong in the authority design. If a governance multisig loses enough owners to miss its threshold, the team may be unable to schedule the very transaction that would restore control. If a self-administered timelock has no usable proposer, waiting longer will not manufacture one.&lt;/p&gt;

&lt;p&gt;Distinguish owner rotation inside a Safe from replacing that Safe as a timelock role holder. They involve different authorization paths. A surviving Safe quorum may rotate an owner using the Safe's controls. Replacing an unavailable proposer requires another route that already has sufficient authority in the timelock. The matrix must show which route exists.&lt;/p&gt;

&lt;p&gt;A canceller introduces a different liveness risk. An actor able to cancel pending governance operations may also cancel an operation intended to remove that actor's cancellation privilege. A delay therefore does not guarantee that a hostile canceller can eventually be removed. Do not put that promise in the recovery column unless the deployed design actually supports it.&lt;/p&gt;

&lt;p&gt;Resolve the trade-off explicitly. Some systems accept that veto power as part of their governance model. Others constrain cancellation or establish a separate, carefully bounded recovery mechanism. Every extra mechanism adds another authority path to inspect. A secret recovery key cannot count as both undisclosed and part of a transparent trust model.&lt;/p&gt;

&lt;p&gt;Run the exercise with one unavailable signer and then with a compromised signer. Record what still works, who can interrupt a queued operation and whether the recovery route can itself be blocked. An exercise that ends with contacting an unnamed administrator has discovered missing governance, not completed recovery.&lt;/p&gt;

&lt;p&gt;Rehearse voluntary retirement too. Removing upgrade authority can eliminate one future change path, but it may also eliminate the only route to correct a defect. Before that irreversible governance decision, identify which emergency powers remain and whether they can be retired separately. A permanently paused contract with no usable recovery function has very different consequences from an immutable contract whose normal user operations remain available.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bind monitoring to the reviewed transaction
&lt;/h2&gt;

&lt;p&gt;Prepare a compact operation record before gathering approvals. It should identify the chain, proxy and upgrade gate, then bind the implementation and calldata to the proposed state transition. Keep the observed authority configuration beside it so a reviewer can notice a changed signer set or a newly enabled module.&lt;/p&gt;

&lt;p&gt;The minimum useful record contains the following fields:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Network identifier, inspected block number and block hash.&lt;/li&gt;
&lt;li&gt;Proxy address, current implementation and actual upgrade controller.&lt;/li&gt;
&lt;li&gt;Proposed implementation, runtime-code hash and initialization calldata.&lt;/li&gt;
&lt;li&gt;Timelock operation identifier, predecessor, salt and earliest execution time.&lt;/li&gt;
&lt;li&gt;Proposer, executor, canceller, relevant role administrators and multisig configuration.&lt;/li&gt;
&lt;li&gt;Rehearsal inputs, expected postconditions and accepted recovery limitations.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Monitor configuration changes as well as proposal events. Owner rotation, threshold changes, role grants and module enablement can invalidate an earlier review without changing the upgrade calldata. Record what invalidates approval and who is responsible for canceling or replacing the queued operation when that happens.&lt;/p&gt;

&lt;p&gt;After execution, compare both emitted events and resulting state with the expected change. A transaction receipt alone does not show that every intended postcondition holds. Read the implementation reference again, inspect critical roles and exercise the representative user path at an appropriate confirmation point for that network.&lt;/p&gt;

&lt;p&gt;Keep failed and canceled attempts in the same history. They explain why a replacement operation exists and prevent a later operator from reviving an obsolete plan. Monitoring should lead to a defined response: notification, cancellation, containment or renewed review. A dashboard that only records the bad event after execution provides a different control from one that enables intervention beforehand.&lt;/p&gt;

&lt;p&gt;Decide who receives an alert when the normal responder is unavailable. Store a plain-language interpretation alongside decoded transaction data so the fallback operator can distinguish a routine owner rotation from a newly opened execution path. That handover is part of making the delay usable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Accept the design through adversarial questions
&lt;/h2&gt;

&lt;p&gt;Review the matrix with the people expected to operate it. Start with a direct call from each privileged account and ask whether that call can change the implementation without the recorded delay. Repeat the question for role administration, Safe modules and any contract allowed to forward arbitrary calls.&lt;/p&gt;

&lt;p&gt;Next, examine timing boundaries. An attempt before readiness should fail. An executed operation should not execute again as the same completed operation. A canceled operation should not remain immediately executable under its canceled schedule. Verify rescheduling behavior against the chosen implementation and check that monitoring distinguishes the replacement from its predecessor.&lt;/p&gt;

&lt;p&gt;Challenge the emergency boundary separately. Prove the guardian can stop the intended exposure and cannot perform an unrelated administrative transition. Test that preserved withdrawals really remain possible in the paused state. Then exercise the approved unpause route with the corrected implementation and the actual authorization configuration.&lt;/p&gt;

&lt;p&gt;Finally, remove an expected dependency from the rehearsal: a signer, a proposer, an execution service or the usual interface. Identify which operations remain available through documented tools and which become impossible. Preserve impossible outcomes as constraints requiring a design decision. They are more useful than a successful demonstration that quietly assumes every privileged actor is online.&lt;/p&gt;

&lt;p&gt;The acceptance result should be a versioned matrix tied to a release and a deployment snapshot. Each recovery claim needs supporting evidence; each exception needs a named decision owner. That gives the next engineer a concrete basis for deciding whether an upgrade may proceed when the people, permissions or operating conditions have changed.&lt;/p&gt;

&lt;h2&gt;
  
  
  More insights to read
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;a href="https://www.bulbapp.io/p/e2580ef7-324b-442d-b818-92b8c8442b3d/five-smart-contract-upgrade-patterns-compared" rel="noopener noreferrer"&gt;Five Smart Contract Upgrade Patterns Compared&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-1-222fe44ee14c" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dmytronasyrov.medium.com/upgradeable-solidity-smart-contracts-part-1-versioning-7e6e97cafc28" rel="noopener noreferrer"&gt;Upgradeable Solidity Smart Contracts. Part 1: Versioning&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/smart-contracts-their-potential-and-real-limitations-part-2-40942c055d79" rel="noopener noreferrer"&gt;Smart Contracts. Their Potential and Real Limitations. Part 2&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://medium.com/pharos-production/web3-smart-contracts-oracles-part-1-3905b127c01d" rel="noopener noreferrer"&gt;Web3. Smart Contracts. Oracles. Part 1&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  About the author
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl02tar7chezznck9ecyt.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fl02tar7chezznck9ecyt.jpg" alt="Portrait of Dmytro Nasyrov wearing a dark suit and light blue shirt against a dark background." width="800" height="800"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;small&gt;Dmytro Nasyrov. Photo supplied by the author.&lt;/small&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Written by &lt;a href="https://pharosproduction.com/dmytro-nasyrov/" rel="noopener noreferrer"&gt;Dmytro Nasyrov&lt;/a&gt; PhD, software architect with 24 years of production experience. Dmytro is the founder and CTO of Pharos Production. He works on production software architecture for FinTech, AI, Web3 and blockchain systems.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>web3</category>
      <category>security</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Smart Contract CI/CD: Foundry, Slither, and Audit Gates</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Wed, 02 Sep 2026 07:23:19 +0000</pubDate>
      <link>https://dev.to/pharos_production/smart-contract-cicd-foundry-slither-and-audit-gates-4ael</link>
      <guid>https://dev.to/pharos_production/smart-contract-cicd-foundry-slither-and-audit-gates-4ael</guid>
      <description>&lt;p&gt;A green CI run can still approve an unsafe smart contract release. Unit tests may run against one commit while the audit covers another. Slither may pass because its configuration changed. A deployer may reconstruct constructor arguments from chat history.&lt;/p&gt;

&lt;p&gt;A useful smart contract CI/CD pipeline must prove that one source commit passed every release gate and produced the artifact intended for deployment. This tutorial connects Foundry testing and Slither analysis to an audit-scope check, then ends with a deployment rehearsal. A failed gate blocks the release; it does not become a warning in a dashboard.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with one release object
&lt;/h2&gt;

&lt;p&gt;The pipeline needs a durable identity before it needs more tools. Create a release manifest from the candidate commit and keep every later artifact bound to it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"release"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"v1.4.0"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceCommit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;FULL_GIT_SHA&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"solc"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;PINNED_VERSION&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"foundry"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;PINNED_VERSION&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chainId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;TARGET_CHAIN_ID&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"auditScopeCommit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;AUDITED_GIT_SHA&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deploymentScript"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"script/Deploy.s.sol:Deploy"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Generate the manifest once. Jobs may append evidence, but they should not silently replace the source commit, compiler or target network.&lt;/p&gt;

&lt;p&gt;Use the same rule for every delivery team. A release assembled by &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;Pharos Production&lt;/a&gt;, an internal platform group or an external contractor should fail when its evidence cannot be reproduced from the declared commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Turn CI jobs into release gates
&lt;/h2&gt;

&lt;p&gt;Each job needs four fields: the question it answers, the evidence it retains, the condition that blocks release and the person who owns a failure.&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Gate&lt;/th&gt;
&lt;th&gt;Question&lt;/th&gt;
&lt;th&gt;Retained evidence&lt;/th&gt;
&lt;th&gt;Block condition&lt;/th&gt;
&lt;th&gt;Owner&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Reproducible build&lt;/td&gt;
&lt;td&gt;Can a clean runner compile the declared commit?&lt;/td&gt;
&lt;td&gt;tool versions, lockfile and build log&lt;/td&gt;
&lt;td&gt;hidden dependency or compiler drift&lt;/td&gt;
&lt;td&gt;build owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Unit and fuzz tests&lt;/td&gt;
&lt;td&gt;Do specified behaviors hold over bounded inputs?&lt;/td&gt;
&lt;td&gt;test result and failing seed&lt;/td&gt;
&lt;td&gt;any unexplained failure&lt;/td&gt;
&lt;td&gt;contract author&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Invariant campaign&lt;/td&gt;
&lt;td&gt;Do system properties survive call sequences?&lt;/td&gt;
&lt;td&gt;runs, depth, call metrics and counterexample&lt;/td&gt;
&lt;td&gt;broken invariant or meaningless exploration&lt;/td&gt;
&lt;td&gt;protocol reviewer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Static analysis&lt;/td&gt;
&lt;td&gt;Were findings produced under the reviewed configuration?&lt;/td&gt;
&lt;td&gt;Slither config, SARIF and disposition file&lt;/td&gt;
&lt;td&gt;unowned material finding or unexplained suppression&lt;/td&gt;
&lt;td&gt;security reviewer&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Audit scope&lt;/td&gt;
&lt;td&gt;Does the release remain inside the reviewed code boundary?&lt;/td&gt;
&lt;td&gt;audited commit and classified diff&lt;/td&gt;
&lt;td&gt;relevant unreviewed contract or deployment change&lt;/td&gt;
&lt;td&gt;audit owner&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Deployment rehearsal&lt;/td&gt;
&lt;td&gt;Can the exact script execute outside production?&lt;/td&gt;
&lt;td&gt;parameters, addresses, bytecode hashes and receipt&lt;/td&gt;
&lt;td&gt;manual step or output mismatch&lt;/td&gt;
&lt;td&gt;release operator&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Approval&lt;/td&gt;
&lt;td&gt;Are all required checks and owners resolved?&lt;/td&gt;
&lt;td&gt;protected-branch checks and release sign-off&lt;/td&gt;
&lt;td&gt;missing check, bypass or unresolved hard stop&lt;/td&gt;
&lt;td&gt;release manager&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The table is more important than a single workflow file. It prevents a common smart contract release pipeline failure: a green job with no retained artifact and no person responsible for interpreting it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7kacfn1ycz1p6nb0j292.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F7kacfn1ycz1p6nb0j292.png" alt="Commit-bound smart contract release pipeline from Foundry tests through invariants, Slither, audit scope and rehearsal, with failed gates routed to block" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Every gate retains evidence tied to the candidate commit; failure routes to BLOCK.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the Foundry CI path
&lt;/h2&gt;

&lt;p&gt;The official &lt;a href="https://github.com/foundry-rs/foundry-toolchain" rel="noopener noreferrer"&gt;Foundry toolchain action&lt;/a&gt; documents a GitHub Actions path that checks formatting and builds the Forge project before running its tests. A readable skeleton looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;smart-contract-release&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;FOUNDRY_PROFILE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ci&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;foundry&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;foundry-release-gate&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;persist-credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;
          &lt;span class="na"&gt;submodules&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;recursive&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;foundry-rs/foundry-toolchain@v1&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;lt;PINNED_FOUNDRY_VERSION&amp;gt;"&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;forge fmt --check&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;forge build --sizes&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;forge test -vvv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The tags make the example readable. In a production repository, pin third-party actions to reviewed commit SHAs and record the selected Foundry version in the release manifest. Reproducibility disappears when &lt;code&gt;stable&lt;/code&gt; means something different during a later incident review.&lt;/p&gt;

&lt;p&gt;Do not treat &lt;code&gt;forge test&lt;/code&gt; as one undifferentiated signal. Separate fast unit tests from fork tests and longer invariant campaigns when they have different dependencies or owners. A fork test also needs the reference block number and RPC provenance recorded with its result.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the invariant gate observable
&lt;/h2&gt;

&lt;p&gt;Foundry's &lt;a href="https://getfoundry.sh/forge/invariant-testing" rel="noopener noreferrer"&gt;invariant testing&lt;/a&gt; executes randomized call sequences and checks declared invariants after each call. Its &lt;code&gt;runs&lt;/code&gt; and &lt;code&gt;depth&lt;/code&gt; settings define the campaign, while handler call metrics expose what the fuzzer actually reached. Josselin Feist, the author of Slither, set a higher assurance threshold for this work:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;“Writing smart contracts requires a higher level of security assurance than most other fields of software engineering.”&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Attribution: Josselin Feist, then Engineering Director of the Blockchain team at Trail of Bits, "&lt;a href="https://blog.trailofbits.com/2025/02/12/the-call-for-invariant-driven-development/" rel="noopener noreferrer"&gt;The call for invariant-driven development&lt;/a&gt;," Trail of Bits, February 12, 2025. In CI, that higher bar means an invariant result needs evidence that the campaign reached the relevant states, not only a green exit code.&lt;/p&gt;

&lt;p&gt;A passing invariant job is weak evidence when most calls revert or important selectors are never targeted. Retain at least these fields with the result:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Foundry version and profile.&lt;/li&gt;
&lt;li&gt;invariant contract and declared properties.&lt;/li&gt;
&lt;li&gt;runs, depth, timeout and failure seed.&lt;/li&gt;
&lt;li&gt;target selectors, handler call counts, reverts and discarded inputs.&lt;/li&gt;
&lt;li&gt;the minimized counterexample when a property fails.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Set thresholds from the protocol's state space and CI budget, not from a generic template. The release gate should fail on a broken property. It should also fail when the campaign did not exercise the state transitions that give the property meaning.&lt;/p&gt;

&lt;p&gt;Fragmented ownership is the central problem this pipeline removes: testing, analysis, audit scope and deployment cannot live in separate handoffs. Pharos Production addresses that problem through an &lt;a href="https://pharosproduction.com/services/blockchain-development-company/" rel="noopener noreferrer"&gt;audit-first blockchain development workflow&lt;/a&gt; that connects contract engineering, testing, security review and deployment in one delivery path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Treat Slither output as a disposition queue
&lt;/h2&gt;

&lt;p&gt;The official &lt;a href="https://github.com/crytic/slither-action/blob/main/README.md" rel="noopener noreferrer"&gt;Slither Action&lt;/a&gt; can fail on a selected severity, load a repository configuration and produce SARIF for GitHub code scanning.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;  &lt;span class="na"&gt;slither&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slither-release-gate&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;permissions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;contents&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;read&lt;/span&gt;
      &lt;span class="na"&gt;security-events&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;write&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v6&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;persist-credentials&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run Slither&lt;/span&gt;
        &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;crytic/slither-action@v0.4.2&lt;/span&gt;
        &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slither&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;fail-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
          &lt;span class="na"&gt;sarif&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;results.sarif&lt;/span&gt;
          &lt;span class="na"&gt;slither-config&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;slither.config.json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pin the action and analyzer version under the same policy as Foundry. Then retain a disposition for every material result: fixed, accepted with a technical reason or assigned to an owner with a blocking deadline.&lt;/p&gt;

&lt;p&gt;An inline suppression is a code change. Review it like one. The gate should reject a suppression without the detector name, bounded scope, technical rationale and reviewer. It should also expose configuration diffs, because deleting a detector can make a pipeline green without making the contract safer.&lt;/p&gt;

&lt;h2&gt;
  
  
  Block code that escaped the audit boundary
&lt;/h2&gt;

&lt;p&gt;Store the audit's repository URL, scope commit, compiler settings, exclusions and report identifier in &lt;code&gt;audit-scope.json&lt;/code&gt;. The release job can then detect whether contract or deployment code changed after that boundary.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;AUDITED_SHA&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.auditScopeCommit'&lt;/span&gt; release-manifest.json&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt; git diff &lt;span class="nt"&gt;--quiet&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AUDITED_SHA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;...HEAD &lt;span class="nt"&gt;--&lt;/span&gt; src/ script/&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="k"&gt;then
  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Contract or deployment code changed after the recorded audit scope."&lt;/span&gt;
  git diff &lt;span class="nt"&gt;--stat&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$AUDITED_SHA&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;...HEAD &lt;span class="nt"&gt;--&lt;/span&gt; src/ script/
  &lt;span class="nb"&gt;exit &lt;/span&gt;1
&lt;span class="k"&gt;fi&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This deliberately blocks any relevant diff. A reviewer may clear the gate only by recording its disposition: out of scope with a reason, covered by a focused review or included in a new audit boundary. Changing the stored commit without that evidence defeats the control.&lt;/p&gt;

&lt;p&gt;An audit gate does not claim that reviewed code is safe. It answers a narrower question: is the release candidate still the code that received the recorded review?&lt;/p&gt;

&lt;h2&gt;
  
  
  Rehearse the deployment without production authority
&lt;/h2&gt;

&lt;p&gt;Run the exact deployment script against Anvil, a fork or a designated test network. Use the same contract selection, constructor arguments and post-deployment assertions intended for production, but never load a production signing key into pull-request CI.&lt;/p&gt;

&lt;p&gt;The rehearsal should produce a machine-readable receipt containing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"sourceCommit"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;FULL_GIT_SHA&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"chainId"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;REHEARSAL_CHAIN_ID&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"deployer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;NON_PRODUCTION_ADDRESS&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"contracts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"name"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Treasury"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"address"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;DEPLOYED_ADDRESS&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"runtimeBytecodeHash"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"&amp;lt;HASH&amp;gt;"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"postDeployChecks"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"passed"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Fail the gate if the script needs an undocumented console command, if expected roles differ or if the bytecode cannot be traced to the build artifact. A successful transaction alone is not a deployment rehearsal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make the gates enforceable
&lt;/h2&gt;

&lt;p&gt;GitHub's &lt;a href="https://docs.github.com/en/repositories/configuring-branches-and-merges/in-your-repository/managing-protected-branches/about-protected-branches" rel="noopener noreferrer"&gt;protected-branch documentation&lt;/a&gt; explains that required status checks must reach an accepted state before changes can merge. Give every release job a unique, stable name and require those checks on the protected release branch.&lt;/p&gt;

&lt;p&gt;Where available, bind a required check to its expected GitHub App. Apply the rule to administrators if your threat model includes emergency bypasses becoming routine. Keep production deployment in a separately approved environment; passing CI should create a releasable packet, not silently exercise mainnet authority.&lt;/p&gt;

&lt;p&gt;The minimum packet contains the manifest, tool versions, Foundry results, invariant metrics, Slither report and dispositions, audit-scope record, post-audit diff decision, rehearsal receipt and named approval. If one item cannot be bound to the candidate commit, the smart contract CI/CD pipeline has found a release blocker before mainnet did.&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>web3</category>
      <category>security</category>
      <category>devops</category>
    </item>
    <item>
      <title>Smart Contract Release Audit: 8 Repository Artifacts</title>
      <dc:creator>Dmytro Nasyrov</dc:creator>
      <pubDate>Thu, 27 Aug 2026 10:45:19 +0000</pubDate>
      <link>https://dev.to/pharos_production/how-to-choose-a-smart-contract-development-company-8-repository-checks-9m</link>
      <guid>https://dev.to/pharos_production/how-to-choose-a-smart-contract-development-company-8-repository-checks-9m</guid>
      <description>&lt;p&gt;An auditable smart contract release is a chain of evidence, not a green CI badge. The repository should bind the build, threat model, invariant tests, privileged roles, deployment rehearsal, audit scope and handover runbook to one commit.&lt;/p&gt;

&lt;p&gt;This guide turns that chain into eight repository checks and a 60-minute technical review. Use it to determine whether a delivery process can reproduce and explain one EVM release. A failed build, unmapped privileged authority or audit report without a scope commit blocks the review.&lt;/p&gt;

&lt;p&gt;This is a release-audit framework, not a security certification.&lt;/p&gt;

&lt;h2&gt;
  
  
  A release claim needs a repository artifact
&lt;/h2&gt;

&lt;p&gt;"Security-first" is not evidence. A threat model and a failing test are evidence. So are a CI result and a deployment manifest.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://docs.soliditylang.org/en/latest/security-considerations.html" rel="noopener noreferrer"&gt;Solidity security guidance&lt;/a&gt; recommends code review and testing. It also discusses audits, small modular contracts and fail-safe design. A release review should therefore inspect how those practices appear in the repository, not merely ask whether the team uses them.&lt;/p&gt;

&lt;p&gt;A neutral rule matters here: &lt;a href="https://pharosproduction.com" rel="noopener noreferrer"&gt;Pharos Production's company profile&lt;/a&gt; should receive no credit for a claim that cannot be mapped to an inspectable artifact either. Apply the same test to any team presenting the release.&lt;/p&gt;

&lt;p&gt;Confidential client work does not invalidate this approach. The delivery team can provide a sanitized repository, a public project, an internal reference implementation or a live screen-share with sensitive names removed. You do not need customer source code. You need proof that the release process exists and can be reproduced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Freeze one release as the review boundary
&lt;/h2&gt;

&lt;p&gt;Select one recent project close to the target chain and risk model. The review package should identify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the exact commit being demonstrated.&lt;/li&gt;
&lt;li&gt;compiler and framework versions, plus the dependency lock.&lt;/li&gt;
&lt;li&gt;reproducible build commands and CI entry points.&lt;/li&gt;
&lt;li&gt;the threat model with declared system invariants.&lt;/li&gt;
&lt;li&gt;every role that can upgrade, pause, mint or administer the system.&lt;/li&gt;
&lt;li&gt;deployment scripts plus a non-production rehearsal record.&lt;/li&gt;
&lt;li&gt;audit scope, remediation changes and unresolved risks.&lt;/li&gt;
&lt;li&gt;the handover runbook with named owners.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not request seed phrases, private keys, customer data or proprietary business logic. A good evidence room proves the process without exposing secrets.&lt;/p&gt;

&lt;h2&gt;
  
  
  An eight-check repository scorecard
&lt;/h2&gt;

&lt;p&gt;Score each check from &lt;code&gt;0&lt;/code&gt; to &lt;code&gt;2&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;0&lt;/code&gt;: absent, verbal only or not reproducible.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;1&lt;/code&gt;: an artifact exists but is incomplete, stale or not bound to the demonstrated release.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;2&lt;/code&gt;: a reviewer can reproduce it and trace it to the same commit, configuration and release scope.&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Check&lt;/th&gt;
&lt;th&gt;Evidence to inspect&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;0&lt;/code&gt; looks like&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;2&lt;/code&gt; looks like&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1. Build provenance&lt;/td&gt;
&lt;td&gt;pinned compiler, lockfile, documented commands and CI workflow&lt;/td&gt;
&lt;td&gt;"It builds on our lead developer's laptop"&lt;/td&gt;
&lt;td&gt;a clean checkout builds with the documented toolchain and no hidden local step&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2. Threat model&lt;/td&gt;
&lt;td&gt;assets, actors, trust boundaries, external dependencies, abuse cases and invariants&lt;/td&gt;
&lt;td&gt;a generic security checklist&lt;/td&gt;
&lt;td&gt;named failure scenarios are mapped to controls and tests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3. Test design&lt;/td&gt;
&lt;td&gt;unit, fuzz, invariant, integration or fork tests selected for the system's risks&lt;/td&gt;
&lt;td&gt;a coverage percentage with no risk mapping&lt;/td&gt;
&lt;td&gt;the team can reintroduce a bounded defect and show the relevant test fail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4. Static analysis and review debt&lt;/td&gt;
&lt;td&gt;tool configuration, CI output, suppressions, manual-review notes and finding owners&lt;/td&gt;
&lt;td&gt;a green badge or tool logo&lt;/td&gt;
&lt;td&gt;every material finding is fixed, accepted with rationale or assigned for action&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5. Upgrade and admin authority&lt;/td&gt;
&lt;td&gt;immutable or proxy decision, role map, current controllers, emergency powers and storage-layout checks&lt;/td&gt;
&lt;td&gt;"We use a multisig" with no address or authority map&lt;/td&gt;
&lt;td&gt;every privileged action has a controller, change path and observable event&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6. Deployment rehearsal&lt;/td&gt;
&lt;td&gt;versioned scripts, network parameters, address manifest, verification steps and dry-run receipt&lt;/td&gt;
&lt;td&gt;manual console commands reconstructed from memory&lt;/td&gt;
&lt;td&gt;the team can replay a non-production deployment from the reviewed commit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7. Audit handoff&lt;/td&gt;
&lt;td&gt;scope commit, exclusions, report, remediation pull requests, retest and accepted residual risk&lt;/td&gt;
&lt;td&gt;an audit PDF with no repository reference&lt;/td&gt;
&lt;td&gt;each finding traces from the reviewed commit to a change and final disposition&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8. Operational handover&lt;/td&gt;
&lt;td&gt;monitoring, incident roles, pause or containment procedure, key rotation and upgrade runbook&lt;/td&gt;
&lt;td&gt;support described only in the proposal&lt;/td&gt;
&lt;td&gt;the operator can run the system without an undocumented team-only action&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Across the 8 checks, the maximum is 16, but the total is not the decision. A hard stop overrides the score. Adjust the depth of each check to value at risk. Privilege concentration and external dependencies also matter.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjerlgkfdwg6d7nq0pont.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjerlgkfdwg6d7nq0pont.png" alt="Repository release-audit flow from a claim through a commit-bound artifact, reproducibility and ownership checks to a score, with failed gates routed to zero or block" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;A claim earns points only after it is bound to one release, reproduced and assigned a disposition. Any failed hard gate routes to &lt;code&gt;0&lt;/code&gt; or &lt;code&gt;BLOCK&lt;/code&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Broader procurement questions such as team scope, regulatory fit, multi-chain experience and engagement evidence belong in a separate &lt;a href="https://pharosproduction.com/insights/comparisons/blockchain-development-companies/" rel="noopener noreferrer"&gt;blockchain development company evaluation framework&lt;/a&gt;. This repository review begins after one delivery system and one release boundary have been selected.&lt;/p&gt;

&lt;h2&gt;
  
  
  Check the test model, not the test count
&lt;/h2&gt;

&lt;p&gt;Line coverage says which lines executed. It does not say that the important property was asserted.&lt;/p&gt;

&lt;p&gt;Ask the team to name the properties that must survive arbitrary user actions. Examples include conservation of assets, bounded issuance, withdrawal availability and authorization of privileged state changes. Then locate those properties in the test suite.&lt;/p&gt;

&lt;p&gt;Foundry's &lt;a href="https://getfoundry.sh/forge/invariant-testing" rel="noopener noreferrer"&gt;invariant-testing documentation&lt;/a&gt; explains that invariant campaigns run randomized sequences of calls and assert the declared invariants after each call. The repository should also expose what the fuzzer actually explored. Inspect its targets and run depth, then examine reverts and handler behavior. A passing campaign that never reaches a meaningful state is weak evidence.&lt;/p&gt;

&lt;p&gt;One demonstration is especially useful: ask the team to make a reversible local change that violates a declared invariant, then run the relevant test. The point is not theater. It verifies that the test can detect the failure it claims to control.&lt;/p&gt;

&lt;h2&gt;
  
  
  Read static-analysis suppressions as engineering debt
&lt;/h2&gt;

&lt;p&gt;Static analysis is a gate only when its configuration and output are reviewable. Slither can analyze Solidity and Vyper projects, integrate with CI and emit machine-readable results, as described in the &lt;a href="https://github.com/crytic/slither" rel="noopener noreferrer"&gt;project documentation&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Do not award points for running &lt;code&gt;slither .&lt;/code&gt; alone. Inspect excluded detectors and path filters. Then review inline suppressions and the treatment of each material finding. A suppression needs a technical reason and a defined scope. It also needs an owner. Otherwise the tool may be green because the repository taught it not to look.&lt;/p&gt;

&lt;p&gt;Required status checks can prevent a protected GitHub branch from accepting changes until configured checks pass. That mechanism is documented in &lt;a href="https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/about-protected-branches" rel="noopener noreferrer"&gt;GitHub's protected-branch guidance&lt;/a&gt;. During due diligence, confirm that the security job is actually required on the release branch and that its expected source cannot be replaced casually.&lt;/p&gt;

&lt;h2&gt;
  
  
  Make upgrade authority explicit
&lt;/h2&gt;

&lt;p&gt;"Upgradeable" is not a feature checkbox. It changes the trust model.&lt;/p&gt;

&lt;p&gt;For proxy-based systems, request the proxy pattern, initializer logic, storage-layout comparison and the current authority that can change implementation code. OpenZeppelin's &lt;a href="https://docs.openzeppelin.com/upgrades-plugins/writing-upgradeable" rel="noopener noreferrer"&gt;upgradeable-contract guidance&lt;/a&gt; documents initializer and storage-layout constraints. &lt;a href="https://eips.ethereum.org/EIPS/eip-1967" rel="noopener noreferrer"&gt;ERC-1967&lt;/a&gt; defines standard implementation, beacon and optional admin storage slots for common proxy designs.&lt;/p&gt;

&lt;p&gt;The evidence room should answer four questions without a sales call:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Which address can upgrade, pause, mint, seize, recover or change critical parameters?&lt;/li&gt;
&lt;li&gt;What contract, multisig, timelock or governance path controls that address?&lt;/li&gt;
&lt;li&gt;Which event or monitor reveals a change?&lt;/li&gt;
&lt;li&gt;What happens if the controller is compromised or unavailable?&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;An immutable contract still needs an authority review. The answer may be "no upgrade path," but ownership, external dependencies and emergency behavior must remain visible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Bind the audit to the release
&lt;/h2&gt;

&lt;p&gt;An audit report describes a scope at a point in time. Record the repository URL and commit hash beside it. Preserve the compiler settings, excluded components and deployed addresses in the same record. Then trace every finding to a remediation pull request, retest result or explicit risk acceptance.&lt;/p&gt;

&lt;p&gt;This avoids a common evidence break: the audit covers one commit while the deployment comes from a later branch with unreviewed changes. The team does not need to claim that an audit guarantees safety. It needs to show which code was reviewed and what changed afterward.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://scs.owasp.org/SCSVS/" rel="noopener noreferrer"&gt;OWASP Smart Contract Security Verification Standard&lt;/a&gt; can help structure the threat model and review scope. Its control groups cover architecture and code as well as governance, authorization and external interactions. Treat it as a control reference, not proof that the repository satisfies those controls.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run a 60-minute technical review
&lt;/h2&gt;

&lt;p&gt;A bounded live session is more discriminating than another capability deck:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Time&lt;/th&gt;
&lt;th&gt;Reviewer asks the delivery team to do&lt;/th&gt;
&lt;th&gt;Evidence produced&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;0-10 minutes&lt;/td&gt;
&lt;td&gt;check out the named commit and build it with documented commands&lt;/td&gt;
&lt;td&gt;reproducible build or a concrete blocker&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10-25 minutes&lt;/td&gt;
&lt;td&gt;run one unit path and one risk-based fuzz or invariant test&lt;/td&gt;
&lt;td&gt;test output plus an explanation of the property being checked&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;25-40 minutes&lt;/td&gt;
&lt;td&gt;walk through roles, upgrade paths and one compromised-admin scenario&lt;/td&gt;
&lt;td&gt;privilege map and containment decision&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;40-50 minutes&lt;/td&gt;
&lt;td&gt;trace one audit finding from scope commit to remediation&lt;/td&gt;
&lt;td&gt;commit-bound audit trail&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50-60 minutes&lt;/td&gt;
&lt;td&gt;replay a non-production deployment and inspect the handover package&lt;/td&gt;
&lt;td&gt;deployment receipt, address manifest and runbook&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The session is not a coding contest. It tests whether the release evidence remains coherent under inspection.&lt;/p&gt;

&lt;h2&gt;
  
  
  Hard stops that override the score
&lt;/h2&gt;

&lt;p&gt;Block or pause the release review when any of these remains unresolved:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a clean checkout cannot build without an undocumented machine or manual step.&lt;/li&gt;
&lt;li&gt;the audit report cannot be tied to a commit and declared scope.&lt;/li&gt;
&lt;li&gt;upgrade, pause, mint or deployment authority cannot be mapped to current controllers.&lt;/li&gt;
&lt;li&gt;a material static-analysis or audit finding is suppressed without rationale and ownership.&lt;/li&gt;
&lt;li&gt;production deployment depends on unrecorded console actions.&lt;/li&gt;
&lt;li&gt;a critical invariant has no test or explicit compensating control.&lt;/li&gt;
&lt;li&gt;the operator cannot monitor or contain the system without undocumented team-only knowledge.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A release can score well on six checks and still fail on unknown upgrade authority. Do not let arithmetic hide a control that can replace the code after review.&lt;/p&gt;

&lt;h2&gt;
  
  
  Adapt the evidence request to the engagement
&lt;/h2&gt;

&lt;p&gt;During early discovery, finished code may not exist. Review the team's repository template and threat-model method instead. Inspect its CI gates and sample handover package separately. Mark future evidence as a delivery requirement.&lt;/p&gt;

&lt;p&gt;For non-EVM work, replace Solidity and Foundry with the target ecosystem's compiler and test framework. Substitute its analyzer and authority model for Slither and ERC-1967. The evidence jobs stay the same.&lt;/p&gt;

&lt;p&gt;A small immutable utility contract needs less ceremony than a protocol controlling treasury assets. It does not get a free pass on reproducible builds, authority mapping or release provenance. Scale the depth, not the existence, of the controls.&lt;/p&gt;

&lt;p&gt;Send this request before the sales call:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Please prepare a 60-minute technical review tied to one recent, representative release. First, show a sanitized repository or screen-share with a clean build, risk-mapped tests and static-analysis disposition. Then show the privileged-role map, non-production deployment rehearsal, commit-bound audit handoff and operational runbook. Do not share private keys or customer data. Omit confidential business logic as well.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The response shows whether another operator can reproduce the release without undocumented help.&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>web3</category>
      <category>security</category>
      <category>blockchain</category>
    </item>
  </channel>
</rss>
