A candidate interviewing for a smart-contract engineering role at a mid-size DeFi protocol in late 2025 got asked, on the very first technical call, to explain what happens to a storage variable when a new implementation gets deployed behind an existing proxy. Not write the proxy. Just explain, out loud, why getting the order of state variables wrong doesn't throw an error, it quietly corrupts data instead. He'd shipped two audited contracts to mainnet already and still had to think for a second.
That's roughly what a blockchain developer interview tests in 2026: not whether you can write Solidity syntax, most of it reads like JavaScript with types once you've seen it, but whether you understand what happens when code you wrote controls real money and can't be patched with a quiet hotfix once it's live. Solidity itself is a small language by market share. It showed up at around 1.1% adoption among professional developers in the 2024 Stack Overflow Developer Survey, a rounding error next to JavaScript or Python. Reported compensation for blockchain-focused roles slid from consistently six-figure territory in 2023 toward roughly $86,000 in the same 2024 survey year. The technical bar didn't drop. The 2021-2022 token-funded hiring wave cooled off hard and took a chunk of roles with it.
Here's a genuinely contrarian read on how to prep for this, and I could be wrong, teams and interview loops vary a lot: memorizing more Solidity syntax matters less than being able to trace what one specific line of code does to storage, gas, or an external call, out loud, under a follow-up question you didn't rehearse. This page covers blockchain developer interview questions across four areas: fundamentals and consensus, smart contracts and Solidity mechanics, security and gas optimization, and DeFi and web3 tooling. Forty-six questions total, difficulty-tagged, with real Solidity wherever the code itself is the actual point.
Blockchain fundamentals and consensus
This section is the warm-up round, but it's a real filter. Candidates who've only worked at the application layer, calling a library function, reading a subgraph, sometimes can't explain what the chain underneath is actually doing, and that gap shows up fast once an interviewer pushes past the first answer.
Easy questions
15Nodes agree on the same transaction history by running a shared protocol rule set and rejecting anything that violates it, rather than trusting one server's word. In Bitcoin's proof-of-work model the rule is simple: the chain with the most cumulative work wins. In proof-of-stake chains, validators lock up capital and get slashed for provable misbehavior, so lying is expensive rather than merely against policy.
The core idea either way: consensus doesn't require trusting any single participant, it requires that a majority of participants, by hash power or by stake, find honest behavior more profitable than attacking the network.
An attacker controlling a majority of hash power, or stake, can in principle exclude or reorder transactions and double-spend by building a longer private chain that overtakes the honest one once revealed. It gets harder over time mostly because the cost scales with total network hash power or total staked value, both of which tend to grow, and because on proof-of-stake chains a successful attack still gets punished retroactively through social-layer forks and slashing once it's discovered.
This isn't a solved problem industry-wide, just one that scales with a blockchain's own market cap. Smaller proof-of-work blockchains like Ethereum Classic have been hit more than once.
A soft fork tightens the rules. Blocks valid under the new rules are still valid under the old ones, so upgraded and non-upgraded nodes coexist on the same chain (SegWit was a soft fork). A hard fork loosens or changes rules in a way old nodes will reject as invalid, which splits the network into two chains if even one competent node keeps running the old software.
Ethereum and Ethereum Classic, after the 2016 hard fork that reversed The DAO hack, is the canonical example. Soft forks need a majority of miners or validators to enforce them; hard forks need near-universal coordination or you get a permanent split.
A modifier that checks msg.sender against a stored owner address before letting a function's body run, reverting otherwise. It's access control, not encryption, anyone can read a public function's logic. onlyOwner restricts who can trigger state-changing calls like withdrawing fees or pausing the contract.
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "not owner");
_;
}
function withdrawFees() external onlyOwner {
payable(owner).transfer(address(this).balance);
}A single owner address is also a centralization risk interviewers expect you to flag unprompted. A compromised owner key can drain fees or pause the contract maliciously, which is why production contracts increasingly use a multisig or a timelock as the owner instead of one externally-owned account.
receive() fires on a plain ETH transfer with empty calldata, it must be external payable and takes no arguments. fallback() fires when calldata doesn't match any existing function selector, or when ETH arrives with data but no receive() is defined. A contract with neither simply rejects plain ETH transfers, which is sometimes intentional, forcing all incoming ETH through a function that can validate it.
receive() external payable {
emit Deposited(msg.sender, msg.value);
}
fallback() external payable {
emit UnknownCall(msg.sender, msg.data);
}Event data goes into the transaction receipt's log, not into contract storage, so it never touches the expensive SSTORE opcode, 20,000 gas for a fresh storage slot versus a few hundred for a log entry, per the EVM's own gas schedule. The tradeoff: contract code can't read event logs back. They're for off-chain indexers, subgraphs, and frontends, not for other functions in the same contract to query later.
constant values must be known at compile time and get inlined directly into the bytecode, no storage slot, essentially free to read. immutable values get set exactly once, in the constructor, and are also baked into the deployed bytecode rather than a storage slot, but unlike constant they can depend on constructor arguments or deployment-time computation. Both are far cheaper to read than a regular storage variable since no SLOAD is needed. The difference is purely about when the value is known, compile time versus deployment time.
An interface can only declare external function signatures, no implementation, no state variables, no constructor. It's a pure contract shape, useful for calling a contract you don't own the source of, an ERC-20 token, say, without needing its full implementation. An abstract contract can mix implemented and unimplemented functions, hold state, and be inherited from, which is what you want when defining a base contract that shares real logic across several concrete implementations.
The rule of thumb: reach for an interface when you're just calling into an external contract type. Reach for an abstract contract when you're building your own inheritance hierarchy and actually want to share code, a shape alone isn't enough.
msg.sender is whoever, or whatever contract, directly called the current function. tx.origin is always the original externally-owned account that kicked off the entire transaction chain, even if it passed through five contracts to get there. A contract that checks require(tx.origin == owner) instead of require(msg.sender == owner) can be tricked: the real owner gets phished into calling a malicious contract, which then calls the victim contract on the owner's behalf. tx.origin still reads as the owner's address even though msg.sender is the malicious contract, and the check passes.
The fix is simply to use msg.sender for access control, always. tx.origin has essentially no legitimate access-control use case in modern Solidity.
You need an RPC connection to a node, Infura, Alchemy, or a public endpoint, the contract's ABI, and a way to get the user's wallet to sign transactions. MetaMask injecting window.ethereum is the classic path, though wagmi and viem have mostly replaced hand-rolled ethers.js setup in newer codebases because they handle wallet connection state, chain switching, and caching automatically.
import { useReadContract, useWriteContract } from 'wagmi';
const { data: balance } = useReadContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'balanceOf',
args: [userAddress],
});
const { writeContract } = useWriteContract();
function transfer(to, amount) {
writeContract({
address: tokenAddress,
abi: erc20Abi,
functionName: 'transfer',
args: [to, amount],
});
}Interviewers usually push into error handling next: what happens if the user rejects the signature, the RPC times out, or the wallet is on the wrong chain? A frontend that only handles the happy path is a common tell that someone's only ever built a tutorial dApp.
For EVM-chain roles, Ethereum, most L2s, the large majority of DeFi and NFT postings, yes, Solidity remains the primary skill listed for blockchain developer jobs. It's a genuinely small population though: roughly 1.1% adoption among professional developers in the 2024 Stack Overflow Developer Survey, low overall demand relative to something like Python or JavaScript, but with a correspondingly small supply of developers who know it well. Rust matters specifically for Solana, Move for Aptos and Sui, and neither replaces the need for Solidity if you're targeting EVM-ecosystem roles, which is still most of the market.
Less than most candidates expect. You need to know what a hash function guarantees, deterministic, one-way, collision-resistant, and roughly how elliptic curve signatures work at a conceptual level, enough to explain why a private key produces a signature a public key can verify without revealing the key itself, not derive the curve math from scratch on a whiteboard. Interviewers weighting cryptography heavily are the exception. Most weight security patterns, gas mechanics, and system design far more than the underlying math.
Tutorial-only candidates tend to describe gas, slippage, and MEV as abstract concepts they've read about. Candidates who've deployed real contracts describe them as specific, remembered costs, what a mainnet deployment actually cost in ETH at the time, a specific slippage setting they had to tune because a swap kept failing, a specific transaction they watched get sandwiched. The tell isn't vocabulary, plenty of well-prepared tutorial candidates know the right words, it's whether the details attach to a real, particular incident instead of the general shape of the concept.
Interviewers who ask what's the worst bug you shipped and how did you catch it are fishing for exactly this. A candidate with real deployment experience usually has an answer immediately. A candidate without it often has to construct one on the spot, and that hesitation is itself information.
A smart contract is code deployed to a blockchain at a specific address, with an ABI describing its callable functions and its own persistent storage. When you call an API, you're trusting a company's server to run its logic correctly and honestly. When you call a contract's function, every node in the network re-executes that same bytecode against the same shared state, and the network's consensus rules decide what happens next, so the result doesn't depend on trusting whoever wrote the code.
The bigger practical difference is state and cost. An API call is usually free and instant, backed by a database you never see. A contract call that changes state is a transaction: it costs gas, waits to be included in a block, and can't be canceled once it's mined. Read-only calls are free and instant against your own node's copy of the chain, but writes are public and irreversible, anyone watching the mempool can see your transaction before it lands, and once it's confirmed the code doesn't get quietly patched the way you'd hotfix a backend, unless you built an upgrade path in from the start.
ERC-20 is the fungible standard, every unit is identical and interchangeable, tracked as one balance per address plus a total supply. It's what you reach for when one unit is worth exactly as much as another: a currency, a governance token, a reward point.
ERC-721 is non-fungible, each token has its own ID and its own metadata, like a deed or a ticket for a specific seat. Balances live per token ID rather than per address, so transferring token #4302 has nothing to do with token #4303.
ERC-1155 sits in between, a single contract can hold many token IDs, and each ID can behave as fungible (a stack of in-game gold) or effectively unique (a 1-of-1 skin). The practical win is cost: you don't deploy one contract per collection, and batch transfers of several item types in a single call are native to the standard instead of something you build yourself. The tradeoff is that older tooling built only for 721 sometimes doesn't handle 1155's batch semantics correctly.
Medium questions
25The trilemma says a blockchain can optimize hard for at most two of decentralization, security, and scalability, with tradeoffs showing up on the third. Bitcoin and Ethereum's base layer prioritize decentralization and security, capping throughput on purpose, Ethereum mainnet runs somewhere around 15 transactions per second. Higher-throughput L1s push scalability with a smaller, higher-spec validator set, trading off some decentralization to get there.
Rollups are Ethereum's actual answer in practice: keep L1 as the secure settlement layer, push execution to L2s that inherit L1's security guarantees but batch thousands of transactions into a single L1 transaction.
Bitcoin's UTXO model has no persistent balance. Your holdings are a set of unspent transaction outputs you can prove ownership of, and a transaction consumes some of those and creates new ones, with change coming back as a new output to yourself. Ethereum's account model tracks a running balance and a nonce per address, closer to a bank ledger.
UTXO parallelizes well since unrelated transactions never touch shared state, but it makes stateful smart contracts awkward. That's part of why Ethereum chose accounts: contracts need persistent, mutable storage that survives across transactions, not disposable one-time outputs.
A Merkle tree hashes leaf data, transactions, pairwise up to a single root hash stored in the block header. A simplified payment verification client, a phone wallet, say, doesn't download every transaction ever made. It downloads block headers and asks a full node for a Merkle proof, a small set of sibling hashes, that a specific transaction sits under a given root.
Verifying that proof costs O(log n) hashes instead of O(n), which is exactly why a phone wallet can confirm a payment landed without storing the whole chain.
Finality is the point past which reverting a transaction becomes practically, or provably, impossible. Bitcoin has probabilistic finality: each additional confirmation makes a reorg exponentially less likely, but never technically zero, which is why exchanges wait for multiple confirmations on large deposits.
Ethereum has run economic finality via Casper FFG since the Merge. A block becomes finalized after two consecutive justified checkpoints, roughly 12 to 15 minutes, and reverting a finalized block would require attackers to burn at least a third of all staked ETH, tens of billions of dollars at current prices. That's finality in a much stronger economic sense than Bitcoin's confirmation count.
A proof-of-work miner races other miners to find a nonce whose block hash lands below a difficulty target, burning electricity and hardware regardless of the outcome. Whoever wins proposes the next block and collects the reward. A proof-of-stake validator instead locks up capital, 32 ETH to run a solo Ethereum validator, and gets pseudorandomly selected to propose or attest to blocks. Correct behavior earns modest yield; provable misbehavior, double-signing, getting caught equivocating, gets a slice of that stake slashed.
The practical shift: proof-of-work security is bounded by hash rate an attacker would need to rent or buy. Proof-of-stake security is bounded by stake an attacker would need to acquire, and then watch get destroyed the moment it's misused.
Every Ethereum account tracks a nonce that increments by exactly one with each transaction it sends. A transaction is only valid if its nonce matches the account's current expected value, so an attacker who captures a signed transaction and tries to rebroadcast it later gets rejected the moment the original lands and the nonce moves forward.
It's also why a "stuck" transaction needs a nonce-matching replacement, same nonce, higher gas price, rather than just sending a fresh one. A new nonce doesn't cancel anything, it just queues behind the stuck transaction.
call executes code in the target contract's own storage context, msg.sender becomes the calling contract, and it can modify the target's state. delegatecall executes the target's code but in the caller's storage context, msg.sender and msg.value stay as the original caller's, which is exactly how a proxy runs an implementation contract's logic against the proxy's own storage. staticcall is call with state changes forbidden, any attempted write inside one reverts.
The follow-up interviewers like: what happens if a delegatecall target's storage layout doesn't match the caller's? Undefined, quietly wrong behavior, not a revert, which is exactly why proxy upgrades are dangerous if slot order isn't preserved exactly.
Before Solidity 0.8.0, arithmetic silently wrapped: uint8 x = 255; x += 1; became 0 with no error, which OpenZeppelin's SafeMath library patched by wrapping every operation in checks. Solidity 0.8.0 made checked arithmetic the default; the same overflow now reverts automatically, no library required.
The unchecked keyword opts back into the old wrapping behavior for a specific block, which teams use deliberately for gas savings where overflow is provably impossible, a bounded loop counter, say, not as a shortcut to skip thinking about it. Interviewers still ask this because plenty of production code targets pre-0.8 compilers, so knowing when SafeMath is actually necessary versus vestigial matters.
abi.encode pads every value to 32 bytes, so two different inputs essentially never hash to the same encoded output. abi.encodePacked tightly packs values with no padding, which is cheaper but means multiple different input combinations can produce an identical byte sequence, and therefore an identical hash.
// collision risk: encodePacked with more than one dynamic-length argument
bytes32 hash1 = keccak256(abi.encodePacked("AAA", "BBB"));
bytes32 hash2 = keccak256(abi.encodePacked("AA", "ABBB"));
// hash1 can equal hash2 here, a real signature or allowlist bypass vectorThe rule that actually matters: never use encodePacked with more than one dynamic-length argument, string, bytes, dynamic arrays, if the result feeds into a hash used for signatures or access control. Fixed-size types only, or switch to encode.
A require string gets stored in the deployed bytecode and copied into the revert data every time it fires, and long descriptive strings cost real deployment gas just for existing in the contract. Custom errors, Solidity 0.8.4 and later, encode only a 4-byte selector plus typed arguments, closer to how a function call's ABI works, so both deployment and revert-time costs drop noticeably, and custom errors can carry structured data a string can't.
error InsufficientBalance(uint256 requested, uint256 available);
function withdraw(uint256 amount) external {
if (amount > balances[msg.sender]) {
revert InsufficientBalance(amount, balances[msg.sender]);
}
balances[msg.sender] -= amount;
}Before EIP-6780, part of the March 2024 Dencun upgrade, selfdestruct wiped a contract's code and sent its ETH balance to a target address, and teams used that as a cheap way to "delete" a contract or reclaim gas refunds. EIP-6780 restricted the code-removal behavior to only work within the same transaction the contract was created in. In every other case it now just sends the ETH balance and leaves the code and storage untouched.
For interview purposes: any design that relied on selfdestruct to actually remove a contract post-deployment, a self-destructing escrow, a one-time factory cleanup, needs a different approach now, usually a pausable flag checked in every function instead of relying on the contract disappearing. It's a good question for spotting candidates who learned Solidity from an older tutorial.
The pattern is: validate all preconditions first (checks), update the contract's own state next (effects), and only then reach out to any other address or contract (interactions). The order matters because any external call, even something that looks harmless like an ERC-721 safeMint that calls onERC721Received on the recipient, hands control flow to code you don't own before your own function has finished.
Interviewers sometimes show a read-only reentrancy case: a view function that reads state mid-callback and returns a stale or manipulated value to a third-party contract that trusts it. That variant doesn't touch the vulnerable contract's own funds directly, but it corrupts anything downstream that relies on the reported state, which is a less obvious and increasingly common failure mode.
Validators have some ability to influence block.timestamp, within a roughly 15-second tolerance under most client implementations, and full visibility into blockhash before a transaction using it lands in their own block. Anyone with block-production power, or in some cases anyone who can simulate the transaction against pending state, can bias or predict the "random" outcome before committing.
The standard fix is an external verifiable randomness source. Chainlink VRF is the one most commonly cited in practice: it returns a random value plus a cryptographic proof that the value wasn't tampered with, generated off-chain and verified on-chain, so no single validator can bias the result without the proof failing.
Front-running submits a transaction ahead of a target transaction to benefit from an action the target is about to take, buying an asset before a large pending buy order pushes its price up. Back-running submits a transaction immediately after a target transaction to react to a state change it just caused, buying the moment a large trade or a liquidation creates a temporary price dislocation.
Both exploit the same underlying fact, mempool visibility plus block-ordering control, but front-running preys on intent, what someone's about to do, and back-running preys on outcome, what they just did.
A common anti-pattern loops over a dynamically growing array to pay out or process every entry, refunding every bidder in a failed auction, say. If the array grows large enough, the loop runs out of gas before finishing, and if that loop has to complete for the contract to reach a usable state, the whole contract gets stuck. A related version: paying out to a list of addresses in one transaction, where a single recipient with a fallback function that intentionally reverts blocks the whole payout for everyone else, since one failed transfer aborts the entire transaction.
The fix in both cases is a pull-over-push pattern: let each user withdraw their own share individually, rather than the contract pushing funds to everyone in a single loop. It shifts gas cost and failure risk onto each claimant instead of concentrating it in one fragile transaction.
Pack multiple small variables, bool, uint8, address, into a single 32-byte storage slot instead of letting each get its own, since a fresh SSTORE costs 20,000 gas regardless of how few bits are actually used. Cache storage reads in a local memory variable inside a loop instead of re-reading the same slot from storage every iteration. Use calldata instead of memory for external function parameters you don't need to modify, since copying calldata to memory costs gas you don't need to spend. Prefer custom errors over require strings. And use unchecked blocks for arithmetic you've already proven can't over or underflow, a loop counter bounded by array length, say.
None of these matter for a contract that runs twice a year. They matter enormously for anything called thousands of times a day, where a few hundred gas per call compounds into real money across users.
Slither runs dozens of automated detectors, reentrancy patterns, uninitialized storage pointers, incorrect tx.origin use, shadowed state variables, unchecked low-level call return values, and flags them consistently across an entire codebase in seconds. What it doesn't catch is business-logic bugs: a lending protocol with mathematically sound code that still lets someone borrow more than their collateral supports, because of a flawed interest-rate formula, won't trip any static analyzer, since nothing about it matches a known vulnerability pattern.
The realistic workflow: run Slither as a fast first pass to clear the mechanical stuff, then spend actual audit time on the protocol-specific logic a tool has no model of.
A unit test checks one specific input you thought to write. Foundry's fuzzer generates hundreds or thousands of randomized inputs against the same test function and looks for any input that breaks an invariant you've asserted, surfacing edge cases, a specific amount that triggers a rounding error, a sequence of deposits and withdrawals that leaves accounting inconsistent, that a human writing test cases by hand would need to get lucky, or extremely thorough, to find.
function testFuzz_WithdrawNeverExceedsBalance(uint256 amount) public {
amount = bound(amount, 0, 1000 ether);
vault.deposit{value: amount}();
uint256 before = address(vault).balance;
vault.withdraw(amount);
assertLe(amount, before); // invariant: can't withdraw more than existed
}Interviewers sometimes ask about invariant testing specifically, a step further than fuzzing single functions, where Foundry calls a random sequence of functions against a contract and checks that a global property, total supply equals sum of balances, say, never breaks across the entire sequence.
Optimistic rollups assume every batch of transactions is valid by default and only run a fraud proof if someone challenges it within a dispute window, typically seven days, which is why withdrawing back to Ethereum L1 takes about a week unless a liquidity provider fronts the funds for a fee. ZK rollups generate a cryptographic validity proof for every batch before it's accepted on L1, so there's no dispute window and withdrawals finalize once the proof verifies.
| Aspect | Optimistic rollup | ZK rollup |
|---|---|---|
| Validity check | Assumed valid unless challenged | Proven valid before acceptance |
| Withdrawal to L1 | Roughly 7-day dispute window (or a paid fast withdrawal) | Finalizes once the proof verifies |
| EVM compatibility | Near-full, mature tooling | zkEVMs have closed most of the gap by 2026 |
| Examples | Arbitrum, Optimism | zkSync, Starknet, Scroll |
The practical tradeoff for a developer: optimistic rollups have near-full EVM compatibility with mature tooling. ZK rollups need proving systems that historically lagged on full EVM equivalence, though zkEVMs have closed most of that gap, and proof generation itself adds real computational cost and latency on the sequencer side.
Before EIP-1559, every transaction bid a single gas price in a first-price auction, which made fees volatile and forced users to guess what everyone else was bidding. EIP-1559 replaced that with a base fee that adjusts algorithmically block-to-block based on how full the previous block was, rising when blocks run over 50% full, falling when under, and the base fee gets burned rather than paid to the validator. Users add a priority fee on top, a tip, as an incentive to get included over another transaction at the same base fee.
Gas prices still vary because the priority fee is still a market. During genuine demand spikes the base fee itself climbs quickly, up to 12.5% per block, and users competing for the same limited block space still bid tips against each other. EIP-1559 made pricing more predictable and transparent, not free of variability.
An RPC node answers questions about current or specific historical state, what's this address's balance right now, but it's a poor fit for questions that require scanning and aggregating across a long history, every trade a given wallet has ever made, say, since that would mean the frontend re-scanning potentially millions of blocks on every page load. The Graph indexes on-chain events into a queryable database ahead of time, defined by a subgraph schema written once, and serves aggregated queries through GraphQL in milliseconds instead of an RPC node scanning live for every user.
The tradeoff is indexing lag, a subgraph can be a block or two behind the chain tip, and the operational cost of maintaining one, which is why time-sensitive single-value lookups usually still go straight to the contract, and only aggregate or historical queries go through an indexer.
Interacting with most DeFi protocols using an ERC-20 token takes two separate transactions: approve(spender, amount) to grant an allowance, then a second transaction that actually calls transferFrom. That's two gas payments and two wallet popups for what feels like one action, and forgetting to reset an old, unlimited approval is a genuine, repeatedly exploited attack surface once a protocol later gets compromised.
permit (EIP-2612) lets a user sign an off-chain, gasless approval message, using EIP-712 typed data, that the spending contract submits on-chain itself, bundled into the same transaction as the actual action. That's one signature and one transaction instead of two. Not every ERC-20 implements permit, which is exactly why interviewers ask you to check for it rather than assume it.
Foundry writes tests in Solidity itself, no context-switching between languages, and its fuzzer and invariant testing are first-class built-in features rather than plugins. Hardhat's test suite runs in JavaScript or TypeScript against a local Hardhat Network node, a natural fit for teams whose frontend and test tooling already live in the same language, with a broader plugin ecosystem for deployment scripts, verification, and gas reporting simply from being around longer.
Plenty of production teams run both: Foundry for fast, low-level unit and fuzz testing close to the contract logic, Hardhat for integration tests and anything that needs to talk to a frontend stack in the same language. Picking one exclusively is a legitimate choice too, as long as you know why you picked it.
A Safe isn't a special account type at the protocol level, it's a smart contract with a list of owner addresses and a threshold, say 3-of-5. To move funds or call a privileged function elsewhere, nobody broadcasts a transaction straight from a private key. Instead, the transaction data gets constructed off-chain, enough owners sign that exact payload, usually coordinated through the Safe's UI or transaction service so owners don't need to be online at the same time, and then any single owner submits the collected signatures on-chain. The contract checks that enough valid signatures exist for that specific transaction hash and nonce, then executes it internally.
Teams prefer this over a single EOA because one private key is a single point of failure, whoever holds it can drain the treasury, and if it leaks or gets phished there's no recovery. With a 3-of-5 setup, one compromised or lost key doesn't matter, and the on-chain nonce stops anyone from replaying an old signed batch. The real cost is speed: every admin action, an upgrade, a pause, a fund transfer, now needs coordination across multiple humans, which is exactly why serious protocols pair a Safe with a timelock, so even a fully-signed transaction sits in a visible delay window before it executes.
A push payment sends funds as a side effect of some other action, an auction contract that, the moment someone is outbid, immediately does oldBidder.call{value: refund}("") to send their money back. That call hands execution to whatever address oldBidder is. If it's a contract with a fallback that reverts, or one that simply burns the gas stipend, that single refund failing can revert the whole outer transaction, permanently blocking the auction from accepting a new bid. If the fallback does something worse than reverting, you're in reentrancy territory instead.
The pull pattern flips it: instead of sending funds during the state change, you record what the user is owed, then let them call a separate withdraw function whenever they want.
mapping(address => uint256) public pendingReturns;
function withdraw() external {
uint256 amount = pendingReturns[msg.sender];
pendingReturns[msg.sender] = 0;
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok, "transfer failed");
}Zeroing the balance before sending means even a failed transfer or a double call leaves nothing left to steal. The tradeoff is UX, users take a second action to actually get paid, so if your product depends on funds arriving automatically you need to say so clearly. For anything sending money to an address you don't control, pull is the pattern that fails safe.
Hard questions
12A newly produced block can still get reorganized out of the canonical chain if a competing block accumulates more subsequent work or attestations behind it. Each additional block stacked on top makes that reorg exponentially less likely, since an attacker would need to out-produce the entire honest network for that many blocks in a row.
Exchanges calibrate confirmation counts to an asset's attack cost. High-value chains with large validator sets need fewer confirmations for the same practical safety than smaller, cheaper-to-attack chains, which is why the same exchange might ask for 2 confirmations on one chain and 30 or more on another.
A contract sends ETH, or calls an external contract, before updating its own internal state. If the receiving address is a contract with a fallback function, that fallback can call back into the original function before the first call finishes and the state update ever happens, draining funds across nested calls before the balance reaches zero.
This was The DAO's 2016 bug exactly. Its withdraw function sent ETH first and zeroed the balance after, letting an attacker's fallback function recursively call withdraw again and again against a balance that hadn't been decremented yet, as Chainlink's writeup on the exploit traces in detail. Roughly 3.6 million ETH, worth about $60 million at 2016 prices, got drained before the community hard-forked Ethereum to reverse it, which is also how Ethereum Classic came to exist.
The fix is the checks-effects-interactions pattern: validate conditions, update all state, then make the external call last. A reentrancy guard modifier is a belt-and-suspenders backup, not a replacement for getting the order right.
// vulnerable: external call happens before state update
function withdraw(uint amount) external {
require(balances[msg.sender] >= amount);
(bool ok, ) = msg.sender.call{value: amount}("");
require(ok);
balances[msg.sender] -= amount; // too late
}
// fixed: checks-effects-interactions + guard
bool private locked;
modifier nonReentrant() {
require(!locked, "reentrant call");
locked = true;
_;
locked = false;
}
function withdraw(uint amount) external nonReentrant {
require(balances[msg.sender] >= amount, "insufficient balance");
balances[msg.sender] -= amount; // effects first
(bool ok, ) = msg.sender.call{value: amount}(""); // interaction last
require(ok, "transfer failed");
}delegatecall runs the implementation's bytecode but reads and writes the proxy's storage slots by position, not by variable name. Deploy a new implementation that reorders state variables, or inserts one in the middle instead of appending it, and slot 3 in the new contract might correspond to a totally different variable's old data. The bug doesn't revert. It silently reads or writes the wrong data, which is worse than a crash because it can go unnoticed for a while.
// V1 layout
contract V1 {
address public owner; // slot 0
uint256 public balance; // slot 1
bool public paused; // slot 2
}
// UNSAFE V2: inserted a variable in the middle
contract V2Unsafe {
address public owner; // slot 0
uint256 public newFee; // slot 1, collides with old "balance"
uint256 public balance; // slot 2, collides with old "paused"
bool public paused; // slot 3
}
// SAFE V2: only append new variables
contract V2Safe {
address public owner; // slot 0
uint256 public balance; // slot 1
bool public paused; // slot 2
uint256 public newFee; // slot 3, appended
}The practical rule: never reorder or remove inherited state variables in an upgrade, only append. OpenZeppelin's upgrades plugin diffs the storage layout automatically and fails the build if you break it.
A transparent proxy keeps upgrade logic, the upgradeTo function, admin checks, in the proxy itself, and routes admin calls differently from user calls to avoid function selector clashes. That routing costs gas on every single call, a few hundred gas, forever. UUPS moves upgrade logic into the implementation contract instead, so the proxy is a dumb, cheap delegatecall forwarder, and every call skips the admin-check branch entirely.
The catch with UUPS: deploy a new implementation that forgets to include the upgrade function, and the contract becomes permanently unupgradeable, since the proxy has no fallback upgrade logic of its own. OpenZeppelin's UUPSUpgradeable base contract exists specifically so teams don't have to get that right from scratch.
Contracts have a hard 24KB deployed bytecode limit under EIP-170, which large protocols with many features can hit. The diamond pattern splits logic across multiple facet contracts, each implementing a subset of functions, and routes every call through a single proxy that looks up which facet owns a given function selector and delegatecalls into it. From outside it looks like one contract with one address; internally it's composed of many independently upgradeable pieces.
The real cost is complexity. Function selector clashes between facets have to be managed manually, storage across all facets has to follow a strict shared convention, usually a diamond storage pattern using keccak-derived slots to sidestep the ordinary inheritance-slot problem entirely, and debugging a call that spans facets is genuinely harder than debugging a monolithic contract. Most teams only reach for this once they've actually hit the bytecode limit, not preemptively.
MEV, maximal extractable value, is profit a block producer, or a searcher paying a block producer, can extract by choosing which transactions to include and in what order, beyond the normal block reward and fees. A sandwich attack is the clearest example on a DEX: a searcher spots a pending swap in the mempool, front-runs it with their own buy order to push the price up right before the victim's trade executes, lets the victim's trade land at the now-worse price, then immediately back-runs with a sell order that captures the price they artificially created.
Mitigations include private mempools or relays, Flashbots Protect being the widely used one, that hide a transaction from public searchers until it's already included, and setting a tight slippage tolerance so a sandwiched trade reverts instead of executing at a bad price. Neither eliminates MEV entirely, they just change who gets to extract it and how much room there is to do it.
A flash loan lets you borrow an enormous, uncollateralized amount within a single transaction, as long as you repay it plus a fee before that transaction ends. An attacker borrows a large position, swaps heavily against a thin liquidity pool that a target protocol reads directly as its price oracle, artificially spiking or crashing that pool's spot price within the same transaction, then interacts with the target protocol, borrowing against inflated collateral, or liquidating someone at a manipulated price, before repaying the flash loan, all atomically, with no real capital at risk beyond the fee.
The defense isn't detecting the flash loan itself, it's not trusting a single spot price from a manipulable pool as your oracle in the first place. Time-weighted average prices, or an aggregated external feed like Chainlink's, can't be moved within one transaction the way a single pool's instantaneous price can.
A time-weighted average price samples a pool's price at intervals and averages it over a window, 30 minutes, say, rather than reading whatever the price happens to be at the exact instant a transaction executes. Moving a TWAP meaningfully requires sustaining a manipulated price across that entire window, paying the arbitrage cost of other traders correcting it back toward fair value over and over, instead of a single atomic flash-loan-funded swap.
It's not immune. A well-funded attacker can still manipulate a short-window TWAP on a low-liquidity pool, which is exactly why protocols pick both the window length and the underlying pool's liquidity depth deliberately, not as an afterthought.
A raw signed message is opaque to the signer's wallet, they see a hex blob, and the same signature can sometimes get replayed across different contracts or chains if the message content doesn't explicitly bind itself to one. EIP-712 defines a structured, typed data format that wallets can actually display in human-readable form, and includes a domain separator, hashed contract address, chain ID, version, so a signature produced for one contract on one chain becomes meaningless if replayed against a different contract or a different chain.
It's the backbone of gasless approvals and a lot of meta-transaction systems: a user signs an off-chain message once, and a relayer submits it on-chain later, with the domain separator making sure that signature can't be lifted and reused somewhere it was never meant to work.
A standard externally-owned account is controlled by a single private key with no programmable logic. Lose the key and the funds are gone, no multisig, no spending limits, no recovery, unless a separate smart contract wallet is built and everything routed through it manually. ERC-4337 standardizes smart accounts as the primary account type without needing a protocol-level consensus change: a UserOperation gets bundled and submitted by a bundler, validated according to whatever logic the smart account defines, session keys, spending limits, social recovery, and can have gas paid by a third party, a paymaster, so a user can transact without holding the chain's native gas token at all.
The practical shift for interviews: "gasless transactions" almost always means account abstraction with a paymaster covering the gas cost, not that gas stopped existing. Someone is still paying for it, just not necessarily the end user in that specific transaction.
An ECDSA signature is the pair (r, s), and for any valid signature there's a second value, r paired with n minus s (where n is the curve order), that verifies as equally valid for the same message and the same signer, with no access to the private key. Anyone who sees your signature in the mempool or in an event log can flip s and produce a different set of signature bytes over the identical message, and ecrecover returns the same signer for both. Nothing about who authorized what changed, only the encoding did.
This bit Bitcoin early on because its transaction ID was a hash of the whole transaction, so a malleated signature changed the txid without changing what was authorized. It bites Ethereum contracts whenever a signature's raw bytes, rather than a hash of its content, get used as a unique key.
mapping(bytes32 => bool) public usedSignatures;
function claim(bytes calldata sig, bytes32 messageHash) external {
require(!usedSignatures[keccak256(sig)], "already used");
usedSignatures[keccak256(sig)] = true;
//... verify sig against messageHash and pay out
}An attacker grabs a pending valid signature, flips s, resubmits the malleated version, and because it hashes differently the replay guard never catches it, even though it authorizes the exact same claim. The fix is to key replay protection off an explicit nonce or the signed message content, never the signature bytes, and to use a library like OpenZeppelin's ECDSA, which forces s into the lower half of the curve order and rejects the malleable form outright.
A reorg happens when two validators produce competing blocks at close to the same height, both individually valid, and the network briefly has two tips. Nodes build on whichever tip they see first, but once one fork accumulates more total work under proof of work, or more attestation weight under proof of stake, every node following the shorter fork drops those blocks and switches over. Any transaction that only existed in the discarded blocks goes back to the mempool as unconfirmed, unless it also happens to be included in the winning fork.
For an indexer or any off-chain service watching events, this is dangerous because a reorg looks identical to a normal new block arriving. You get a log for a transaction, store it as final, and the chain later reorgs it away with no explicit undo signal from most RPC providers unless you're specifically checking for one. The fix is to never treat a block as final the instant you see it. Track blocks by hash and parent hash, not just number, and on every new block confirm its parent hash matches the tip you already stored. When it doesn't, walk backward to find the common ancestor, mark everything after that point, and everything derived from it, as reverted, then reprocess the winning fork from there. In practice that means holding off on anything user-facing, crediting a deposit, releasing a payout, until you've seen enough confirmations for that chain's finality profile, and building your storage so reverting to an earlier block is a routine, tested operation rather than something you hope never comes up.
Across the mock interview sessions run through LastRoundAI for smart contract and blockchain roles, the reentrancy question rarely trips people up on the first answer anymore. Most candidates can recite checks-effects-interactions correctly. What actually stalls a session is the follow-up: why does a specific storage layout choice matter for the upgrade path, or walk me through what happens if a delegatecall target's storage doesn't match. Those follow-ups separate people who memorized an OpenZeppelin template from people who understand why the template is shaped that way.
The second pattern worth flagging: candidates coming from a web2 background tend to lean hard on security vocabulary, reentrancy, front-running, oracle manipulation, without being able to connect it to a specific line of code they'd change. Candidates with real mainnet deployment experience do the opposite. They describe a bug in terms of the actual diff, not the category it belongs to.
The candidates who get through blockchain loops consistently aren't the ones with the longest list of memorized vulnerabilities. They're the ones who can trace a follow-up question live, without getting flustered when the interviewer asks "why" a second time. Budget real prep time for explaining your reasoning out loud on a contract you didn't write, since that's closer to what an actual audit conversation or a live pairing round feels like than solving a problem alone.
If you're prepping these blockchain developer interview questions and one concept isn't clicking, permit signatures, TWAP oracles, whatever it is, Concept Explainer breaks down any CS or blockchain-specific topic the way interviewers actually test it, not the way a textbook defines it. It runs on the same plan as everything else on LastRoundAI: 15 free credits a month that reset every month, Starter is $19/mo if you need more, and answers come back in under 200 milliseconds across 50+ languages.
For the actual interview, Interview Copilot listens in real time and feeds you structured, sub-200ms guidance during the call itself, invisible on screen share. That matters more for blockchain roles than most, given how often the conversation turns into a live whiteboard trace of a storage layout or a call sequence. It runs on desktop and a mobile-friendly web app, there's no separate native mobile app yet.
LastRoundAI listens to the call and suggests clear, structured answers to questions like the ones above, in real time and invisible on screen share.
How this list was built
Worth being straight about where these questions come from, because plenty of pages in this category are not. The set was compiled from a research pass across official documentation, vendor release notes, published engineering writing and public discussion of hiring processes, then cross-checked against the current version of each technology so nothing here describes behaviour that has since changed.
What that means in practice: these are the questions the material supports as reasonable and current for this role, not a transcript of any one company's loop. We have not sat in on your interview and we are not going to claim we have. Treat the list as well-sourced preparation rather than a leaked question bank, and expect your panel to phrase things their own way.
If you spot something out of date, tell us at contact@lastroundai.com and we will fix it.
Frequently asked questions
What should a blockchain developer put on their resume for interviews?
Outcomes with numbers attached, and the specific tools you personally used rather than the team stack. Interviewers pick questions from your resume, so anything listed there should be something you are happy to be interrogated about.
How do I stand out as a blockchain developer candidate?
Bring one thing that went wrong and what you changed afterwards. Candidates who can narrate a failure honestly consistently read as more senior than candidates with an unbroken record of successes.
What questions should a blockchain developer ask the interviewer?
Something that only applies to this team. Asking what the last thing they shipped was, or what the on-call rotation actually looks like, tells you more than a question about culture and signals that you were listening.
What does a blockchain developer interview usually cover?
A mix of practical skill, judgement on trade-offs, and how you work with people who disagree with you. The technical portion tends to be scoped to what the team actually does rather than a generic syllabus, so read the job description closely.
How much experience do I need to interview as a blockchain developer?
Less than most postings imply. Requirements are usually a wish list, and teams routinely hire people who meet most of it. What is rarely negotiable is being able to evidence the core skill with something you actually built or ran.

