Skip to main content

Command Palette

Search for a command to run...

🔐 Master Solidity: Must-Know Interview Questions for Ethereum Developers 🛠️ (Practical Only)

Published
6 min read
M

Security Researcher for Web3 and Dark Web Bug hunter Ethical Hacker

Introduction

In the dynamic realm of Ethereum development, Solidity expertise is paramount. Interviews for Solidity developers often include practical coding challenges to assess a candidate’s ability to solve real-world problems. This article provides a comprehensive guide to essential interview questions, complete with sample code in Solidity 0.8.18, real-world scenarios, common pitfalls, and best practices.

Easy Questions

1. 🔄 Difference Between transfer and send

Question: Demonstrate the difference between transfer and send functions in Solidity.

pragma solidity ^0.8.18;

contract TransferSendDemo {
    address payable public recipient = payable(address(0x123));

    function transferFunds() public payable {
        recipient.transfer(msg.value);
    }

    function sendFunds() public payable {
        bool sent = recipient.send(msg.value);
        require(sent, "Failed to send Ether");
    }
}

Explanation:

  • transfer reverts if the transfer fails, forwarding 2300 gas, which is sufficient for most basic operations but might fail if the recipient contract is complex.

  • send returns a boolean indicating success or failure, also forwarding 2300 gas, and does not revert. It’s less safe but can be used for conditional logic.

Real-World Scenario:

  • Use transfer for simple transactions. If interacting with a contract that may consume more gas, prefer send with proper checks.

Pitfall:

  • Avoid using send without handling the failure case properly, as it can silently fail.

2. 🌀 Gas-Efficient for Loop

Question: Write a gas-efficient for loop in Solidity.

pragma solidity ^0.8.18;

contract EfficientLoop {
    uint256[] public numbers;

    function addNumbers(uint256 _count) public {
        for (uint256 i = 0; i < _count; i++) {
            numbers.push(i);
        }
    }
}

Explanation:

  • Ensure that loops are optimized and avoid excessive iterations that could lead to out-of-gas errors. Using smaller loops and batching operations can save gas.

Real-World Scenario:

  • For contract functions that need to iterate through large data sets, consider off-chain processing or optimizing loops to avoid high gas costs.

Pitfall:

  • Large loops can lead to high gas costs and failed transactions. Always test with varying input sizes.

3. 🔍 Storage Collision in Proxy Contract

Question: Explain a storage collision in a proxy contract and demonstrate with an example.

pragma solidity ^0.8.18;

contract Proxy {
    address public implementation;

    function setImplementation(address _impl) public {
        implementation = _impl;
    }

    fallback() external payable {
        (bool success, ) = implementation.delegatecall(msg.data);
        require(success, "Delegatecall failed");
    }
}

contract ImplementationV1 {
    uint256 public value;
}

contract ImplementationV2 {
    uint256 public newValue; // Potential storage collision
}

Explanation:

  • Storage collisions occur when different implementations have overlapping storage layouts. Ensure that upgradeable contracts manage storage slots carefully.

Real-World Scenario:

  • Use a clear and consistent storage layout when designing upgradeable contracts. Consider using libraries to avoid collisions.

Pitfall:

  • Overwriting storage slots accidentally can corrupt contract state. Use tools like OpenZeppelin’s upgradeable contracts to mitigate risks.

Medium Questions

4. 📊 Storage vs. Memory in Array Handling

Question: How does the storage slot usage differ for arrays in storage vs. memory?

pragma solidity ^0.8.18;

contract ArrayDemo {
    uint64[] public storageArray = [1, 2, 3, 4, 5];

    function memoryArray() public pure returns (uint64[] memory) {
        uint64[] memory tempArray = new uint64[](5);
        tempArray[0] = 1;
        tempArray[1] = 2;
        tempArray[2] = 3;
        tempArray[3] = 4;
        tempArray[4] = 5;
        return tempArray;
    }
}

Explanation:

  • Arrays in storage are persistent and cost more gas to access and modify. Arrays in memory are temporary and cheaper but not persistent.

Real-World Scenario:

  • Use memory for temporary calculations and storage for data you need to persist. Be mindful of gas costs when choosing between them.

Pitfall:

  • Mismanaging storage and memory can lead to high gas costs and inefficiencies. Always use memory for temporary operations to save gas.

5. 🛠️ abi.encode vs. abi.encodePacked

Question: Show the difference between abi.encode and abi.encodePacked.

pragma solidity ^0.8.18;

contract EncodingDemo {
    function encodeData(uint256 num, string memory str) public pure returns (bytes memory, bytes memory) {
        bytes memory encoded = abi.encode(num, str);
        bytes memory packed = abi.encodePacked(num, str);
        return (encoded, packed);
    }
}

Explanation:

  • abi.encode provides a full ABI-compliant encoding with type information, while abi.encodePacked produces a more compact byte array, which can be used for hashing but may lead to collisions.

Real-World Scenario:

  • Use abi.encode for encoding data for contract interactions and abi.encodePacked for creating compact hashes.

Pitfall:

  • abi.encodePacked can lead to hash collisions if not used carefully, especially when concatenating multiple variables.

6. 🔄 Inflation Attack in ERC4626

Question: What is an inflation attack in ERC4626 and how can it be demonstrated?

pragma solidity ^0.8.18;

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

    function deposit(uint256 amount) public {
        // Example vulnerable implementation
        balances[msg.sender] += amount;
        totalSupply += amount;
    }
}

Explanation:

  • An inflation attack in ERC4626 can occur if an attacker exploits vulnerabilities in token issuance or share calculations, potentially minting excessive tokens.

Real-World Scenario:

  • Always audit the share calculation logic and consider edge cases that might lead to unintended token inflation.

Pitfall:

  • Ensure accurate calculations and consider implementing safeguards to prevent unauthorized token creation.

Hard Questions

7. ⚙️ Custom Errors vs. require with Error Strings

Question: Compare how custom errors and require with error strings are encoded at the EVM level.

pragma solidity ^0.8.18;

contract ErrorDemo {
    error CustomError(string message);

    function requireError(bool condition) public pure {
        require(condition, "Error occurred");
    }

    function customErrorFunction(bool condition) public pure {
        if (!condition) {
            revert CustomError("Custom error occurred");
        }
    }
}

Explanation:

  • Custom errors are more gas-efficient as they use less space in the transaction data compared to strings in require. Custom errors can save gas when errors are frequent or the error message is large.

Real-World Scenario:

  • Use custom errors for error handling in contracts that interact frequently or handle large data, to reduce transaction costs.

Pitfall:

  • Custom errors require careful handling in client-side code to properly decode and display error messages.

8. 🧩 Function Selector Clash in Proxy

Question: What is a function selector clash in a proxy and how does it happen?

pragma solidity ^0.8.18;

contract Proxy {
    address public implementation;

    function setImplementation(address _impl) public {
        implementation = _impl;
    }

    fallback() external payable {
        (bool success, ) = implementation.delegatecall(msg.data);
        require(success, "Delegatecall failed");
    }
}

contract ClashExample {
    function clash(uint256 x) public pure returns (uint256) {
        return x * 2;
    }

    function clash(uint256 x, uint256 y) public pure returns (uint256) {
        return x + y;
    }
}

Explanation:

  • Function selector clashes occur when different functions have the same selector, which can lead to incorrect function execution. Avoid using the same function signature in different implementations.

Real-World Scenario:

  • Implement versioning or unique function selectors to prevent clashes in upgradeable contracts.

Pitfall:

  • Test for selector clashes during contract upgrades to avoid unexpected behaviors.

9. 📜 Beacon in Proxy Context

Question: What is a beacon in the context of proxies and how is it used?

pragma solidity ^0.8.18;

contract Beacon {
    address public implementation;

    function setImplementation(address _impl) public {
        implementation = _impl;
    }
}

contract Proxy {
    Beacon public beacon;

    function setBeacon(address _beacon) public {
        beacon = Beacon(_beacon);
    }

    fallback() external payable {
        address impl = beacon.implementation();
        (bool success, ) = impl.delegatecall(msg.data);
        require(success, "Delegatecall failed");
    }
}

Explanation:

  • A beacon contract centralizes the address of the current implementation, allowing multiple proxies to use a single beacon to resolve their implementations.

Real-World Scenario:

  • Use beacons in upgradeable proxy patterns to efficiently manage contract upgrades across multiple proxies.

Pitfall:

  • Ensure the beacon contract is secure and properly managed to avoid vulnerabilities.

Conclusion

Mastering Solidity goes beyond theoretical knowledge; it requires practical skills to handle real-world scenarios effectively. The questions and examples provided in this article cover essential practical aspects of Solidity development, from handling gas-efficient loops to managing proxy contracts and custom errors. Understanding these concepts, recognizing common pitfalls, and applying best practices will prepare you for success in Ethereum development interviews and real-world applications.

More from this blog

Testing Blockchain Technology

11 posts