<?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>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>
