🌱 Beginner ⏱️ 30 min

Variables & Data Types

Solidity's type system is the first place most auditors look for bugs. Integer overflow, precision loss, timestamp manipulation, and storage collision vulnerabilities all originate in how data types behave. This lesson covers every important type with security implications for each.

Value Types — The Complete Reference

Value types are stored directly (not as references). When assigned to a new variable, they are copied.

TypeSizeRange / ValuesDefaultSecurity Note
bool1 bit (stored as 1 byte)true / falsefalseCan be accidentally reset by bad initialization
uint88 bits0 to 2550Overflows at 255 in unchecked blocks
uint1616 bits0 to 65,5350Common in Uniswap tick math
uint3232 bits0 to 4,294,967,2950Timestamps fit in uint32 until year 2106
uint6464 bits0 to 1.8 * 10^190Often used for timestamps, amounts
uint128128 bits0 to 3.4 * 10^380Uniswap v3 uses for liquidity
uint256256 bits0 to 1.15 * 10^770Default uint — use this unless you have reason not to
int256256 bits-5.8*10^76 to 5.8*10^760type(int256).min negation can overflow!
address20 bytesEthereum addressaddress(0)address(0) check is critical for access control
bytes3232 bytesFixed byte array0x00...00Commonly used for hashes, roles
enumuint8 internallyNamed valuesFirst valueCasting invalid uint to enum = undefined behavior

Integers in Depth — Sizes, Overflow, and Safety

🔢 Integer Types — Sizes and Arithmetic
// All integer types and their boundaries uint8 maxU8 = 255; // 2^8 - 1 uint16 maxU16 = 65535; // 2^16 - 1 uint256 maxU256 = type(uint256).max; // 2^256 - 1 = 115792089...71615 int256 maxI256 = type(int256).max; // 2^255 - 1 int256 minI256 = type(int256).min; // -2^255 // DANGER: type(int256).min negation overflows! int256 danger = -type(int256).min; // REVERTS in 0.8.x! Still = min in unchecked // Retrieving min/max at runtime: function getMaxUint() external pure returns (uint256) { return type(uint256).max; } // Wrapping arithmetic (useful when you want overflow behavior): unchecked { uint256 wrapped = type(uint256).max + 1; // = 0 (wraps around) }

Reference Types — Arrays, Structs, Mappings

📂 Reference Types in Solidity
contract ReferenceTypes { // FIXED-SIZE ARRAY: size known at compile time uint256[5] public fixedArray; // DYNAMIC ARRAY: size can change address[] public users; // STRUCT: group related data struct User { address wallet; uint256 balance; uint64 lastActive; bool isActive; } mapping(address => User) public userProfiles; // MAPPING: key-value store (cannot iterate!) mapping(address => uint256) public balances; // NESTED MAPPING: e.g. allowances[owner][spender] = amount mapping(address => mapping(address => uint256)) public allowances; function arrayOps() external { users.push(msg.sender); // add to end users.pop(); // remove last element uint256 len = users.length; // get length delete users[0]; // zeroes element, does NOT shift others } }
⚠️ Mappings Cannot Be Iterated or Returned

Mappings do not store their keys. You cannot loop over all entries in a mapping, and you cannot return a mapping from a function. If you need to iterate, maintain a separate array of keys alongside the mapping. This is a common source of confusion for beginners.

Storage Locations: storage, memory, calldata

LocationWhereLifetimeMutable?CostWhen to Use
storageOn-chain, contract stateForeverYesVery expensive (20K write)State variables, persistent data
memoryIn-memory, temporaryFunction call durationYesCheap (3 gas/word)Local variables, return values, complex objects
calldataInput data bufferFunction call durationNo (read-only)CheapestExternal function params (saves gas vs memory)
💾 Storage Location Examples
contract StorageLocations { uint256[] public myArray; // storage (state variable) // calldata: cheapest for external function inputs (read-only) function processData(uint256[] calldata input) external pure returns (uint256) { // input is calldata — cannot modify input[0] = 5 (compile error) return input[0]; } // memory: mutable temporary copy function processDataMutable(uint256[] memory input) external pure returns (uint256) { input[0] = 42; // OK — memory is mutable return input[0]; } // CRITICAL: Storage pointer vs memory copy function storageRef() external { // storage reference — modifies the actual state variable uint256[] storage ref = myArray; ref.push(99); // myArray is now [99] // memory copy — does NOT modify state uint256[] memory copy = myArray; copy[0] = 123; // myArray[0] is STILL 99 } }

Type Casting — Safe and Unsafe Conversions

🔄 Type Casting — Explicit and Implicit
// Implicit conversion (safe — widening) uint8 small = 100; uint256 big = small; // OK: 100 fits in uint256 // Explicit conversion — DANGEROUS if truncating uint256 large = 300; uint8 truncated = uint8(large); // 300 % 256 = 44 — silent data loss! // In Solidity 0.8.x: explicit truncation is allowed but silent // You MUST check bounds yourself before downcasting function safeCast(uint256 value) internal pure returns (uint8) { require(value <= type(uint8).max, "Value too large"); return uint8(value); } // Or use OpenZeppelin SafeCast library: import "@openzeppelin/contracts/utils/math/SafeCast.sol"; uint8 safe = SafeCast.toUint8(large); // Reverts if > 255 // address conversions: address addr = address(0x1234); address payable payableAddr = payable(addr); // Required to send ETH uint256 asUint = uint256(uint160(addr)); // address → uint: must go via uint160

Integer Overflow/Underflow — History and the 0.8.x Fix

🚨 Historical Vulnerability: Integer Overflow

Before Solidity 0.8.0 (released December 2020), integer arithmetic silently wrapped around on overflow/underflow. This caused some of the biggest hacks in early DeFi history. The BeautyChain token lost hundreds of millions because a multiplication overflow allowed minting unlimited tokens.

💥 Integer Overflow PoC — Pre-0.8.x Vulnerability
// VULNERABLE (Solidity < 0.8.0) contract VulnerableToken { mapping(address => uint256) public balances; function transfer(address to, uint256 amount) external { // UNDERFLOW: if balances[msg.sender] = 0 and amount = 1: // 0 - 1 = 2^256 - 1 (wraps to max uint256!) // The require passes because 2^256-1 >= 1 require(balances[msg.sender] - amount >= 0); // BUG: always true for uint! balances[msg.sender] -= amount; // Attacker now has massive balance balances[to] += amount; } } // CORRECT fix for < 0.8.0: Use SafeMath (OpenZeppelin) using SafeMath for uint256; balances[msg.sender] = balances[msg.sender].sub(amount); // Reverts on underflow // Solidity 0.8.0+: Arithmetic is CHECKED BY DEFAULT pragma solidity ^0.8.0; // Now: 0 - 1 reverts with Panic(0x11) // SafeMath is no longer needed for simple arithmetic // When you WANT wrap-around behavior (e.g., in gas-optimized loops): unchecked { i++; // Saves gas by skipping overflow check — safe if you know i < max }

Special Global Variables — msg, block, tx

🌍 Global Variables Reference
// MSG — Information about the current call msg.sender; // address: who called this function (immediate caller) msg.value; // uint256: ETH sent (in wei) with this call msg.data; // bytes: full calldata (ABI-encoded function call) msg.sig; // bytes4: function selector (first 4 bytes of msg.data) // BLOCK — Information about the current block block.timestamp; // uint256: current block time (Unix seconds) — CAN BE MANIPULATED block.number; // uint256: current block height block.basefee; // uint256: current block's base fee (EIP-1559) block.difficulty; // uint256: PoW difficulty (now PREVRANDAO in PoS) block.chainid; // uint256: chain ID (1 = mainnet) block.coinbase; // address payable: current block's validator address block.gaslimit; // uint256: block gas limit // TX — Information about the originating transaction tx.origin; // address: EOA that initiated the entire tx chain (NEVER use for auth!) tx.gasprice; // uint256: gas price of the transaction // OTHERS gasleft(); // uint256: remaining gas in current execution address(this).balance; // uint256: ETH balance of this contract blockhash(n); // bytes32: hash of block n (only last 256 blocks)

Constants and Immutables — Gas Savings and Security

💰 constant vs immutable
contract GasOptimized { // CONSTANT: value set at compile time, inlined in bytecode // No SLOAD needed — free to read! // Must be a literal value or constant expression uint256 public constant MAX_SUPPLY = 1_000_000 * 10**18; bytes32 public constant ADMIN_ROLE = keccak256("ADMIN_ROLE"); // IMMUTABLE: set once in constructor, then inlined in bytecode // No SLOAD needed — free to read! // Value determined at deploy time (not compile time) address public immutable owner; uint256 public immutable deploymentTime; constructor() { owner = msg.sender; // Set once in constructor deploymentTime = block.timestamp; } // REGULAR STATE VARIABLE: stored in contract storage slot // Costs 2,100 gas (cold) or 100 gas (warm) to read uint256 public mutableValue; // expensive to read }
Featureconstantimmutablestate variable
When value setCompile timeConstructorAnytime
Can be changed after deploy?NoNoYes
Gas to readFree (inlined)Free (inlined)100-2,100 gas (SLOAD)
Can use runtime values?NoYes (constructor args)Yes

Common Vulnerabilities — Real Examples

1. Precision Loss from Integer Division

💥 Precision Loss Exploit
// Solidity uses INTEGER division — always rounds DOWN uint256 a = 1; uint256 b = 3; uint256 result = a / b; // = 0, not 0.333... // Real vulnerability: fee calculation rounding function calculateFee(uint256 amount) public pure returns (uint256) { // BUG: If amount < 100, fee = 0 (rounding to zero) // Attacker can make many small transactions with zero fee return amount / 100; // 1% fee — but 50 / 100 = 0 } // FIX: Multiply before dividing to preserve precision // Use fixed-point math with a scaling factor (e.g., 1e18 = WAD) uint256 constant WAD = 1e18; function calculateFeeFixed(uint256 amount) public pure returns (uint256) { // Multiply first, scale down last return (amount * 1 * WAD / 100) / WAD; // Or simply: require amount has enough precision }

2. Timestamp Dependence Vulnerability

🚨 block.timestamp Can Be Manipulated

Validators can adjust block.timestamp by up to ~15 seconds from the true time. While small, this is enough to manipulate any smart contract that uses timestamps for logic like "is this auction over?", "can this reward be claimed?", or — most dangerously — as a source of randomness.

💥 Timestamp Manipulation Vulnerability
// VULNERABLE: Using timestamp as pseudo-random number contract BadLottery { function pickWinner() external { // NEVER do this: timestamp is not random // A validator proposing this block CONTROLS the timestamp // They can choose a timestamp where they win! uint256 winner = block.timestamp % participants.length; participants[winner].transfer(address(this).balance); } } // Also dangerous: block.difficulty (now PREVRANDAO in PoS) // PREVRANDAO is better than timestamp but still manipulable by validators // SECURE: Use Chainlink VRF for true randomness import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol"; // Timestamp IS acceptable for: // - Timelocks (15-second manipulation is fine for 24h locks) // - Cooldown periods where precision doesn't matter // - Display purposes // Timestamp is NOT acceptable for: // - Randomness (any amount of control is exploitable) // - High-value financial cutoffs (exact-second matters)
✏️ Try It — Safe Arithmetic vs Overflow Demonstration
// Deploy this in Remix IDE to see overflow behavior pragma solidity ^0.8.20; contract OverflowDemo { // This will REVERT in 0.8.x (checked arithmetic) function checkedOverflow() external pure returns (uint8) { uint8 x = 255; return x + 1; // REVERTS with Panic(0x11) } // This will WRAP AROUND (unchecked block) function uncheckedOverflow() external pure returns (uint8) { uint8 x = 255; unchecked { return x + 1; // Returns 0 (wraps around) } } // Precision loss demonstration function precisionLoss() external pure returns (uint256 bad, uint256 good) { uint256 amount = 7; bad = amount / 3 * 3; // 7/3=2, 2*3=6 — WRONG (lost 1) good = amount * 3 / 3; // 7*3=21, 21/3=7 — CORRECT } }

Common Mistakes Section

⚠️ Mistake #1: Dividing Before Multiplying

Always multiply before dividing to avoid precision loss. a / b * c loses precision; a * c / b is much more accurate. This is the basis of Solidity's fixed-point math conventions (WAD = 1e18, RAY = 1e27).

⚠️ Mistake #2: Using uint Instead of a Smaller Type to Save Gas

Counterintuitively, using uint8 or uint16 in state variables does NOT save gas over uint256 for isolated storage slots. The EVM operates in 256-bit words. Small types only save gas when packed together in a struct. Outside a struct, use uint256.

⚠️ Mistake #3: Using block.timestamp for Randomness

Never use block.timestamp, block.number, or blockhash() as sources of randomness. All of these are visible before block inclusion and some are directly controlled by validators. Use Chainlink VRF for secure randomness.

Summary / Key Takeaways

Type / ConceptKey FactSecurity Relevance
uint overflow (pre-0.8)Silently wrapped aroundBalance → max uint (theft)
Solidity 0.8.xChecked arithmetic by defaultOverflow reverts (safe)
unchecked {}Skips overflow checksOnly use when mathematically provable safe
Integer divisionAlways rounds down (floors)Fee rounding = zero fee exploit
Storage locationstorage / memory / calldataWrong location = data not persisted
block.timestampValidator-adjustable by ~15sNever use for randomness
constant / immutableInlined in bytecodeFree to read, saves 2,100 gas
Type downcastingSilently truncatesSafeCast needed for safety