Skip to content

Top 35 Solidity Interview Questions and Answers [2026]

October 19, 2026 27 min read
Solidity interview guide by Second Talent
TL;DR: Solidity interviews in 2026 test whether a candidate can reason about storage, gas and every external call, then find the bug in a short contract. Candidates should know Solidity 0.8.37 (September 10, 2026), the 0.9.0 deprecations that started in 0.8.31, OpenZeppelin Contracts 5.7.0, and what Pectra (EIP-7702) and Fusaka changed on mainnet.

Since Solidity 0.8.31, the compiler warns that transfer and send are deprecated and scheduled for removal in 0.9.0, which ends a pattern older tutorials still teach as the safe way to pay out ETH. The current release is Solidity 0.8.37.

The questions below are for senior smart contract and blockchain developer interviews, with four find-the-bug rounds.

Key takeaways
  1. 1Fusaka caps a single transaction at 2^24 (16,777,216) gas under EIP-7825, so huge batch calls now need splitting.
  2. 2Since OpenZeppelin Contracts 5.6.0, ERC1967Proxy and TransparentUpgradeableProxy revert at deploy if no initialize call is passed.
  3. 3After Pectra, an EOA with an EIP-7702 delegation can run code, so tx.origin == msg.sender no longer proves the caller has no code.
  4. 4A TLOAD or TSTORE costs 100 gas, against 2,100 for a cold SLOAD, which is why reentrancy locks moved to transient storage.

EVM and Gas

1. How does Solidity lay out state variables in storage?

State variables go into 32-byte slots in declaration order, starting at slot 0, and consecutive variables smaller than 32 bytes share a slot when they fit.

The storage layout docs add the rules a candidate should know: structs and fixed arrays always start a new slot, a mapping takes one slot that stays empty while its values live at keccak256(key . slot), and a dynamic array stores its length in its slot and its data from keccak256(slot).

contract Packed {
    uint128 a;      // slot 0, low 16 bytes
    uint128 b;      // slot 0, high 16 bytes
    uint256 c;      // slot 1
    address owner;  // slot 2, 20 bytes
    bool paused;    // slot 2, packed next to owner
}

Packing saves gas only when the packed values are read or written together. Writing one uint128 on its own costs a read-modify-write of the whole slot.

A strong candidate also says that the layout is a public contract for upgradeable code, which questions 20 and 21 come back to.

2. When do you use calldata, memory and storage?

Use calldata for read-only external inputs, memory for temporary values you build or modify, and storage for references to state.

Calldata is read-only and avoids the copy into memory, so an external function that only reads an array or bytes parameter should take it as calldata.

The trap is assignment semantics. Assigning a storage struct to a memory variable copies it, so changes to the copy are lost. Assigning it to a storage local creates a reference, so changes write to state.

Interviewers like to show a loop that "updates" a memory copy of a user record and ask why nothing changed.

3. Why does the first read of a storage slot cost more than the second?

Since EIP-2929, the first access to a slot in a transaction is cold and costs 2,100 gas, and later reads of the same slot are warm and cost 100.

The EIP-2929 spec also charges 2,600 for the first touch of an account, and adds the 2,100 cold surcharge to an SSTORE on a cold slot.

Practical consequences a candidate should name:

  • Caching a state variable in a local inside a loop saves the warm reads, about 100 gas each, not 2,100.
  • Access lists (EIP-2930 transactions) can pre-warm slots and addresses a call will touch.
  • Gas numbers in tests depend on whether earlier calls in the same transaction already warmed a slot, so a benchmark that calls a function twice measures the warm path.

4. What is transient storage, and where does it go wrong?

Transient storage (EIP-1153) is a per-contract key-value store that is discarded at the end of the transaction, read with TLOAD and written with TSTORE at 100 gas each.

Per the EIP, writes made inside a frame that reverts are rolled back, and TSTORE inside a STATICCALL throws.

Solidity 0.8.24 exposed it in inline assembly, and 0.8.28 added the transient data location for state variables.

The language docs still limit it to value types: no mappings, arrays or structs, no locals, and no initializer on the declaration.

contract Guarded {
    bool transient locked;

    modifier nonReentrant() {
        require(!locked, "reentrant call");
        locked = true;
        _;
        locked = false;
    }
}

Two pitfalls separate senior answers.

First, a value left set leaks into later calls in the same transaction, which breaks composability when a router or multicall calls the contract twice; the docs recommend clearing transient storage at the end of every call.

Second, transient values are not a replacement for memory, because they survive across inner calls and revert differently.

5. What is the difference between call, delegatecall and staticcall?

call runs the target's code in the target's context, delegatecall runs the target's code in the caller's context, and staticcall is a call that reverts on any state change.

With delegatecall, storage writes land in the caller's storage, and msg.sender and msg.value stay those of the original call.

That is the whole basis of proxies and libraries with external functions, and also the source of their worst bugs. The target's storage layout is applied to the caller's slots, so a mismatch overwrites unrelated variables (question 25).

Calling delegatecall on a user-supplied address hands that user full control of the contract's storage and balance.

Solidity Language

6. What does checked arithmetic cover, and what does it miss?

Since 0.8.0, +, -, * and friends revert with a Panic(0x11) on overflow or underflow, unless the code sits in an unchecked { } block. Division and modulo by zero revert with Panic(0x12) even inside unchecked.

What it misses is where integer bugs live today:

  • Explicit narrowing casts truncate silently. uint8(uint256(300)) is 44, not a revert. OpenZeppelin's SafeCast exists for this.
  • Precision loss. Dividing before multiplying rounds to zero for small values, and rounding direction decides who absorbs the dust.
  • Inline assembly is never checked.
  • unchecked blocks that were safe when written stop being safe when a later change widens an input range.

7. How should a contract send ETH in 2026?

Use a low-level call{value: amount}(""), check the returned bool, and protect the function against reentrancy.

The Solidity 0.8.31 announcement deprecated send and transfer as an anti-pattern, and the compiler now prints "'transfer' is deprecated and scheduled for removal" on any use (we checked with solc 0.8.37).

The 2,300 gas stipend those functions forward was never a reliable reentrancy defense, and it breaks payments to smart wallets whose receive needs more gas.

The announcement is explicit that call "does not provide any reentrancy protection", so the answer must include checks-effects-interactions or a guard.

A strong candidate adds that a pull pattern, where recipients withdraw their own balance, avoids a failing recipient blocking everyone else in a loop.

8. Why can abi.encodePacked produce hash collisions?

Packed encoding concatenates dynamic values without length prefixes, so different inputs can produce the same bytes. abi.encodePacked("ab", "c") and abi.encodePacked("a", "bc") both encode to "abc", so their hashes match.

The ABI spec warns that packed encoding is ambiguous once more than one dynamic type is involved.

This matters wherever a hash is used as an identity: signature digests, Merkle leaves, commitment schemes and mapping keys. The fix is abi.encode, which pads and length-prefixes, or EIP-712 typed data for anything a user signs.

9. What can go wrong with a low-level call besides reentrancy?

Three things: it succeeds against an address with no code, it hides failures if the bool is ignored, and it copies whatever return data the callee sends.

The Solidity docs warn that call, delegatecall and staticcall return true when the account does not exist, so a call to a mistyped or self-destructed address looks like success.

High-level interface calls check that the target has code, and they revert if the callee returns nothing while the interface expects a value.

That is why a plain IERC20.transfer call reverts against tokens that return nothing, a case question 26 tests.

When a contract calls untrusted addresses, a senior candidate limits the gas it forwards and the return data it copies, so a hostile callee cannot make the caller run out of gas.

Security

10. What forms of reentrancy should a reviewer check for?

Single-function, cross-function, cross-contract and read-only reentrancy. All four happen because an external call hands control to code you do not own before your state is consistent.

Flowchart for reviewing a Solidity function that makes an external call: if state is written after the call, move the write above it using checks, effects, interactions; if other functions use that state, guard them with the same lock against cross-function reentrancy; if view functions read it mid-call, treat it as read-only reentrancy because other protocols may price off it; when all checks are clear, write a test that re-enters.
  • Single-function: the callee calls the same function again before the balance update.
  • Cross-function: the callee calls a different function that reads the stale state, for example transfer during withdraw.
  • Cross-contract: two contracts of the same protocol share state, and only one has a lock.
  • Read-only: a view function returns a mid-update value, and another protocol prices collateral from it.

The defenses are checks-effects-interactions first, then a lock shared by every function that touches the same state.

OpenZeppelin 5.5.0 added nonReentrantView to ReentrancyGuard and ReentrancyGuardTransient, a read-only version of the modifier aimed at the last case (v5.5.0 notes).

11. How do you design access control for a protocol with admin functions?

Give each privileged action its own role, put the powerful roles behind a multisig and a timelock, and never use tx.origin for authorization. Ownership with a single key is fine for a prototype and a liability for anything holding funds.

  • Role-based access (AccessControl) separates pausing, parameter changes and upgrades, so a compromised pauser key cannot upgrade the code.
  • Two-step ownership transfer (Ownable2Step) prevents handing a contract to a mistyped address.
  • A timelock on upgrades and parameter changes gives users time to exit.
  • Every initializer, setter and upgrade hook needs a check; the most common access control bug is a function that has none.

tx.origin checks let any contract the owner interacts with act as the owner, and since EIP-7702 they no longer reliably prove that the caller is an EOA without code (question 31).

12. How is a price oracle manipulated, and how do you defend against it?

An attacker moves the price source within one transaction, usually with a flash loan against a thin AMM pool, then borrows or liquidates against the false price before moving it back.

Any oracle that reads an AMM's current reserves or spot price is open to this.

Defenses a candidate should list:

  • Use an external feed such as Chainlink, and check the updatedAt value that latestRoundData returns (see the Data Feeds API reference) against the feed's heartbeat. A stale or zero answer must revert.
  • If an on-chain price is needed, use a time-weighted average over a window long enough that moving it costs more than the attack pays.
  • Add bounds: maximum deviation between two sources, and borrow caps on thin assets.
  • Watch for read-only reentrancy (question 10) as a second path to a false price.

13. How do you stop a signature from being replayed?

Bind the signed message to one use, one contract and one chain: include a nonce (or a used-digest flag), the chain ID and the verifying contract, and an expiry.

EIP-712 typed data puts the chain ID and verifying contract in the domain separator, so a signature for one deployment cannot be used on a fork or on a second deployment.

Two more details a senior candidate raises. Raw ecrecover returns address(0) for an invalid signature, so comparing against an unset signer variable accepts garbage; OpenZeppelin's ECDSA.recover reverts instead.

And because ECDSA signatures are malleable, the replay key must be the digest or a nonce, never the signature bytes; OpenZeppelin 5.5.0 notes its malleability protection is now partly deprecated, which makes that rule stricter.

14. What is front-running, and how do contracts defend against it?

Front-running is placing a transaction before a pending one to profit from it, and sandwiching wraps a victim's swap between a buy and a sell.

Pending transactions in the public mempool are visible, and block builders choose the order, so any value a transaction reveals before it is mined can be extracted (MEV).

  • Slippage limits and deadlines on every swap, set by the user, not hard-coded to zero or block.timestamp.
  • Commit-reveal for auctions, games and anything where seeing the input early is the attack.
  • Initialization in the deploy transaction, so nobody can call an initializer between deploy and setup (question 22).
  • Private order flow helps users, but the contract cannot assume it.

15. Which way should a protocol round, and why does it matter?

Round in favor of the protocol: round down what a user receives and round up what a user owes. Mixed rounding lets an attacker repeat a small operation many times and keep the dust each time, which has drained lending markets and vaults.

The ERC-4626 standard writes this into its rules: previewDeposit and convertToShares round down, while previewMint and previewWithdraw round up.

A candidate should also multiply before dividing, use a full-precision mulDiv for products that can exceed 256 bits, and write a fuzz test that checks a deposit followed by a withdraw never returns more than went in.

Tokens and Accounts

16. Which ERC-20 quirks break integrations?

Tokens that return nothing instead of true, tokens that return false instead of reverting, fee-on-transfer and rebasing tokens, and the approve race.

A protocol that accepts arbitrary tokens has to handle all of them or list the ones it does not support.

  • Missing or false return values: use OpenZeppelin's SafeERC20, which handles both.
  • Fee-on-transfer: credit the balance difference measured before and after the transfer, not the requested amount.
  • Rebasing: balances change without transfers, so cached balances drift.
  • Approve race: changing a non-zero allowance to another non-zero value lets the spender use both; use increase and decrease patterns or permit signatures.
  • Blocklists and pausing in some stablecoins can make a transfer to one address revert and block a whole batch.

17. ERC-721 or ERC-1155, and what does safeTransferFrom add?

ERC-721 tracks one owner per token ID; ERC-1155 tracks balances per ID and supports batch transfers, so it suits game items and editions where many copies share an ID.

Both have safe transfer functions that call the recipient's onERC721Received or onERC1155Received when the recipient is a contract, and revert if it does not return the expected selector.

That callback is an external call into code the sender does not control, so minting or transferring with the safe variants is a reentrancy point. Mint functions that update counters or supply after _safeMint have been exploited this way.

A senior candidate updates state first and treats every safe transfer as a call out.

18. What is the ERC-4626 inflation attack?

An attacker front-runs the first deposit into an empty vault, deposits a tiny amount, then donates assets directly so the exchange rate jumps and the victim's deposit rounds down to zero shares.

The OpenZeppelin ERC-4626 guide notes that the donation needed is about the size of the deposit being attacked.

OpenZeppelin's defense combines virtual shares and assets in the exchange rate with a decimals offset that gives shares more precision than the asset.

Per the guide, an offset of 0 already makes the attack unprofitable: the attacker loses at least as much as the victim's deposit. Larger offsets make that loss orders of magnitude bigger.

Other answers include seeding the vault with a dead-share deposit at deploy and letting users set a minimum shares out.

19. How does ERC-4337 account abstraction work?

Users sign UserOperation objects instead of transactions; bundlers collect them from a separate mempool and submit them through a singleton EntryPoint contract, which calls each account's validateUserOp and then executes the operation.

A paymaster can pay gas for the user. The ERC-4337 spec is now Final and needs no consensus change.

Interview follow-ups worth asking:

  • Why validation must be cheap and deterministic: bundlers simulate it, and validation code that reads changing state could make them pay for failed bundles.
  • How a paymaster limits who it sponsors and protects its deposit.
  • Library versions: OpenZeppelin 5.6.0 made EntryPoint v0.9 the default for its Account contract, and 5.7.0 dropped the draft- prefix from its ERC-4337 files, so imports changed.

Upgradeable Contracts

20. Compare transparent, UUPS and beacon proxies.

All three keep state in a proxy and forward calls with delegatecall; they differ in where the upgrade code lives. Per the OpenZeppelin 5.x proxy docs:

Grid comparing three proxy patterns in OpenZeppelin Contracts 5.x. Transparent: upgrade code in the proxy behind its own ProxyAdmin, upgraded by the ProxyAdmin owner, one proxy, and admin calls never reach the app. UUPS: upgrade code in the implementation, upgraded by whoever _authorizeUpgrade allows, one proxy, and a version without upgrade code freezes it. Beacon: upgrade code in the UpgradeableBeacon, upgraded by the beacon owner, every proxy on that beacon, and the beacon address is fixed at deploy.
  • Transparent: the proxy deploys its own ProxyAdmin and accepts upgrades only through it. Calls from the admin are never forwarded to the implementation.
  • UUPS: the implementation inherits UUPSUpgradeable, exposes upgradeToAndCall, and must override _authorizeUpgrade with an access check. Deploying a version without that code ends upgradeability.
  • Beacon: many BeaconProxy instances read their implementation from one UpgradeableBeacon, so one call upgrades all of them. Since v5 the beacon address is set once at construction.

In 5.x, UUPS contracts that report UPGRADE_INTERFACE_VERSION "5.0.0" only have upgradeToAndCall(address,bytes); the old upgradeTo is gone, and tooling that calls it fails.

21. How do storage collisions happen in upgrades, and how does ERC-7201 help?

A collision happens when a new implementation's variables map to slots that held different data: inserting a variable in the middle, reordering, changing a type, or changing the inheritance order.

Proxies avoid colliding with their own admin and implementation addresses by storing them at the ERC-1967 slots, which are hashes far from slot 0.

ERC-7201 applies the same idea to application state.

Each module keeps its variables in a struct rooted at keccak256(keccak256(id) - 1) & ~0xff, annotated @custom:storage-location erc7201:<id>, so adding a parent contract does not shift a child's variables.

A candidate should still run a storage layout diff, such as the OpenZeppelin upgrades plugins' validation, on every upgrade.

/// @custom:storage-location erc7201:example.vault
struct VaultStorage {
    uint256 totalAssets;
    mapping(address => uint256) shares;
}

bytes32 private constant VAULT_STORAGE =
    keccak256(abi.encode(uint256(keccak256("example.vault")) - 1)) & ~bytes32(uint256(0xff));

function _vault() private pure returns (VaultStorage storage $) {
    bytes32 slot = VAULT_STORAGE; // assembly cannot read a hashed constant directly
    assembly { $.slot := slot }
}

22. Why do upgradeable contracts use initializers, and what are the traps?

A proxy never runs the implementation's constructor, so setup moves into an initialize function guarded so it runs once.

The traps are an initializer anyone can call first, an implementation contract left uninitialized, and a parent initializer called twice or not at all.

  • Call _disableInitializers() in the implementation's constructor so nobody can initialize the implementation directly and take ownership of it.
  • Pass the initialize calldata to the proxy constructor so deploy and setup happen in one transaction. OpenZeppelin 5.6.0 made this mandatory: ERC1967Proxy and TransparentUpgradeableProxy revert with ERC1967ProxyUninitialized when no initialize call is given, unless _unsafeAllowUninitialized is overridden.
  • Use reinitializer(n) for version-specific setup in later upgrades.

Since 5.5.0, Initializable and UUPSUpgradeable are no longer transpiled into the upgradeable package; aliases remain until the next major release, and OpenZeppelin advises importing them from @openzeppelin/contracts.

Code Review Rounds

Each snippet below compiles with solc 0.8.37 (we compiled all four, plus the examples in questions 1, 4, 21 and 34). Give the candidate five minutes per snippet and ask for the bug, the exploit and the fix.

23. Find the bug: a vault with withdrawAll.

contract Vault {
    mapping(address => uint256) public balances;

    function deposit() external payable {
        balances[msg.sender] += msg.value;
    }

    function withdrawAll() external {
        uint256 amount = balances[msg.sender];
        require(amount > 0, "nothing to withdraw");
        (bool ok, ) = msg.sender.call{value: amount}("");
        require(ok, "send failed");
        balances[msg.sender] = 0;
    }
}

Bug: classic reentrancy. The balance is cleared after the external call, so an attacker's receive calls withdrawAll again and is paid the same balance repeatedly until the vault is empty.

Fix: set balances[msg.sender] = 0 before the call, and add a nonReentrant guard if other functions read balances.

The follow-up that separates seniors: if the last line were balances[msg.sender] -= amount, the nested calls would underflow on the way out and checked arithmetic would revert the whole transaction.

That accident is not a defense; the same code inside unchecked, or a balance reset to zero as here, drains the vault.

24. Find the bugs: a signature-based airdrop.

interface IERC20 {
    function transfer(address to, uint256 amount) external returns (bool);
}

contract Airdrop {
    IERC20 public immutable token;
    address public immutable signer;

    constructor(IERC20 token_, address signer_) {
        token = token_;
        signer = signer_;
    }

    function claim(address to, uint256 amount, uint8 v, bytes32 r, bytes32 s) external {
        bytes32 digest = keccak256(abi.encodePacked(to, amount));
        require(ecrecover(digest, v, r, s) == signer, "bad signature");
        token.transfer(to, amount);
    }
}

Bugs:

  • Replay: nothing records a used claim, so anyone who sees one valid signature can call claim until the contract is empty.
  • No domain: the digest has no chain ID or contract address, so the same signature works on a second deployment or another chain.
  • No expiry, so a leaked signature never stops working.
  • Zero signer: if signer_ is ever address(0), invalid signatures that make ecrecover return zero pass.
  • Unchecked transfer: the bool is ignored.

Fix: an EIP-712 digest over (to, amount, nonce, deadline), a claimed mapping or a per-user nonce, ECDSA.recover and SafeERC20. For a large airdrop, a Merkle root with a claimed bitmap avoids a hot signing key entirely.

25. Find the bug: a hand-written proxy.

contract SimpleProxy {
    address public implementation; // slot 0
    address public admin;          // slot 1

    constructor(address impl) {
        implementation = impl;
        admin = msg.sender;
    }

    function upgrade(address impl) external {
        require(msg.sender == admin, "not admin");
        implementation = impl;
    }

    fallback() external payable {
        address impl = implementation;
        assembly {
            calldatacopy(0, 0, calldatasize())
            let ok := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
            returndatacopy(0, 0, returndatasize())
            switch ok
            case 0 { revert(0, returndatasize()) }
            default { return(0, returndatasize()) }
        }
    }
}

contract TokenV1 {
    uint256 public totalSupply;                   // slot 0
    mapping(address => uint256) public balanceOf; // slot 1

    function mint(uint256 amount) external {
        totalSupply += amount;
        balanceOf[msg.sender] += amount;
    }
}

Bug: storage collision. Through the proxy, TokenV1.totalSupply is slot 0, which is the proxy's implementation. The first mint adds to the implementation address and points the proxy at an address with no code.

A delegatecall to it succeeds and returns no data, so the token stops working. A chosen amount can point it at attacker code.

Also: mint has no access control, and the proxy's own functions shadow any implementation function with the same selector. Fix: keep proxy state in ERC-1967 slots and use the OpenZeppelin proxies rather than a hand-written one.

26. Find the bugs: a staking contract.

interface IERC20 {
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

contract Staking {
    IERC20 public immutable token;
    mapping(address => uint256) public staked;
    uint256 public totalStaked;

    constructor(IERC20 token_) {
        token = token_;
    }

    function stake(uint256 amount) external {
        token.transferFrom(msg.sender, address(this), amount);
        staked[msg.sender] += amount;
        totalStaked += amount;
    }

    function unstake(uint256 amount) external {
        staked[msg.sender] -= amount;
        totalStaked -= amount;
        token.transfer(msg.sender, amount);
    }
}

Bugs:

  • Ignored return values: a token that returns false on a failed transferFrom lets a user stake nothing and be credited anyway, then unstake real tokens.
  • Tokens that return nothing make both calls revert, because the interface expects a bool.
  • Fee-on-transfer tokens deliver less than amount, so the contract owes more than it holds and the last users cannot unstake.

Fix: SafeERC20's safeTransferFrom and safeTransfer, and credit balanceAfter - balanceBefore in stake. The ordering in unstake is already correct: state changes come before the transfer.

Testing and Tooling

27. How do fuzz and invariant tests differ in Foundry?

A fuzz test runs one test function many times with random inputs; an invariant test calls random sequences of functions and checks properties that must hold after every call.

Per the Foundry testing config reference, fuzz tests default to 256 runs, and invariant tests default to 256 runs with a depth of 500 calls and fail_on_revert = false.

The invariant testing guide recommends handler contracts that wrap the target, bound inputs to realistic ranges and track ghost variables, and registering only the handler with targetContract.

An optional afterInvariant() hook runs once against the final state of each run, which suits expensive end-of-run checks.

Good invariants for a vault: total shares map to total assets within rounding, no user can withdraw more than they deposited plus yield, and the contract's token balance never drops below what it owes.

function testFuzz_depositWithdraw(uint256 amount) public {
    amount = bound(amount, 1, 1e30);
    deal(address(asset), alice, amount);
    vm.startPrank(alice);
    asset.approve(address(vault), amount);
    uint256 shares = vault.deposit(amount, alice);
    uint256 back = vault.redeem(shares, alice, alice);
    vm.stopPrank();
    assertLe(back, amount); // rounding never pays the user extra
}

28. Foundry or Hardhat 3: how do you choose?

Both now support tests written in Solidity, so the choice is about the team's stack and deployment workflow more than test syntax.

Foundry (stable v1.8.5, October 5, 2026) is Rust-based, with fuzzing, invariant testing and cheatcodes built in, and cast and anvil for chain work.

Hardhat 3 shipped on August 13, 2025 in beta, described by Nomic Foundation as ready for production, with Solidity tests as a first-class option, multichain support, a Rust-powered runtime and Hardhat Ignition for deployments.

The latest release is hardhat@3.20.0 (October 15, 2026). Teams with TypeScript deployment scripts and front-end integration often keep Hardhat; protocol teams that live in fuzzing often pick Foundry. Many run both.

29. How do gas costs work on an L2, and what changes in your code?

On a rollup, a transaction pays L2 execution gas plus a fee for publishing its data to Ethereum.

The OP Stack fee docs describe the L1 data fee as a function of the compressed transaction size and the L1 base fee and blob base fee, and on blob-enabled chains the blob price mostly sets it.

Pectra raised the blob target from 3 to 6 per block, which added rollup data capacity.

What changes in code: calldata size matters more than execution, so packing arguments and avoiding redundant data pays off, while saving a few hundred gas of execution matters less than on mainnet.

Senior candidates also check the EVM version.

Solidity 0.8.20 switched the default target to Shanghai and warned that PUSH0 would fail on chains that did not support it yet; 0.8.31 made osaka the default, so evmVersion should be set per target chain, not left at the default.

30. How do you prepare a codebase for an external audit?

Freeze the scope, write down what the system must guarantee, and fix everything cheap tools can find before auditors spend paid hours on it.

An audit is a time-boxed review, not a guarantee, so the team's own tests and invariants carry most of the weight.

  • A short spec: roles, trust assumptions, external dependencies (oracles, tokens accepted) and the invariants.
  • Full test coverage plus fuzz and invariant suites the auditors can extend.
  • Static analysis such as Slither run and triaged, with false positives documented.
  • A pinned compiler version, and a check of the known-bugs list for that version. The 0.8.37 release fixed three important bugs, so the compiler version is part of the audit scope.
  • A plan for after the audit: re-review of fixes, a bug bounty and monitoring.

What Changed Recently

May 7, 2025
Pectra on mainnet: EIP-7702, higher blob target; Solidity 0.8.30 defaults to prague
Aug 13, 2025
Hardhat 3 ships with Solidity tests as a first-class option
Dec 3, 2025
Fusaka on mainnet; Solidity 0.8.31 defaults to osaka and deprecates send and transfer
Feb 25, 2026
OpenZeppelin 5.6.0 requires proxy initialization at deploy
Apr 29, 2026
Solidity 0.8.35 adds the erc7201 builtin and an experimental mode flag
Jul 29, 2026
OpenZeppelin 5.7.0 drops the draft prefix from ERC-4337 files
Sep 10, 2026
Solidity 0.8.37 fixes three important bugs and adds block.slotnum for Amsterdam

31. What did EIP-7702 in Pectra change for contract developers?

It lets an EOA set a delegation so its address runs a chosen contract's code, which went live with Pectra on May 7, 2025.

Per the EIP-7702 spec, the account's code becomes the delegation indicator 0xef0100 || address, set by a signed authorization tuple [chain_id, address, nonce, y_parity, r, s].

  • tx.origin == msg.sender no longer means "an EOA with no code"; the EIP calls reentrancy guards built on that check the most affected case.
  • chain_id 0 makes an authorization valid on every chain, so wallets must treat it with care.
  • No initcode runs during delegation, so setup calls can be front-run unless the EOA's key signs the setup data.
  • Re-delegating to a new contract can collide with storage the old one wrote; the EIP suggests ERC-7201 namespaced storage.

32. What did Fusaka change that affects Solidity code?

Fusaka went live on December 3, 2025, and three of its EIPs reach contract developers directly:

  • EIP-7825 caps one transaction at 2^24 gas (16,777,216). Large batch operations, migrations and deployment scripts that fit in one block before may now need splitting.
  • EIP-7939 adds the CLZ opcode (count leading zeros). Solidity 0.8.31 exposes it as the Yul builtin clz(x), useful for log2 and bit math libraries.
  • EIP-7951 adds a secp256r1 (P-256) precompile, the curve passkeys and secure enclaves use, which makes passkey-signed smart accounts cheaper to verify.

Fusaka also shipped PeerDAS for blob data. The 0.8.31 release on the same day set the default EVM version to osaka.

33. What is deprecated ahead of Solidity 0.9.0?

Solidity 0.8.31 added deprecation warnings for the first batch of features scheduled for removal in the 0.9.0 breaking release:

  • send and transfer on addresses; use call plus reentrancy protection.
  • ABI coder v1; v2 is the default and a drop-in replacement.
  • Virtual modifiers.
  • Comparisons between variables of contract types; 0.9.0 will require an explicit conversion to address.
  • The /// @solidity memory-safe-assembly comment; use assembly ("memory-safe").

Later releases continued the cleanup: 0.8.35 warns about identifiers such as at, layout and transient that are selected to become keywords, and 0.8.37 deprecates the constantinople, petersburg, istanbul and berlin EVM versions.

No 0.9.0 release date has been announced. Separately, the team's October 14, 2026 update describes Core Solidity as a prototype frontend for solc that compiles to the same Yul backend, not yet for mainnet use.

34. What are custom storage layouts and the erc7201 builtin?

Since Solidity 0.8.29, a contract can move its whole storage to a chosen base slot with the layout at specifier, and 0.8.35 added an erc7201 builtin that computes an ERC-7201 namespace root at compile time.

contract Main layout at erc7201("example.main") {
    uint256 x; // stored from the erc7201("example.main") slot onward
}

The docs set the limits: only the most derived contract of an inheritance tree can declare a layout, not abstract contracts, interfaces or libraries; the base must be a compile-time expression; and it does not move transient variables.

That makes it useful for EIP-7702 delegate contracts and fresh deployments, but it does not replace per-module namespaced structs in libraries meant for inheritance.

35. What changed in OpenZeppelin Contracts from 5.5 to 5.7?

The changes that break existing code are in proxies, the upgradeable package and account abstraction:

  • 5.5.0 (October 31, 2025): ReentrancyGuard, ReentrancyGuardTransient, ERC721Holder and ERC1155Holder are no longer transpiled, so upgradeable projects import them from @openzeppelin/contracts; nonReentrantView added; minimum pragma raised to 0.8.24 in many contracts.
  • 5.6.0 (February 25, 2026): proxies must be initialized at construction; Account defaults to EntryPoint v0.9; ERC4626 gains _transferIn and _transferOut hooks.
  • 5.7.0 (July 29, 2026): EIP712 drops its storage fallback, so name and version must each fit in 31 bytes or the constructor reverts; ERC-4337 files lose the draft- prefix; a minimal Paymaster is added.

A candidate who upgraded a project across these versions should be able to name at least the proxy initialization change, because it changes deployment scripts.

Signs of a Strong Answer

  • They trace every external call, including token hooks and callbacks, before reading anything else in a function.
  • They state slot numbers when they talk about upgrades, and they ask for a storage layout diff before approving one.
  • They name the rounding direction for each conversion and say who keeps the dust.
  • They write the invariant first, then the handler, and can explain why fail_on_revert is off by default.
  • They know the deployed compiler version, check its known bugs, and set evmVersion per target chain.
  • They treat tx.origin == msg.sender checks as broken after EIP-7702 and explain why.

Hiring Solidity Engineers

Smart contract engineers who have shipped audited code, run invariant suites and handled an upgrade in production are scarce on general job boards.

Second Talent helps US startups hire vetted AI-Native Engineers from Asia, including Solidity developers, Ethereum developers and broader blockchain developers, screened with questions like these.

For zero-knowledge work, see the ZK engineer role guide.

Rates run $29 to $39 USD an hour, billed monthly, with matches in 24 hours and a 90-day one-time replacement guarantee. Tell us what you need.

Get developers who ship with AI agents.

Starts at $29/Hour.

Start Hiring

$0 cost until you hire

Talk to Second Talent

How would you like to talk?

Chat on WhatsApp Message us at your own pace. No spam.

Prefer email? hello@secondtalent.com

Loading available times…