Web3 Interview Questions - Medium

Medium-level Web3 interview questions covering DeFi, advanced Solidity, security, and protocol design.

Q1: Explain DeFi and common DeFi protocols.

Answer:

DeFi Stack

Automated Market Maker (AMM)

Constant Product Formula: $x \times y = k$


Q2: What are Solidity modifiers and function visibility?

Answer:

Modifiers

 1contract AccessControl {
 2    address public owner;
 3    mapping(address => bool) public admins;
 4    
 5    constructor() {
 6        owner = msg.sender;
 7    }
 8    
 9    // Modifier: reusable check
10    modifier onlyOwner() {
11        require(msg.sender == owner, "Not owner");
12        _; // Continue execution
13    }
14    
15    modifier onlyAdmin() {
16        require(admins[msg.sender], "Not admin");
17        _;
18    }
19    
20    modifier validAddress(address _addr) {
21        require(_addr != address(0), "Invalid address");
22        _;
23    }
24    
25    // Using modifiers
26    function transferOwnership(address newOwner) 
27        public 
28        onlyOwner 
29        validAddress(newOwner) 
30    {
31        owner = newOwner;
32    }
33    
34    function addAdmin(address admin) public onlyOwner {
35        admins[admin] = true;
36    }
37    
38    function restrictedFunction() public onlyAdmin {
39        // Admin-only logic
40    }
41}

Function Types

 1contract FunctionTypes {
 2    uint256 public value;
 3    
 4    // public: Can be called internally and externally
 5    function publicFunction() public returns (uint256) {
 6        return value;
 7    }
 8    
 9    // external: Only external calls (more gas efficient)
10    function externalFunction() external returns (uint256) {
11        return value;
12    }
13    
14    // internal: This contract and derived contracts
15    function internalFunction() internal returns (uint256) {
16        return value * 2;
17    }
18    
19    // private: Only this contract
20    function privateFunction() private returns (uint256) {
21        return value * 3;
22    }
23    
24    // view: Reads state, doesn't modify
25    function viewFunction() public view returns (uint256) {
26        return value;
27    }
28    
29    // pure: Doesn't read or modify state
30    function pureFunction(uint256 a, uint256 b) public pure returns (uint256) {
31        return a + b;
32    }
33    
34    // payable: Can receive ETH
35    function payableFunction() public payable {
36        // msg.value contains sent ETH
37    }
38}

Q3: Explain reentrancy attacks and how to prevent them.

Answer:

Vulnerable Contract

 1// ❌ VULNERABLE to reentrancy
 2contract VulnerableBank {
 3    mapping(address => uint256) public balances;
 4    
 5    function deposit() public payable {
 6        balances[msg.sender] += msg.value;
 7    }
 8    
 9    function withdraw(uint256 amount) public {
10        require(balances[msg.sender] >= amount, "Insufficient balance");
11        
12        // DANGER: External call before state update
13        (bool success, ) = msg.sender.call{value: amount}("");
14        require(success, "Transfer failed");
15        
16        // State updated AFTER external call
17        balances[msg.sender] -= amount;
18    }
19}

Attack Contract

 1contract Attacker {
 2    VulnerableBank public bank;
 3    
 4    constructor(address _bank) {
 5        bank = VulnerableBank(_bank);
 6    }
 7    
 8    function attack() public payable {
 9        bank.deposit{value: 1 ether}();
10        bank.withdraw(1 ether);
11    }
12    
13    // Fallback function called when receiving ETH
14    receive() external payable {
15        if (address(bank).balance >= 1 ether) {
16            bank.withdraw(1 ether); // Reenter!
17        }
18    }
19}

Attack Flow

Prevention Methods

 1// ✅ Method 1: Checks-Effects-Interactions Pattern
 2contract SafeBank1 {
 3    mapping(address => uint256) public balances;
 4    
 5    function withdraw(uint256 amount) public {
 6        require(balances[msg.sender] >= amount, "Insufficient balance");
 7        
 8        // Update state BEFORE external call
 9        balances[msg.sender] -= amount;
10        
11        // External call last
12        (bool success, ) = msg.sender.call{value: amount}("");
13        require(success, "Transfer failed");
14    }
15}
16
17// ✅ Method 2: ReentrancyGuard
18import "@openzeppelin/contracts/security/ReentrancyGuard.sol";
19
20contract SafeBank2 is ReentrancyGuard {
21    mapping(address => uint256) public balances;
22    
23    function withdraw(uint256 amount) public nonReentrant {
24        require(balances[msg.sender] >= amount, "Insufficient balance");
25        
26        balances[msg.sender] -= amount;
27        (bool success, ) = msg.sender.call{value: amount}("");
28        require(success, "Transfer failed");
29    }
30}
31
32// ✅ Method 3: Pull over Push
33contract SafeBank3 {
34    mapping(address => uint256) public balances;
35    mapping(address => uint256) public pendingWithdrawals;
36    
37    function initiateWithdrawal(uint256 amount) public {
38        require(balances[msg.sender] >= amount, "Insufficient balance");
39        balances[msg.sender] -= amount;
40        pendingWithdrawals[msg.sender] += amount;
41    }
42    
43    function completeWithdrawal() public {
44        uint256 amount = pendingWithdrawals[msg.sender];
45        require(amount > 0, "No pending withdrawal");
46        
47        pendingWithdrawals[msg.sender] = 0;
48        (bool success, ) = msg.sender.call{value: amount}("");
49        require(success, "Transfer failed");
50    }
51}

Q4: What are proxy patterns and upgradeable contracts?

Answer:

Proxy Architecture

Transparent Proxy Pattern

 1// Proxy contract (never changes)
 2contract TransparentProxy {
 3    address public implementation;
 4    address public admin;
 5    
 6    constructor(address _implementation) {
 7        implementation = _implementation;
 8        admin = msg.sender;
 9    }
10    
11    modifier onlyAdmin() {
12        require(msg.sender == admin, "Not admin");
13        _;
14    }
15    
16    function upgradeTo(address newImplementation) external onlyAdmin {
17        implementation = newImplementation;
18    }
19    
20    fallback() external payable {
21        address impl = implementation;
22        
23        assembly {
24            // Copy calldata
25            calldatacopy(0, 0, calldatasize())
26            
27            // Delegatecall to implementation
28            let result := delegatecall(gas(), impl, 0, calldatasize(), 0, 0)
29            
30            // Copy return data
31            returndatacopy(0, 0, returndatasize())
32            
33            switch result
34            case 0 { revert(0, returndatasize()) }
35            default { return(0, returndatasize()) }
36        }
37    }
38}
39
40// Implementation V1
41contract ImplementationV1 {
42    uint256 public value;
43    
44    function setValue(uint256 _value) public {
45        value = _value;
46    }
47    
48    function getValue() public view returns (uint256) {
49        return value;
50    }
51}
52
53// Implementation V2 (upgraded)
54contract ImplementationV2 {
55    uint256 public value;
56    
57    function setValue(uint256 _value) public {
58        value = _value;
59    }
60    
61    function getValue() public view returns (uint256) {
62        return value;
63    }
64    
65    // New function
66    function increment() public {
67        value++;
68    }
69}

Storage Collision

Solution: Use storage gaps or structured storage

 1contract ImplementationV1 {
 2    // Reserve slots for proxy
 3    bytes32 private constant IMPLEMENTATION_SLOT = 
 4        bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);
 5    bytes32 private constant ADMIN_SLOT = 
 6        bytes32(uint256(keccak256("eip1967.proxy.admin")) - 1);
 7    
 8    // Implementation storage starts here
 9    uint256 public value;
10}

Answer:

Oracle Architecture

 1import "@chainlink/contracts/src/v0.8/interfaces/AggregatorV3Interface.sol";
 2
 3contract PriceConsumer {
 4    AggregatorV3Interface internal priceFeed;
 5    
 6    constructor() {
 7        // ETH/USD price feed on Ethereum mainnet
 8        priceFeed = AggregatorV3Interface(
 9            0x5f4eC3Df9cbd43714FE2740f5E3616155c5b8419
10        );
11    }
12    
13    function getLatestPrice() public view returns (int) {
14        (
15            uint80 roundId,
16            int price,
17            uint startedAt,
18            uint updatedAt,
19            uint80 answeredInRound
20        ) = priceFeed.latestRoundData();
21        
22        return price; // Returns price with 8 decimals
23    }
24    
25    function getDecimals() public view returns (uint8) {
26        return priceFeed.decimals();
27    }
28}

Custom Oracle Request

 1import "@chainlink/contracts/src/v0.8/ChainlinkClient.sol";
 2
 3contract APIConsumer is ChainlinkClient {
 4    using Chainlink for Chainlink.Request;
 5    
 6    uint256 public volume;
 7    address private oracle;
 8    bytes32 private jobId;
 9    uint256 private fee;
10    
11    constructor() {
12        setPublicChainlinkToken();
13        oracle = 0x...; // Oracle address
14        jobId = "..."; // Job ID
15        fee = 0.1 * 10 ** 18; // 0.1 LINK
16    }
17    
18    function requestVolumeData() public returns (bytes32 requestId) {
19        Chainlink.Request memory request = buildChainlinkRequest(
20            jobId,
21            address(this),
22            this.fulfill.selector
23        );
24        
25        // Set the URL to perform the GET request on
26        request.add("get", "https://api.example.com/volume");
27        request.add("path", "data.volume");
28        
29        // Sends the request
30        return sendChainlinkRequestTo(oracle, request, fee);
31    }
32    
33    function fulfill(bytes32 _requestId, uint256 _volume) public recordChainlinkFulfillment(_requestId) {
34        volume = _volume;
35    }
36}

Q6: What are Layer 2 solutions?

Answer:

Rollup Architecture

Optimistic vs ZK Rollups

Bridge Contract

 1// Simplified L1 → L2 bridge
 2contract L1Bridge {
 3    address public l2Bridge;
 4    
 5    event DepositInitiated(
 6        address indexed from,
 7        address indexed to,
 8        uint256 amount
 9    );
10    
11    function depositETH(address to) public payable {
12        require(msg.value > 0, "Must send ETH");
13        
14        emit DepositInitiated(msg.sender, to, msg.value);
15        
16        // L2 sequencer watches this event
17        // and mints equivalent on L2
18    }
19    
20    function finalizeWithdrawal(
21        address to,
22        uint256 amount,
23        bytes memory proof
24    ) public {
25        // Verify proof from L2
26        require(verifyProof(proof), "Invalid proof");
27        
28        // Transfer ETH
29        (bool success, ) = to.call{value: amount}("");
30        require(success, "Transfer failed");
31    }
32    
33    function verifyProof(bytes memory proof) internal view returns (bool) {
34        // Verify Merkle proof or fraud proof
35        // Implementation depends on L2 type
36        return true;
37    }
38}

Q7: Explain EIP-1559 and gas optimization.

Answer:

Gas Calculation (EIP-1559)

Formula: $$\text{Total Fee} = \text{Gas Used} \times (\text{Base Fee} + \text{Priority Fee})$$

Gas Optimization Techniques

 1contract GasOptimization {
 2    // ❌ Expensive: Storage writes
 3    uint256 public counter;
 4    
 5    function incrementBad() public {
 6        counter = counter + 1; // ~5,000 gas
 7    }
 8    
 9    // ✅ Better: Use += operator
10    function incrementGood() public {
11        counter += 1; // ~5,000 gas (same, but clearer)
12    }
13    
14    // ✅ Best: Cache in memory
15    function incrementBest() public {
16        uint256 temp = counter;
17        temp += 1;
18        counter = temp;
19    }
20    
21    // ❌ Expensive: Dynamic array in storage
22    uint256[] public numbers;
23    
24    function sumBad() public view returns (uint256) {
25        uint256 total = 0;
26        for (uint256 i = 0; i < numbers.length; i++) {
27            total += numbers[i]; // Each read: ~2,100 gas
28        }
29        return total;
30    }
31    
32    // ✅ Better: Cache length
33    function sumGood() public view returns (uint256) {
34        uint256 total = 0;
35        uint256 length = numbers.length; // Cache length
36        for (uint256 i = 0; i < length; i++) {
37            total += numbers[i];
38        }
39        return total;
40    }
41    
42    // ✅ Use calldata for external functions
43    function processBad(uint256[] memory data) public pure returns (uint256) {
44        return data.length;
45    }
46    
47    function processGood(uint256[] calldata data) external pure returns (uint256) {
48        return data.length; // Cheaper: no copy to memory
49    }
50    
51    // ✅ Pack variables
52    struct Inefficient {
53        uint8 a;    // Slot 0
54        uint256 b;  // Slot 1
55        uint8 c;    // Slot 2
56    }
57    
58    struct Efficient {
59        uint8 a;    // Slot 0
60        uint8 c;    // Slot 0 (packed)
61        uint256 b;  // Slot 1
62    }
63    
64    // ✅ Use events instead of storage
65    event DataStored(uint256 indexed id, string data);
66    
67    function storeInEvent(uint256 id, string memory data) public {
68        emit DataStored(id, data); // Much cheaper than storage
69    }
70    
71    // ✅ Short-circuit conditions
72    function checkBad(uint256 a, uint256 b) public pure returns (bool) {
73        return expensiveCheck(a) && cheapCheck(b);
74    }
75    
76    function checkGood(uint256 a, uint256 b) public pure returns (bool) {
77        return cheapCheck(b) && expensiveCheck(a); // Cheap first
78    }
79    
80    function cheapCheck(uint256 x) internal pure returns (bool) {
81        return x > 0;
82    }
83    
84    function expensiveCheck(uint256 x) internal pure returns (bool) {
85        // Expensive computation
86        return x % 2 == 0;
87    }
88}

Q8: What are signatures and how to verify them on-chain?

Answer:

Signature Process

Off-chain Signing

 1const { ethers } = require('ethers');
 2
 3// Sign message off-chain
 4async function signMessage(wallet, message) {
 5  // Create message hash
 6  const messageHash = ethers.utils.id(message);
 7  
 8  // Sign
 9  const signature = await wallet.signMessage(ethers.utils.arrayify(messageHash));
10  
11  // Split signature
12  const sig = ethers.utils.splitSignature(signature);
13  
14  return {
15    message,
16    messageHash,
17    signature,
18    r: sig.r,
19    s: sig.s,
20    v: sig.v
21  };
22}
23
24// Example
25const wallet = new ethers.Wallet('0x...');
26const signed = await signMessage(wallet, "Hello World");

On-chain Verification

 1contract SignatureVerifier {
 2    function verifySignature(
 3        address signer,
 4        string memory message,
 5        bytes memory signature
 6    ) public pure returns (bool) {
 7        bytes32 messageHash = keccak256(abi.encodePacked(message));
 8        bytes32 ethSignedMessageHash = getEthSignedMessageHash(messageHash);
 9        
10        address recoveredSigner = recoverSigner(ethSignedMessageHash, signature);
11        return recoveredSigner == signer;
12    }
13    
14    function getEthSignedMessageHash(bytes32 messageHash) 
15        internal 
16        pure 
17        returns (bytes32) 
18    {
19        return keccak256(
20            abi.encodePacked("\x19Ethereum Signed Message:\n32", messageHash)
21        );
22    }
23    
24    function recoverSigner(bytes32 ethSignedMessageHash, bytes memory signature)
25        internal
26        pure
27        returns (address)
28    {
29        (bytes32 r, bytes32 s, uint8 v) = splitSignature(signature);
30        return ecrecover(ethSignedMessageHash, v, r, s);
31    }
32    
33    function splitSignature(bytes memory sig)
34        internal
35        pure
36        returns (bytes32 r, bytes32 s, uint8 v)
37    {
38        require(sig.length == 65, "Invalid signature length");
39        
40        assembly {
41            r := mload(add(sig, 32))
42            s := mload(add(sig, 64))
43            v := byte(0, mload(add(sig, 96)))
44        }
45    }
46}
47
48// Usage: Meta-transactions
49contract MetaTransaction {
50    mapping(address => uint256) public nonces;
51    
52    function executeMetaTransaction(
53        address user,
54        bytes memory functionSignature,
55        bytes32 r,
56        bytes32 s,
57        uint8 v
58    ) public returns (bytes memory) {
59        bytes32 messageHash = keccak256(
60            abi.encodePacked(user, functionSignature, nonces[user])
61        );
62        
63        address signer = ecrecover(messageHash, v, r, s);
64        require(signer == user, "Invalid signature");
65        
66        nonces[user]++;
67        
68        (bool success, bytes memory returnData) = address(this).call(
69            functionSignature
70        );
71        require(success, "Function call failed");
72        
73        return returnData;
74    }
75}

Q9: Explain flash loans and how they work.

Answer:

Flash Loan Flow

Flash Loan Implementation

 1// Aave flash loan interface
 2interface IFlashLoanReceiver {
 3    function executeOperation(
 4        address[] calldata assets,
 5        uint256[] calldata amounts,
 6        uint256[] calldata premiums,
 7        address initiator,
 8        bytes calldata params
 9    ) external returns (bool);
10}
11
12contract FlashLoanArbitrage is IFlashLoanReceiver {
13    ILendingPool public lendingPool;
14    
15    constructor(address _lendingPool) {
16        lendingPool = ILendingPool(_lendingPool);
17    }
18    
19    function executeFlashLoan(address asset, uint256 amount) public {
20        address[] memory assets = new address[](1);
21        assets[0] = asset;
22        
23        uint256[] memory amounts = new uint256[](1);
24        amounts[0] = amount;
25        
26        uint256[] memory modes = new uint256[](1);
27        modes[0] = 0; // 0 = no debt, flash loan
28        
29        lendingPool.flashLoan(
30            address(this),
31            assets,
32            amounts,
33            modes,
34            address(this),
35            "",
36            0
37        );
38    }
39    
40    function executeOperation(
41        address[] calldata assets,
42        uint256[] calldata amounts,
43        uint256[] calldata premiums,
44        address initiator,
45        bytes calldata params
46    ) external override returns (bool) {
47        // This function is called by the lending pool
48        // You have the borrowed funds here
49        
50        // 1. Perform arbitrage or other operations
51        performArbitrage(assets[0], amounts[0]);
52        
53        // 2. Approve the lending pool to pull the funds back
54        uint256 amountOwed = amounts[0] + premiums[0];
55        IERC20(assets[0]).approve(address(lendingPool), amountOwed);
56        
57        // 3. Return true to indicate success
58        return true;
59    }
60    
61    function performArbitrage(address asset, uint256 amount) internal {
62        // Example: Buy low on DEX1, sell high on DEX2
63        
64        // Buy on Uniswap
65        address[] memory path = new address[](2);
66        path[0] = asset;
67        path[1] = address(USDC);
68        
69        IUniswapV2Router(uniswapRouter).swapExactTokensForTokens(
70            amount,
71            0,
72            path,
73            address(this),
74            block.timestamp
75        );
76        
77        // Sell on Sushiswap (if price is higher)
78        path[0] = address(USDC);
79        path[1] = asset;
80        
81        IUniswapV2Router(sushiswapRouter).swapExactTokensForTokens(
82            USDC.balanceOf(address(this)),
83            amount, // Must get back at least what we borrowed + fee
84            path,
85            address(this),
86            block.timestamp
87        );
88    }
89}

Use Cases:

  • Arbitrage between DEXes
  • Collateral swap
  • Self-liquidation
  • Debt refinancing

Q10: What are common smart contract vulnerabilities?

Answer:

Integer Overflow (Pre-Solidity 0.8)

 1// ❌ Vulnerable (Solidity < 0.8)
 2contract Vulnerable {
 3    uint8 public count = 255;
 4    
 5    function increment() public {
 6        count++; // Overflows to 0
 7    }
 8}
 9
10// ✅ Fixed: Use SafeMath or Solidity 0.8+
11import "@openzeppelin/contracts/utils/math/SafeMath.sol";
12
13contract Safe {
14    using SafeMath for uint8;
15    uint8 public count = 255;
16    
17    function increment() public {
18        count = count.add(1); // Reverts on overflow
19    }
20}
21
22// ✅ Solidity 0.8+ has built-in overflow checks
23contract SafeModern {
24    uint8 public count = 255;
25    
26    function increment() public {
27        count++; // Automatically reverts on overflow
28    }
29}

Access Control Vulnerability

 1// ❌ Vulnerable: Missing access control
 2contract VulnerableWallet {
 3    function withdraw(uint256 amount) public {
 4        // Anyone can call this!
 5        payable(msg.sender).transfer(amount);
 6    }
 7}
 8
 9// ✅ Fixed: Proper access control
10contract SafeWallet {
11    address public owner;
12    
13    constructor() {
14        owner = msg.sender;
15    }
16    
17    modifier onlyOwner() {
18        require(msg.sender == owner, "Not owner");
19        _;
20    }
21    
22    function withdraw(uint256 amount) public onlyOwner {
23        payable(msg.sender).transfer(amount);
24    }
25}

Front-Running

 1// ❌ Vulnerable to front-running
 2contract VulnerableAuction {
 3    uint256 public highestBid;
 4    address public highestBidder;
 5    
 6    function bid() public payable {
 7        require(msg.value > highestBid, "Bid too low");
 8        
 9        // Refund previous bidder
10        payable(highestBidder).transfer(highestBid);
11        
12        highestBid = msg.value;
13        highestBidder = msg.sender;
14    }
15}
16
17// ✅ Better: Commit-reveal scheme
18contract SafeAuction {
19    mapping(address => bytes32) public commitments;
20    mapping(address => uint256) public bids;
21    
22    // Phase 1: Commit
23    function commit(bytes32 commitment) public {
24        commitments[msg.sender] = commitment;
25    }
26    
27    // Phase 2: Reveal
28    function reveal(uint256 bid, bytes32 secret) public payable {
29        require(
30            keccak256(abi.encodePacked(bid, secret)) == commitments[msg.sender],
31            "Invalid reveal"
32        );
33        require(msg.value == bid, "Incorrect payment");
34        
35        bids[msg.sender] = bid;
36    }
37}

Denial of Service

 1// ❌ Vulnerable: Unbounded loop
 2contract VulnerableDistributor {
 3    address[] public recipients;
 4    
 5    function distribute() public {
 6        for (uint256 i = 0; i < recipients.length; i++) {
 7            payable(recipients[i]).transfer(1 ether);
 8            // If one transfer fails, entire function fails
 9            // If array is too large, runs out of gas
10        }
11    }
12}
13
14// ✅ Fixed: Pull over push
15contract SafeDistributor {
16    mapping(address => uint256) public balances;
17    
18    function allocate(address[] memory recipients) public {
19        for (uint256 i = 0; i < recipients.length; i++) {
20            balances[recipients[i]] += 1 ether;
21        }
22    }
23    
24    function withdraw() public {
25        uint256 amount = balances[msg.sender];
26        require(amount > 0, "No balance");
27        
28        balances[msg.sender] = 0;
29        payable(msg.sender).transfer(amount);
30    }
31}

Summary

Medium Web3 topics:

  • DeFi: Lending, DEX, AMM, yield farming
  • Solidity Advanced: Modifiers, visibility, function types
  • Reentrancy: Attack vectors and prevention
  • Proxy Patterns: Upgradeable contracts, delegatecall
  • Oracles: Chainlink, price feeds, external data
  • Layer 2: Rollups, sidechains, bridges
  • EIP-1559: Gas optimization techniques
  • Signatures: ECDSA, on-chain verification, meta-transactions
  • Flash Loans: Uncollateralized borrowing, arbitrage
  • Security: Common vulnerabilities and fixes

These concepts are essential for building secure DeFi protocols.

Related Snippets