Smart Contract Fuzz Testing

We design and develop full-cycle blockchain solutions: from smart contract architecture to launching DeFi protocols, NFT marketplaces and crypto exchanges. Security audits, tokenomics, integration with existing infrastructure.
Showing 1 of 1 servicesAll 1306 services
Smart Contract Fuzz Testing
Complex
~2-3 business days
FAQ
Blockchain Development Services
Blockchain Development Stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1214
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    852
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_logo-advance_0.png
    B2B Advance company logo design
    561
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    823

Audit of Upgradeable Smart Contracts

Upgradeable contracts are a compromise. On one hand, ability to fix bugs and add functionality without migration. On the other — complete rejection of immutability guarantees that make blockchain secure. Any upgrade is a potential attack point: if attacker gains control over upgrade mechanism, they can replace logic with arbitrary code and withdraw all funds. That's why auditing upgradeable contracts is more complex and longer than auditing regular ones.

Compound, Aave, Uniswap V3 use some form of upgradeability. But they invested hundreds of thousands of dollars in auditing exactly the upgrade mechanisms.

Upgradeability Patterns and Their Vulnerabilities

Transparent Proxy (EIP-1967)

Most common pattern from OpenZeppelin. Proxy stores implementation address in deterministic storage slot. Calls are delegated to implementation through delegatecall.

User → Proxy (storage) → delegatecall → Implementation (logic)

Critical vulnerability: storage collision. Proxy uses slot 0x360894... for implementation address. If implementation accidentally uses same slot (or admin slot, or initializer flag) — rewrite during upgrade can break entire contract.

// Dangerous pattern: variables in Implementation
contract VulnerableImpl {
    // Slot 0 in storage
    address public owner;        // COLLISION with proxy admin slot!

    // If Proxy stores admin in slot 0 — owner in implementation
    // will read/write proxy admin address
}

// Correct pattern: use namespaced storage (EIP-7201)
// or ensure impl starts with correct offset
contract SafeImpl {
    // When using OpenZeppelin TransparentUpgradeableProxy
    // implementation MUST NOT have variables in slots
    // reserved for proxy (0x360894..., 0xb53127...)
    // OpenZeppelin Upgrades Hardhat plugin checks this automatically
}

UUPS (Universal Upgradeable Proxy Standard, EIP-1822)

Upgrade logic is in implementation, not proxy. Proxy is simpler and cheaper to deploy.

UUPS vulnerability: selfdestruct in uninitialized implementation. If implementation contract is deployed and not initialized, attacker can call initialize directly on implementation (not through proxy), become owner, and call upgradeTo(maliciousContract) with selfdestruct. After selfdestruct, implementation — proxy becomes non-functional forever.

This happened with several protocols until OpenZeppelin added _disableInitializers() in implementation constructor:

contract MyContractV1 is UUPSUpgradeable, OwnableUpgradeable {
    constructor() {
        _disableInitializers(); // MANDATORY in every implementation
    }

    function initialize(address initialOwner) public initializer {
        __Ownable_init(initialOwner);
        __UUPSUpgradeable_init();
    }

    function _authorizeUpgrade(address newImplementation)
        internal override onlyOwner {}
}

Beacon Proxy

One Beacon stores implementation address, multiple Proxies point to this Beacon. One upgrade updates all proxies simultaneously. Used when need to deploy hundreds of identical contracts (e.g., vault per user).

Beacon vulnerability: centralization. Whoever controls Beacon controls all proxies. If Beacon owner is EOA or weakly protected multisig — catastrophic single point of failure.

What We Check During Audit

Initialization

Most common error in upgradeable contracts — incorrect initialization:

// WRONG: constructor in upgradeable contract doesn't work
contract BrokenUpgradeable {
    address public owner;

    constructor() {
        owner = msg.sender; // NOT called when deploying through proxy!
    }
}

// CORRECT: initializer function
contract CorrectUpgradeable is Initializable, OwnableUpgradeable {
    function initialize() public initializer {
        __Ownable_init(msg.sender);
    }
}

Initialization checklist:

  • All parent contracts initialized through __Parent_init() pattern
  • initializer modifier present (prevents repeated call)
  • _disableInitializers() in constructor
  • All state variables that should be initialized on deploy are covered

Storage Layout Compatibility

During upgrade, new implementation must have exactly same storage layout starting from slot 0. Adding new variables — only at end. Reordering, deletion, type changes — storage collision and data corruption.

// V1
contract MyContractV1 {
    uint256 public value;      // slot 0
    address public owner;      // slot 1
}

// V2 CORRECT: add at end
contract MyContractV2 {
    uint256 public value;      // slot 0 — unchanged
    address public owner;      // slot 1 — unchanged
    uint256 public newValue;   // slot 2 — new
}

// V2 WRONG: insert in middle
contract MyContractV2_BROKEN {
    uint256 public newValue;   // slot 0 — COLLISION with value!
    uint256 public value;      // slot 1 — was in slot 0!
    address public owner;      // slot 2 — was in slot 1!
}

Tools: @openzeppelin/upgrades-core checks storage layout compatibility automatically. In audit — manual verification through forge inspect ContractName storage-layout.

Access Control of Upgrade Functions

Who can call upgrade? This is central audit question:

  • EOA: unacceptable for production. One compromised key = loss of everything.
  • Gnosis Safe multisig: acceptable, requires M-of-N signatures.
  • Timelock: best practice. Upgrade queued for 48-72 hours, community can react.
  • Governor + Timelock: maximum protection through on-chain voting.
// Audit checks: is there timelock before upgrade?
function _authorizeUpgrade(address newImplementation) 
    internal override 
{
    // BAD: only onlyOwner without timelock
    // require(msg.sender == owner, "Not owner");
    
    // GOOD: only through timelock (= through governance voting)
    require(msg.sender == address(timelock), "Only timelock");
}

Invariant Preservation

After each upgrade, check: are contract invariants violated? If before upgrade totalSupply == sum(balances), this must be true after upgrade.

// V1 → V2 compatibility test
contract UpgradeTest is Test {
    function testUpgradePreservesState() public {
        // Deploy V1 through proxy, set state
        MyContractV1 v1 = deployV1();
        v1.setValue(42);
        v1.deposit{value: 1 ether}();

        // Upgrade to V2
        upgradeTo(address(new MyContractV2()));

        // Check state preserved
        MyContractV2 v2 = MyContractV2(address(proxy));
        assertEq(v2.value(), 42);
        assertEq(address(proxy).balance, 1 ether);

        // Check new V2 functionality
        v2.newFunction();
    }
}

Functional Changes on Upgrade

New implementation can change behavior of existing functions. Audit checks: are there changes that break existing user expectations or bypass protective mechanisms?

Example: V1 has limit on withdraw (max 10 ETH per transaction). V2 removes this limit — possibly intentional, but auditor must explicitly confirm.

Specific Attack Vectors

Upgrade + Reentrancy

During upgrade transaction execution (especially if upgrade logic has external calls), reentrancy is possible. Contract in intermediate state (between old and new logic) is vulnerable.

Delegatecall Injection

If implementation contains function with arbitrary delegatecall to external address — attacker can use this to execute code in proxy context (and its storage).

// CRITICALLY DANGEROUS in upgradeable contract
function execute(address target, bytes calldata data) external onlyOwner {
    target.delegatecall(data); // Attacker owner = full control over proxy storage
}

Selfdestruct in Implementation (EIP-6780 Partially Mitigates)

EIP-6780 (Dencun upgrade, March 2024) changed selfdestruct behavior: now selfdestruct in same transaction as CREATE works as before, but called in separate transaction — only transfers ETH, doesn't delete code. For UUPS vulnerability this is partial mitigation, not complete solution.

Audit Tools

Tool Purpose
@openzeppelin/hardhat-upgrades Automatic storage layout verification
forge inspect ... storage-layout Manual slot analysis
Slither Static analysis including upgrade patterns
Echidna / Medusa Fuzzing on invariant preservation
Foundry fork tests Upgrade tests on mainnet fork
Tenderly Upgrade transaction simulation

Audit Process

Phase 1: Static Analysis (3-5 days). Slither + manual proxy pattern analysis. Map storage slots V1, check _disableInitializers, upgrade function access control.

Phase 2: Storage Compatibility (2-3 days). Compare storage layouts V1 and V2. For each variable: slot, type, offset. Special attention to mappings and dynamic arrays (their layout is non-trivial).

Phase 3: Functional Changes (3-5 days). Build diff between V1 and V2 at function level. For each change: intentional? doesn't violate invariants? no new attack vectors?

Phase 4: Upgrade Mechanism (2-3 days). Full upgrade lifecycle: who initiates → timelock → execution → rollback possibility?

Phase 5: Testing (5-7 days). Fork tests on Foundry. Fuzzing of critical functions. Manual tests of edge cases.

Report and Remediation (3-5 days). Document findings, severity classification (Critical/High/Medium/Low/Informational), recommendations.

Full upgradeable contract audit mid-size: 3-5 weeks. Cost — after code volume and upgrade mechanism complexity assessment.