Carriere·10 min de lecture·Par Solingo

Top 30 Web3 Interview Questions for Solidity Developers

Prepare for your next Web3 interview with these 30 essential questions covering Solidity basics, security, gas optimization, EVM internals, and DeFi protocols.

# Top 30 Web3 Interview Questions for Solidity Developers

After interviewing dozens of candidates and reviewing hiring practices at top Web3 companies, here are the 30 questions that appear most frequently — with answers that will help you stand out.

Solidity Basics (Questions 1-8)

1. What's the difference between memory, storage, and calldata?

Answer:

  • Storage: Persistent data stored on the blockchain. Most expensive. State variables use storage by default.
  • Memory: Temporary data that exists during function execution. Erased between external function calls. Cheaper than storage.
  • Calldata: Read-only temporary data for function parameters. Cheaper than memory. Only available for external function parameters.

Example:

contract DataLocations {

uint[] public storageArray; // storage by default

function processArray(uint[] calldata data) external {

// data is in calldata (read-only, cheapest)

uint[] memory tempArray = new uint[](data.length); // memory

storageArray = tempArray; // copies to storage

}

}

Follow-up they might ask: "Why can't you use calldata for return values?"

Answer: Calldata is input-only. Return values use memory.

---

2. Explain the difference between transfer, send, and call for sending ETH.

Answer:

  • transfer(amount): Sends ETH, forwards 2300 gas, reverts on failure. ❌ Avoid (breaks with gas cost changes).
  • send(amount): Sends ETH, forwards 2300 gas, returns false on failure. ❌ Avoid (same issues as transfer).
  • call{value: amount}(""): Sends ETH, forwards all available gas, returns (bool success, bytes memory data). ✅ Recommended.

Best practice (2026):

(bool success, ) = recipient.call{value: amount}("");

require(success, "ETH transfer failed");

Why they ask this: Tests if you're up to date with modern best practices.

---

3. What are function visibility specifiers and when do you use each?

Answer:

  • public: Callable from anywhere (externally, internally, derived contracts). Generates automatic getter for state variables.
  • external: Only callable from outside the contract. More gas-efficient for large arrays (uses calldata).
  • internal: Only callable within the contract or derived contracts.
  • private: Only callable within the defining contract (not derived contracts).

Pro tip: Use external for functions that are never called internally — saves gas.

---

4. What's the difference between view, pure, and no modifier?

Answer:

  • No modifier: Can read AND modify state. Costs gas when called in a transaction.
  • view: Can READ state but not modify it. Free when called externally (off-chain). Costs gas if called by another function in a transaction.
  • pure: Cannot read OR modify state. Only uses function parameters and local variables.

Common mistake: Marking a function view when it calls another contract (that might change state) — this will fail to compile in recent Solidity versions.

---

5. How does delegatecall differ from call?

Answer:

  • call: Executes code in the context of the CALLED contract (uses its storage, msg.sender changes).
  • delegatecall: Executes code in the context of the CALLING contract (uses caller's storage, msg.sender preserved).

Use case: Proxy patterns, upgradeable contracts, libraries.

Security risk: Storage layout must match EXACTLY between proxy and implementation. Misalignment = catastrophic bugs.

// Proxy contract

contract Proxy {

address public implementation;

fallback() external payable {

address impl = implementation;

assembly {

calldatacopy(0, 0, calldatasize())

let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)

returndatacopy(0, 0, returndatasize())

switch result

case 0 { revert(0, returndatasize()) }

default { return(0, returndatasize()) }

}

}

}

---

6. What is the difference between == and = in Solidity?

Answer:

  • =: Assignment operator (sets a value).
  • ==: Comparison operator (checks equality).

Why they ask: Surprisingly common bug. Solidity's compiler now warns about assignments in conditionals, but interviewers still test this.

---

7. What are events and why are they important?

Answer:

Events are a way to emit logs that are stored in the blockchain's transaction receipt (not in contract storage). They are:

  • Cheaper than storage (SSTORE costs 20k gas, LOG costs ~375-1500 gas per event).
  • Accessible off-chain via JSON-RPC and indexing services (The Graph, Etherscan).
  • Not accessible on-chain (other contracts can't read events).

Use cases:

  • State change notifications (Transfer, Approval)
  • Off-chain indexing for dApps
  • Audit trails
event Transfer(address indexed from, address indexed to, uint256 value);

function transfer(address to, uint256 amount) public {

balances[msg.sender] -= amount;

balances[to] += amount;

emit Transfer(msg.sender, to, amount); // ✅ Always emit after state changes

}

Follow-up: "Why use indexed parameters?"

Answer: Makes events filterable. Max 3 indexed parameters per event.

---

8. What are modifiers and when should you use them?

Answer:

Modifiers are reusable code that runs before or after a function. Common use: access control, validation, reentrancy guards.

modifier onlyOwner() {

require(msg.sender == owner, "Not owner");

_; // Function body executes here

}

function withdraw() external onlyOwner {

// Only owner can call this

}

Advanced tip: The _; placeholder determines when the function body runs. You can have code after it:

modifier checkInvariant() {

_;

assert(balance >= reserveBalance); // Runs AFTER function

}

---

Security (Questions 9-16)

9. Explain reentrancy and how to prevent it.

Answer:

Reentrancy occurs when a contract calls an external contract, which then calls back into the original contract before the first execution completes.

Classic example (vulnerable):

function withdraw(uint amount) public {

require(balances[msg.sender] >= amount);

(bool success, ) = msg.sender.call{value: amount}(""); // 🚨 External call BEFORE state update

require(success);

balances[msg.sender] -= amount; // 🚨 State updated too late

}

Attack: Attacker's fallback function calls withdraw() again before balances is updated.

Fixes:

  • Checks-Effects-Interactions pattern:
  • function withdraw(uint amount) public {
    

    require(balances[msg.sender] >= amount); // Check

    balances[msg.sender] -= amount; // Effect

    (bool success, ) = msg.sender.call{value: amount}(""); // Interaction

    require(success);

    }

  • Reentrancy guard (OpenZeppelin):
  • import "@openzeppelin/contracts/utils/ReentrancyGuard.sol";
    
    

    contract Safe is ReentrancyGuard {

    function withdraw(uint amount) public nonReentrant {

    // Protected

    }

    }

    ---

    10. What is integer overflow/underflow and how does Solidity 0.8+ handle it?

    Answer:

    Pre-0.8: Integers wrapped around silently.

    uint8 x = 255;
    

    x += 1; // Becomes 0 (overflow)

    Solidity 0.8+: Built-in overflow/underflow protection. Automatically reverts on overflow.

    When to use unchecked: When you're certain overflow is impossible and want to save gas.

    function increment(uint x) public pure returns (uint) {
    

    unchecked { return x + 1; } // Saves ~20 gas, but dangerous if x can be type(uint).max

    }

    ---

    11. What is a front-running attack?

    Answer:

    An attacker monitors the mempool, sees your transaction, and submits a similar transaction with higher gas price to execute before yours.

    Example: You see a profitable DEX arbitrage opportunity and submit a transaction. MEV bot sees it, copies your transaction with 10x gas price, steals the profit.

    Mitigations:

    • Commit-reveal schemes (submit hash first, reveal later)
    • Private mempools (Flashbots)
    • Slippage protection (limit orders)
    • Time-locks (delay execution)

    ---

    12. What is tx.origin and why shouldn't you use it for authentication?

    Answer:

    • msg.sender: Immediate caller (can be a contract or EOA).
    • tx.origin: Original EOA that initiated the transaction chain.

    Why it's dangerous:

    // 🚨 VULNERABLE
    

    function withdraw() public {

    require(tx.origin == owner); // Attacker tricks owner into calling malicious contract

    payable(owner).transfer(address(this).balance);

    }

    Attack: Attacker deploys malicious contract that calls your withdraw(). If owner interacts with attacker's contract, tx.origin is still owner, so check passes.

    Fix: Always use msg.sender for authentication.

    ---

    13. What is a flash loan attack?

    Answer:

    Flash loans allow borrowing unlimited funds within a single transaction (must be repaid before transaction ends).

    Attack vector: Manipulate protocol state (e.g., oracle prices, liquidity pools) using borrowed funds, exploit the manipulated state, return funds.

    Example: Borrow 100M USDC, dump it in a low-liquidity pool to crash price, liquidate positions in a lending protocol, profit, repay loan.

    Defense:

    • TWAP (Time-Weighted Average Price) oracles
    • Liquidity depth checks
    • Rate limiting
    • Reentrancy guards

    ---

    14. What are access control vulnerabilities?

    Answer:

    Missing or improper onlyOwner/role checks on sensitive functions.

    Example:

    // 🚨 VULNERABLE (anyone can mint)
    

    function mint(address to, uint amount) public {

    _mint(to, amount);

    }

    Fix: Use OpenZeppelin's Ownable or AccessControl.

    ---

    15. What is the difference between assert and require?

    Answer (pre-0.8):

    • require: Refunds remaining gas on failure. For input validation.
    • assert: Consumes all gas on failure. For invariant checks (should never fail).

    Solidity 0.8+: Both refund gas. assert should still only be used for internal errors/invariants.

    function withdraw(uint amount) public {
    

    require(amount <= balance, "Insufficient balance"); // User error

    balance -= amount;

    assert(balance >= 0); // Should never fail (invariant)

    }

    ---

    16. What is selfdestruct and why is it dangerous?

    Answer:

    selfdestruct(address payable recipient) deletes the contract and sends its ETH balance to recipient.

    Dangers:

    • Forcibly sends ETH to recipient (bypasses payable check, can break invariants)
    • Permanently destroys code (but storage remains on-chain)
    • Can't be called by external contracts after deletion (breaks composability)

    2026 note: EIP-6780 (Cancun upgrade) restricts selfdestruct — it now only sends funds, doesn't delete code unless called in same transaction as creation.

    ---

    Gas Optimization (Questions 17-22)

    17. How can you optimize gas in Solidity?

    Key techniques:

  • Use calldata instead of memory for external function parameters
  • Pack storage variables (use uint128 instead of uint256 where possible)
  • Use unchecked for safe arithmetic
  • Cache storage variables in memory
  • Use events instead of storage for historical data
  • Short-circuit conditionals (&& stops at first false)
  • Avoid unnecessary storage writes (SSTORE is 20k gas)
  • Use immutable/constant for values that don't change
  • // ❌ Expensive (reads from storage 3 times)
    

    function bad() public view returns (uint) {

    return storageVar + storageVar + storageVar;

    }

    // ✅ Cheaper (reads from storage once, uses memory)

    function good() public view returns (uint) {

    uint cached = storageVar;

    return cached + cached + cached;

    }

    ---

    18. What is storage packing?

    Answer:

    EVM stores data in 32-byte slots. Multiple small variables can share a slot.

    // 🚨 Uses 3 slots (96 bytes)
    

    contract Unpacked {

    uint128 a; // Slot 0

    uint256 b; // Slot 1 (can't fit in slot 0)

    uint128 c; // Slot 2

    }

    // ✅ Uses 2 slots (64 bytes)

    contract Packed {

    uint128 a; // Slot 0 (bytes 0-15)

    uint128 c; // Slot 0 (bytes 16-31)

    uint256 b; // Slot 1

    }

    Savings: Each storage slot read (SLOAD) costs 2100 gas (cold) or 100 gas (warm).

    ---

    19. What's cheaper: memory or calldata?

    Answer: calldata is cheaper because it's read-only and doesn't require copying data.

    Rule: Always use calldata for external function parameters that you don't modify.

    // ❌ Copies data to memory (expensive)
    

    function processArray(uint[] memory data) external {

    for (uint i = 0; i < data.length; i++) {

    // read data[i]

    }

    }

    // ✅ Reads directly from calldata (cheap)

    function processArray(uint[] calldata data) external {

    for (uint i = 0; i < data.length; i++) {

    // read data[i]

    }

    }

    ---

    20. How does immutable save gas compared to constant?

    Answer:

    • constant: Value must be assigned at compile time. Inlined in bytecode (no storage slot).
    • immutable: Value can be assigned in constructor. Stored in bytecode (not storage). Can't be changed after deployment.

    Both are cheaper than state variables because they don't use SLOAD.

    contract Example {
    

    uint public constant FEE = 100; // Compile-time constant

    address public immutable OWNER; // Set once in constructor

    constructor(address _owner) {

    OWNER = _owner; // ✅ Allowed

    }

    }

    ---

    21. What's the gas cost of deploying a contract?

    Answer:

    Deployment cost = 21,000 (base transaction) + ~200 gas per byte of bytecode + constructor execution + initial storage writes.

    Typical costs (2026):

    • Simple ERC-20: ~1M - 1.5M gas (~$30-$50 at 30 gwei)
    • Complex DeFi protocol: 3M - 6M gas (~$100-$200)

    Optimization: Minimize contract size (use libraries, external contracts for large logic).

    ---

    22. What is the difference between SLOAD and SSTORE?

    Answer:

    • SLOAD: Read from storage. Costs 2100 gas (cold) / 100 gas (warm after first access).
    • SSTORE: Write to storage. Costs 20,000 gas (cold) / 2,900 gas (warm) if changing value, or 5,000 gas if setting to zero (refunds 4,200 gas).

    Key insight: Storage writes are ~10x more expensive than reads. Cache frequently used values.

    ---

    EVM & Advanced (Questions 23-26)

    23. What is the difference between bytecode and runtime bytecode?

    Answer:

    • Bytecode: Includes constructor code + runtime code. What you deploy.
    • Runtime bytecode: Code that remains on-chain after deployment. What executes when you call functions.

    Constructor runs once, then is discarded.

    ---

    24. How does the EVM store data in memory?

    Answer:

    Memory is byte-addressable, but most operations use 32-byte words. Layout:

    • 0x00-0x3f: Scratch space for hashing
    • 0x40-0x5f: Free memory pointer (points to next available memory)
    • 0x60+: Your data

    Why it matters: Efficient memory usage can save gas in complex functions.

    ---

    25. What is a function selector?

    Answer:

    The first 4 bytes of the keccak256 hash of the function signature.

    // Function: transfer(address,uint256)
    

    // Signature: "transfer(address,uint256)"

    // Selector: keccak256("transfer(address,uint256)")[:4] = 0xa9059cbb

    Used for: Routing external calls to the correct function.

    ---

    26. What is the difference between CREATE and CREATE2?

    Answer:

    • CREATE: Address = keccak256(sender, nonce). Predictable if you know sender's nonce.
    • CREATE2: Address = keccak256(0xff, sender, salt, bytecode_hash). Deterministic based on bytecode and salt.

    Use case: CREATE2 allows deploying to the same address on multiple chains (same bytecode + salt = same address).

    ---

    DeFi (Questions 27-30)

    27. How does a constant product AMM work?

    Answer:

    Invariant: x * y = k (reserves of token X × reserves of token Y = constant).

    When you swap, the product must remain constant:

    x_new * y_new = k

    Example: Pool has 100 ETH and 200,000 USDC (k = 20,000,000).

    You swap 10 ETH. New reserves: 110 ETH.

    110 * y_new = 20,000,000 → y_new ≈ 181,818 USDC

    You receive 200,000 - 181,818 = 18,182 USDC (minus fees).

    Price impact: Larger trades move the price more.

    ---

    28. What is slippage and how do you protect against it?

    Answer:

    Slippage = difference between expected price and executed price (due to price movement or liquidity).

    Protection:

    function swap(uint amountIn, uint minAmountOut) external {
    

    uint amountOut = getAmountOut(amountIn);

    require(amountOut >= minAmountOut, "Slippage too high");

    // execute swap

    }

    User sets minAmountOut (e.g., "I want at least 95 USDC for my 1 ETH").

    ---

    29. What is a price oracle and why is it important?

    Answer:

    Oracle = source of off-chain data (e.g., ETH/USD price) on-chain.

    Why important: DeFi protocols need prices for:

    • Liquidations (lending protocols)
    • Swaps (DEXes)
    • Synthetic assets (Synthetix)

    Types:

    • Chainlink: Decentralized oracle network
    • Uniswap TWAP: Time-weighted average price from DEX
    • Band Protocol: Alternative decentralized oracle

    Security: Never use spot price (manipulatable). Use TWAP or decentralized oracles.

    ---

    30. What is a flash loan and how does it work?

    Answer:

    Borrow unlimited funds, use them, repay in the same transaction. If you can't repay, the entire transaction reverts.

    Workflow:

  • Call flashLoan(amount) on lender (Aave, dYdX)
  • Lender sends you amount
  • Lender calls your executeOperation() function
  • You execute your logic (arbitrage, liquidation, etc.)
  • You approve lender to take back amount + fee
  • Lender takes back funds
  • If balance is insufficient, entire transaction reverts
  • No collateral needed because it's atomic.

    ---

    Bonus Behavioral Questions

    "Why do you want to work in Web3?"

    Good answer: Mention decentralization, ownership, composability, specific problem you want to solve.

    Bad answer: "To get rich quick" or vague statements.

    "Tell me about a bug you found in your code."

    Show humility, learning, and systematic debugging approach.

    "How do you stay up to date with Solidity/EVM changes?"

    Mention: Solidity blog, OpenZeppelin forums, Immunefi, Twitter, audit reports, CTFs.

    ---

    How to Use This Guide

  • Read each answer out loud to internalize it.
  • Code the examples in Remix/Foundry to understand deeply.
  • Mock interview yourself — record answers, review, improve.
  • Go deeper on questions you struggle with.
  • Practice makes perfect. The more you articulate these concepts, the more confident you'll be in the actual interview.

    Next step: Build projects that demonstrate these concepts. Practice on Solingo with challenges designed around real interview questions.

    Good luck! 🚀

    Prêt à mettre en pratique ?

    Applique ces concepts avec des exercices interactifs sur Solingo.

    Commencer gratuitement