// ── Function Visibility ──
// public - callable by anyone (internal + external)
// private - only inside this contract
// internal - inside + inherited contracts
// external - only from outside (saves gas)
// ── State Mutability ──
// view - reads state, no modification
// pure - no state reads or writes
// payable - can receive ETH
contract Bank {
mapping(address => uint256) public balances;
// View function (free to call off-chain)
function getBalance(address addr) public view returns (uint256) {
return balances[addr];
}
// Payable function
function deposit() public payable {
require(msg.value > 0, "Must send ETH");
balances[msg.sender] += msg.value;
}
// Transfer ETH
function withdraw(uint256 amount) public {
require(balances[msg.sender] >= amount, "Insufficient balance");
balances[msg.sender] -= amount;
(bool success, ) = msg.sender.call{value: amount}("");
require(success, "Transfer failed");
}
// Pure function
function add(uint256 a, uint256 b) public pure returns (uint256) {
return a + b;
}
// Return multiple values
function getDetails() public view returns (
uint256 balance, address owner, bool active
) {
return (balances[msg.sender], owner, true);
}
}
// ── Modifiers ──
contract AccessControl {
address public owner;
mapping(address => bool) public admins;
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
modifier onlyAdmin() {
require(admins[msg.sender], "Not admin");
_;
}
modifier nonZeroAddress(address addr) {
require(addr != address(0), "Zero address");
_;
}
function setAdmin(address addr, bool status)
external onlyOwner nonZeroAddress(addr)
{
admins[addr] = status;
}
// Modifier with arguments
modifier validAmount(uint256 amount) {
require(amount > 0 && amount <= 1000 ether, "Invalid amount");
_;
}
}
// ── receive() & fallback() ──
contract Receiver {
event Received(address sender, uint256 amount);
// Called when ETH is sent with no data
receive() external payable {
emit Received(msg.sender, msg.value);
}
// Called when no function matches (no data, or no match)
fallback() external payable {
emit Received(msg.sender, msg.value);
}
}
🚫Never use tx.origin for authentication. Use msg.sender instead. tx.origin can be spoofed by a malicious contract. Always validate inputs with require() before modifying state.
// ── Interface ──
interface IERC20 {
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
// ── Abstract Contract ──
abstract contract ERC20Base {
string public name;
string public symbol;
uint8 public decimals;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
constructor(string memory _name, string memory _symbol, uint8 _decimals) {
name = _name;
symbol = _symbol;
decimals = _decimals;
}
function _mint(address to, uint256 amount) internal virtual {
totalSupply += amount;
balanceOf[to] += amount;
}
function _burn(address from, uint256 amount) internal virtual {
balanceOf[from] -= amount;
totalSupply -= amount;
}
}
// ── Concrete Implementation ──
contract MyToken is ERC20Base, IERC20 {
mapping(address => mapping(address => uint256)) public allowance;
address public owner;
modifier onlyOwner() {
require(msg.sender == owner);
_;
}
constructor() ERC20Base("MyToken", "MTK", 18) {
owner = msg.sender;
}
function transfer(address to, uint256 amount)
external override returns (bool)
{
require(balanceOf[msg.sender] >= amount, "Insufficient balance");
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount)
external override returns (bool)
{
allowance[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount)
external override returns (bool)
{
uint256 allowed = allowance[from][msg.sender];
require(allowed >= amount, "Allowance exceeded");
allowance[from][msg.sender] -= amount;
balanceOf[from] -= amount;
balanceOf[to] += amount;
emit Transfer(from, to, amount);
return true;
}
function mint(address to, uint256 amount) external onlyOwner {
_mint(to, amount);
}
}
// ── Multiple Inheritance + Overrides ──
contract A {
function foo() public pure virtual returns (string memory) {
return "A";
}
}
contract B is A {
function foo() public pure virtual override returns (string memory) {
return "B";
}
}
contract C is A, B {
// Must explicitly override both
function foo() public pure override(A, B) returns (string memory) {
return super.foo(); // calls B (rightmost)
}
}
💡Use interfaces for cross-contract communication and abstract contracts for shared base logic. Always mark overridden functions with override. Use virtual on functions that will be overridden.
// ── Foundry Test File ──
import "forge-std/Test.sol";
import "../src/MyToken.sol";
contract MyTokenTest is Test {
MyToken token;
// Run before each test
function setUp() public {
token = new MyToken();
token.mint(address(this), 1000 * 1e18);
}
function test_InitialSupply() public {
assertEq(token.totalSupply(), 1000 * 1e18);
}
function test_Transfer() public {
address bob = address(0x1);
token.transfer(bob, 100 * 1e18);
assertEq(token.balanceOf(bob), 100 * 1e18);
assertEq(token.balanceOf(address(this)), 900 * 1e18);
}
function testFail_TransferInsufficient() public {
// Expected to revert
token.transfer(address(0x1), 2000 * 1e18);
}
function test_RevertWhen_InsufficientBalance() public {
vm.expectRevert("Insufficient balance");
token.transfer(address(0x1), 2000 * 1e18);
}
function testFuzz_Transfer(uint256 amount) public {
vm.assume(amount <= token.balanceOf(address(this)));
address bob = address(0x1);
token.transfer(bob, amount);
assertEq(token.balanceOf(bob), amount);
}
// ── Foundry Cheats ──
function test_AccessControl() public {
vm.prank(address(0x999));
vm.expectRevert("Not owner");
token.mint(address(this), 100);
}
function test_EmitTransfer() public {
vm.expectEmit(true, true, false, true);
emit MyToken.Transfer(address(this), address(0x1), 100);
token.transfer(address(0x1), 100);
}
// ── Foundry CLI ──
// forge test # run all tests
// forge test --match-test test_Transfer # specific test
// forge test -vvv # verbose output
// forge build # compile
// forge coverage # test coverage
// forge snapshot # gas snapshot
}
💡Use Foundry for Solidity testing. It is faster than Hardhat, runs tests in native EVM (not JavaScript), has built-in fuzzing, and excellent cheatcodes (vm.prank, vm.expectRevert, vm.assume).
Q: What is the difference between storage, memory, and calldata?Storage is persistent (blockchain), expensive to read/write. Memory is temporary (function lifetime), cheaper. Calldata is read-only function parameters, cheapest. Use calldata for external function parameters that are only read.
Q: What is reentrancy and how do you prevent it?Reentrancy occurs when an external call allows the caller to re-enter the contract before state is updated. Prevention: (1) Checks-Effects-Interactions pattern, (2) ReentrancyGuard modifier (mutex), (3) Pull-over-push pattern. The famous DAO hack was a reentrancy exploit.
Q: Explain the ERC-20 standard.ERC-20 defines a fungible token interface: totalSupply(), balanceOf(), transfer(), approve(), transferFrom(), allowance(). Events: Transfer, Approval. Used for ICOs, DeFi tokens, stablecoins. ERC-721 is for NFTs (non-fungible tokens).
Q: What gas optimizations do you know?Key optimizations: (1) use calldata instead of memory, (2) use uint256 (smaller types may cost more due to padding), (3) cache storage in memory, (4) short-circuit require() checks, (5) use unchecked{} for guaranteed safe arithmetic, (6) batch operations, (7) minimize storage writes.
Q: What is the difference between call, delegatecall, and staticcall?call() executes code in the context of the CALLED contract. delegatecall() executes code in the context of the CALLING contract (used for proxies). staticcall() is a read-only call (reverts if state changes). Always check the return value of call().
Q: How do upgradeable contracts work?Proxy pattern: the proxy contract stores data and delegates execution to an implementation contract via delegatecall(). When upgrading, the admin changes the implementation address. Common patterns: Transparent Proxy, UUPS (Universal Upgradeable Proxy Standard), Diamond (EIP-2535).
💡Top Solidity interview topics: data locations, reentrancy prevention, access control, ERC-20/721 standards, gas optimization, proxy/upgradeable patterns, delegatecall vs call, error handling, events, and Foundry testing.