A smart contract developer designs, writes, tests and deploys production contracts that hold real value. The role needs deep Solidity and EVM knowledge, gas optimisation, upgradeable patterns, and strong security habits. Mid-level developers own features end to end, write fuzz and invariant tests, prepare code for audits and work closely with front-end and product teams on Web3 products. Many such roles in India are remote and work with global teams.
Advanced Solidity Fuzz and invariant testing Gas optimisation Proxy upgrades Security patterns
Solved for this roleStart practicing
3 QUESTIONS FREESign up to unlock the full question bank and track your progress.Unlock every question and answer, then continue practising company-wise and package-wise.
✓ 3 questions free • Unlock the rest in seconds
01
Which tools do you use for static analysis and fuzzing of smart contracts?
Slither performs static analysis and quickly flags reentrancy, unchecked calls, shadowing and more. Aderyn is another fast static analyser. Foundry provides fuzz and invariant testing, where random sequences of calls try to break properties you define. Echidna and Medusa are property-based fuzzers. Mythril and Halmos use symbolic execution, and Certora offers formal verification. Combining them with manual review and high test coverage catches far more than any single tool.
02
How does inheritance work in Solidity, including virtual and override?
A contract inherits with is, for example contract Token is ERC20, Ownable. Parent functions that may be changed must be marked virtual, and the child must use override; with several parents you list them, as in override(A, B). Solidity uses C3 linearisation, so the order of the parent list matters, from most base to most derived, and super calls the next contract in that order. Constructors of parents run first in linearised order.
03
Write a Python function that computes a Merkle root from a list of transactions.
Hash leaves, then hash pairs until one remains, duplicating the last hash when a level is odd (Bitcoin style).
import hashlib
def h(b):
return hashlib.sha256(b).digest()
def merkle_root(txs):
level = [h(t.encode()) for t in txs]
while len(level) > 1:
if len(level) % 2:
level.append(level[-1])
level = [h(level[i] + level[i+1]) for i in range(0, len(level), 2)]
return level[0].hex()
print(merkle_root(['tx1', 'tx2', 'tx3']))
04
How do you read a contract's storage slot directly with Foundry's cast?
Use cast storage with the contract address and slot number. For mappings, compute the slot as keccak256(key . slot).
# Read slot 0 (often the first state variable)
cast storage 0xContractAddress 0 --rpc-url $RPC_URL
# balanceOf mapping stored at slot 2, key = user address
cast index address 0xUserAddress 2
cast storage 0xContractAddress <slot-from-above> --rpc-url $RPC_URL
This shows that private variables are not secret: anyone can read storage.
05
Tell me about a bug you missed and what you learned from it.
Situation: I shipped a rewards contract where a rounding error let users claim slightly more than earned when claiming very often. Task: fix it and prevent repeats. Action: I owned the mistake in the team call, wrote a failing test reproducing it, changed the division order to round down in the protocol's favour, and added fuzz tests on claim frequency. Result: losses were tiny, and I now always fuzz any maths involving division.
06
How does arithmetic overflow work in Solidity 0.8, and when do you use unchecked?
Since 0.8.0, arithmetic is checked by default: overflow or underflow reverts with a Panic, so SafeMath is no longer needed. Checks cost a little gas. Wrapping code in an unchecked block disables them, which is safe when you have proven the value cannot overflow, such as a loop counter bounded by an array length, or subtracting after a require that a is at least b. Using unchecked carelessly reintroduces the old overflow bugs.
Cross-function reentrancy happens when an attacker re-enters a different function that shares state with the one making the external call, so a guard on only one function does not help. Read-only reentrancy happens when a view function, such as a pool's price getter, returns inconsistent state mid-execution, and another protocol reads it during the callback and misprices assets. Mitigate by guarding all state-sharing functions, updating state before calls, and checking a reentrancy lock in view functions other protocols rely on.
08
What is Ethereum and how is it different from Bitcoin?
Ethereum is a programmable blockchain: besides transferring its native currency ETH, it runs smart contracts, which are programs stored on-chain that execute exactly as written. Bitcoin focuses on being secure, scarce digital money with a deliberately limited scripting language and the UTXO model. Ethereum uses an account model, Proof of Stake since 2022, about 12-second blocks, and a large ecosystem of tokens, DeFi, NFTs and Layer 2 rollups built on top.
09
A transaction keeps failing with an out of gas error. How do you fix it?
First see whether the gas limit was too low or the function is genuinely unbounded. Run eth_estimateGas or simulate with cast run or Tenderly to see actual gas used and where it fails. If estimation is fine, raise the limit with a buffer of 10 to 20 percent. If the function loops over a growing array, redesign it with pagination or pull-based patterns, because the block gas limit will eventually make it impossible to call.
Inherit the audited ERC20 implementation and mint the initial supply in the constructor.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {ERC20} from '@openzeppelin/contracts/token/ERC20/ERC20.sol';
import {Ownable} from '@openzeppelin/contracts/access/Ownable.sol';
contract CampusToken is ERC20, Ownable {
constructor() ERC20('Campus Token', 'CAMP') Ownable(msg.sender) {
_mint(msg.sender, 1_000_000 * 10 ** decimals());
}
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
}
Typical bugs include missing onlyOwner or role checks on sensitive functions such as mint, withdraw or setOracle, initialiser functions in upgradeable contracts that anyone can call, misconfigured roles, and a single EOA owning critical powers. Use OpenZeppelin Ownable2Step or AccessControl, give each role the minimum rights, protect admin actions with a multisig and timelock, and write tests confirming that every privileged function reverts for unauthorised callers.
12
How did you handle a time you received critical code review feedback?
Situation: a senior reviewer rejected my pull request for a staking feature, saying the storage layout would break future upgrades. Task: fix it and learn. Action: I thanked him, asked him to explain the risk, read about EIP-1967 and storage gaps, rewrote the contract with namespaced storage, and added a storage-layout check to CI. Result: the PR merged, and that check later caught a teammate's layout change before an upgrade.
The trilemma says a blockchain finds it hard to maximise decentralisation, security and scalability at the same time on a single layer. Raising block size or using few validators increases throughput but reduces decentralisation. Ethereum's answer is a rollup-centric roadmap: keep the base layer decentralised and secure, and move execution to Layer 2 rollups that post data back to Layer 1. Many high-throughput chains instead accept fewer, more powerful validators.
14
How do you get the latest block number from an Ethereum node using JSON-RPC?
Send a JSON-RPC POST request to the node or provider URL. The result is a hex string that you convert to decimal.
curl -s -X POST https://rpc.example.org \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}'
# Response: {"jsonrpc":"2.0","id":1,"result":"0x15d3a1c"}
# In Python: int('0x15d3a1c', 16)
Other common methods are eth_getBalance, eth_call and eth_getTransactionReceipt.
The ABI (Application Binary Interface) is the JSON description of a contract's functions, events and errors, and it defines how arguments are encoded. A function selector is the first four bytes of the Keccak-256 hash of the function signature, for example transfer(address,uint256) gives 0xa9059cbb. Calldata is the selector followed by ABI-encoded arguments in 32-byte words. Wallets, ethers.js and explorers use the ABI to encode calls and decode results and events.
16
A user lost funds on our DeFi app due to their own mistake and is angry. How would you handle it?
Situation: a user sent tokens to the contract address directly instead of using deposit, and they were stuck. Task: support him honestly. Action: I listened, explained calmly what happened without blaming him, checked whether a recovery function existed, and escalated to the team. We had a rescue function for stray tokens behind the multisig, so after verification we returned them. Result: he thanked us publicly, and we added a front-end warning.
17
What happens, step by step, when you send an Ethereum transaction?
The wallet builds the transaction with nonce, to, value, data, gas limit and fee fields, signs it with the private key, and sends it to a node via eth_sendRawTransaction. The node validates and gossips it to other mempools. A block builder includes it, and the proposer publishes the block. Every node executes it in the EVM, updating state and producing a receipt with status, gas used and logs. After about two epochs the block is finalised.
18
How do you compute a function selector and decode calldata with cast?
cast handles ABI work from the command line.
# Function selector
cast sig 'transfer(address,uint256)'
# 0xa9059cbb
# Encode calldata
cast calldata 'transfer(address,uint256)' 0xAbc... 1000000
# Decode calldata from a transaction
cast 4byte-decode 0xa9059cbb000000...
# Decode with a known signature
cast decode-calldata 'transfer(address,uint256)' 0xa9059cbb...
This is useful when checking what a wallet is really asking you to sign.
19
Write a Foundry unit test and a fuzz test for a contract.
Foundry tests are Solidity contracts; functions starting with test run as tests, and parameters make them fuzz tests.
import {Test} from 'forge-std/Test.sol';
import {Vault} from '../src/Vault.sol';
contract VaultTest is Test {
Vault v;
function setUp() public { v = new Vault(); }
function testDeposit() public {
v.deposit{value: 1 ether}();
assertEq(v.balanceOf(address(this)), 1 ether);
}
function testFuzzDeposit(uint96 amt) public {
vm.assume(amt > 0);
vm.deal(address(this), amt);
v.deposit{value: amt}();
assertEq(address(v).balance, amt);
}
}
Run with forge test -vvv.
20
You need the same contract at the same address on five chains. How do you do it?
Use CREATE2 through a deterministic deployment factory that already exists at the same address on all those chains. Keep the init code identical: same compiler version, optimiser settings, constructor arguments and metadata. Pick a salt, compute the expected address off-chain, then deploy on each chain. Foundry scripts support this by passing a salt. Avoid constructor arguments that differ per chain, or move chain-specific settings into an initialiser restricted to the deployer.
21
An auditor flags that a single owner key can mint tokens and change fees. How do you address it?
This is centralisation risk: if the key is compromised or misused, users lose funds. Move ownership to a multisig with signers on separate devices, add a timelock so users can see and react to changes, set hard-coded bounds such as a maximum fee, and remove powers that are not needed, like unlimited minting. Use role-based access instead of one all-powerful owner, and document the remaining admin powers publicly.
22
What is the difference between CREATE and CREATE2?
CREATE computes a new contract's address from the deployer's address and nonce, so the address depends on deployment order. CREATE2 computes it from the deployer address, a chosen 32-byte salt and the hash of the init code, so the address is known before deployment and is the same wherever those inputs match. CREATE2 enables counterfactual wallets, deterministic factory deployments and same-address deployments across chains, but redeploying at the same address needs care.
23
What are front-running and sandwich attacks, and how do you mitigate them?
Pending transactions are public, so bots can copy or outbid them. Front-running is placing a transaction ahead of a victim's, for example buying before a large purchase. A sandwich attack buys before the victim's swap and sells right after, profiting from the price impact. Mitigations include slippage limits and deadlines, commit-reveal schemes, batch auctions, private transaction relays, and design that does not reward ordering, such as using time-weighted prices.
Type 0 is the legacy transaction with a single gasPrice. Type 1, from EIP-2930, adds an access list. Type 2, from EIP-1559, uses maxFeePerGas and maxPriorityFeePerGas and is the default in wallets. Type 3, from EIP-4844, carries blobs for rollup data. Type 4, from EIP-7702 in the Pectra upgrade of May 2025, lets an EOA set code delegation through a signed authorisation list, enabling batching and sponsored gas for normal wallets.
25
Write a basic voting contract using a struct, mapping and event.
Build or use an indexer. An indexer listens to contract events from a start block, transforms them and stores them in a database like PostgreSQL, so the front-end runs normal fast queries. Options are a subgraph on The Graph, hosted indexing services, or a custom service with ethers and a queue. It must handle reorgs by tracking block hashes, support backfilling, and expose an API. Design events up front with indexed fields that queries need.
27
Write a Foundry invariant test for a token's total supply.
Invariant tests call random functions on target contracts and check a property after every call.
import {Test} from 'forge-std/Test.sol';
import {MyToken} from '../src/MyToken.sol';
import {Handler} from './Handler.sol';
contract SupplyInvariant is Test {
MyToken token;
Handler handler;
function setUp() public {
token = new MyToken();
handler = new Handler(token);
targetContract(address(handler));
}
function invariant_supplyMatchesBalances() public view {
assertEq(token.totalSupply(), handler.sumOfBalances());
}
}
The handler bounds inputs and tracks actors so calls stay realistic.
28
How are mappings stored, and why can you not iterate over them?
A mapping does not store keys. The value for key k in a mapping declared at slot p lives at storage slot keccak256(k . p). Every possible key implicitly exists with a default zero value, so the contract cannot know which keys were set or how many. To iterate, keep a separate array of keys or use OpenZeppelin EnumerableSet or EnumerableMap, accepting extra gas, or rebuild the list off-chain from emitted events.
EIP-1559 splits the fee into a base fee and a priority fee. The protocol sets the base fee per block from demand: if the previous block was more than half full, it rises up to 12.5 percent, otherwise it falls. The base fee is burned. Users add a priority fee (tip) for the proposer and set maxFeePerGas as their cap. They pay base fee plus tip, never more than the cap, which makes fees more predictable.
30
Tell me about a time you learnt a new technology quickly to meet a deadline.
Situation: in my internship I was asked to add NFT minting to a demo in ten days, and I had never written Solidity. Task: deliver a working, tested contract. Action: I spent two days on Solidity basics and OpenZeppelin docs, built the ERC-721 contract, asked a senior to review my approach early, and wrote Foundry tests for every function. Result: the demo shipped on time on Sepolia, and the review caught only minor issues.
31
What is the difference between require, revert, assert and custom errors?
require(condition, message) validates inputs and conditions, reverting with a message if false. revert stops execution explicitly and is often used with custom errors, such as error InsufficientBalance(uint256 need, uint256 have); revert InsufficientBalance(x, y); which are cheaper than strings and carry data. assert checks invariants that should never fail; a failure signals a bug and raises a Panic. Since Solidity 0.8.26 you can also pass custom errors to require.
32
Your team is preparing a contract for an external audit. What do you do beforehand?
Freeze the code at a commit and stop adding features. Write clear documentation: architecture, roles, trust assumptions, invariants and known risks. Reach high test coverage including fuzz and invariant tests, and run Slither and fix its findings. Remove dead code, add NatSpec comments, and make the build reproducible. Share a scope list and a walkthrough call. A clean, well-documented codebase lets auditors spend their limited time on deep logic bugs.
33
Compare interfaces, abstract contracts and libraries.
An interface only declares external functions, events and errors, with no state or implementation; it is used to call other contracts, like IERC20. An abstract contract can have state and implemented functions but leaves at least one unimplemented or is marked abstract, so it cannot be deployed alone. A library holds reusable functions without state; internal library functions are inlined, while external ones are deployed once and called via delegatecall. Libraries can attach to types with using for.
34
How can a smart contract suffer denial of service?
Common causes: loops over arrays that grow with users until they exceed the block gas limit; push payments in a loop where one reverting recipient blocks everyone; relying on an external call that an attacker can make fail; and griefing by filling storage cheaply. For example, an auction that refunds the previous bidder will freeze if that bidder is a contract that reverts. Use pull payments, bounded loops, pagination and failure isolation.
35
What is a signature replay attack and how do you prevent it?
If a contract accepts a signed message to perform an action, an attacker can resubmit the same signature to repeat it, or replay it on another chain or another contract using the same code. Prevent it by including a per-user nonce that increments on use, a deadline, the chain ID and the verifying contract address in the signed data. EIP-712 typed data with a domain separator gives a standard way to include these fields.
36
What is account abstraction, and how do ERC-4337 and EIP-7702 differ?
Account abstraction makes accounts programmable, so wallets can support batching, gas paid in tokens or by sponsors, passkeys, spending limits and social recovery. ERC-4337 does this without protocol changes: users send UserOperations to an alternate mempool, bundlers submit them to an EntryPoint contract, and paymasters can sponsor gas. EIP-7702 is a protocol change that lets an existing EOA point to smart account code, so users keep their address while gaining these features.
37
Explain storage, memory, stack and calldata in the EVM.
Storage is persistent key-value data of 32-byte slots per contract; it is the most expensive, and writes to new slots cost about 20,000 gas. Memory is a temporary byte array cleared after each call, cheap but growing in cost quadratically. The stack holds up to 1024 words for opcode operands. Calldata is the read-only input data of the transaction, the cheapest place to read external function arguments from. Choosing the right location is a key gas optimisation.
38
Your protocol is being exploited right now. What do you do?
Act fast with a prepared runbook. Pause affected contracts through the guardian or multisig, and disable front-end actions. Gather the core team and security partners in a war room. Trace the attack transactions to understand the bug and estimate losses, and check whether a white-hat rescue of remaining funds is needed. Communicate publicly with facts and no speculation, contact exchanges and bridges to flag stolen funds, then fix, audit the fix and publish a post-mortem.
39
How does a constant product automated market maker work, and what is impermanent loss?
A constant product AMM holds two tokens with reserves x and y and keeps x times y equal to k. A trader adds one token and removes the other so that k stays constant after fees, which makes price equal to the reserve ratio and rise with trade size. Liquidity providers earn fees, but when prices move, arbitrage rebalances the pool, leaving them worse off than simply holding; that gap is impermanent loss.
40
How do upgradeable contracts work, and how do Transparent and UUPS proxies differ?
A proxy holds the state and delegates every call to an implementation contract whose address is stored in a standard EIP-1967 slot; upgrading means pointing to a new implementation. In the Transparent pattern, upgrade logic lives in the proxy and an admin cannot call implementation functions, which avoids selector clashes. In UUPS, upgrade logic lives in the implementation, making proxies cheaper, but a faulty upgrade can brick it. Both use initialisers instead of constructors and require storage-layout compatibility.
Frequently asked questions
What does a Smart Contract Developer do?
A smart contract developer designs, writes, tests and deploys production contracts that hold real value. The role needs deep Solidity and EVM knowledge, gas optimisation, upgradeable patterns, and strong security habits. Mid-level developers own features end to end, write fuzz and invariant tests, prepare code for audits and work closely with front-end and product teams on Web3 products. Many such roles in India are remote and work with global teams.
Which skills are tested in a Smart Contract Developer interview?
Expect questions on Advanced Solidity, Fuzz and invariant testing, Gas optimisation, Proxy upgrades, Security patterns. Most rounds also ask you to explain a project and solve one practical problem live.
How do I prepare for a Smart Contract Developer interview?
Revise Solidity Programming, Smart Contract Security, Ethereum and the EVM first using the topic sheets, practise the questions on this page aloud, then do one full mock interview to practise timing.