🔐 Master Solidity: Must-Know Interview Questions for Ethereum Developers 🛠️ (Practical Only)
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:
transferreverts if the transfer fails, forwarding 2300 gas, which is sufficient for most basic operations but might fail if the recipient contract is complex.sendreturns 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
transferfor simple transactions. If interacting with a contract that may consume more gas, prefersendwith proper checks.
Pitfall:
- Avoid using
sendwithout 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
storageare persistent and cost more gas to access and modify. Arrays inmemoryare temporary and cheaper but not persistent.
Real-World Scenario:
- Use
memoryfor temporary calculations andstoragefor 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
memoryfor 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.encodeprovides a full ABI-compliant encoding with type information, whileabi.encodePackedproduces a more compact byte array, which can be used for hashing but may lead to collisions.
Real-World Scenario:
- Use
abi.encodefor encoding data for contract interactions andabi.encodePackedfor creating compact hashes.
Pitfall:
abi.encodePackedcan 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.