Solidity Cheatsheet
Security Pitfalls
Use this Solidity reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.
Reentrancy
The most exploited bug in DeFi. An external call hands control to the callee, which can re-enter your contract before state is updated.
// VULNERABLE function withdraw(uint256 amount) external { require(balances[msg.sender] >= amount); (bool ok, ) = msg.sender.call{value: amount}(""); // callee re-enters here require(ok); balances[msg.sender] -= amount; // too late — already drained } // FIXED: Checks-Effects-Interactions function withdraw(uint256 amount) external nonReentrant { require(balances[msg.sender] >= amount, "Insufficient"); balances[msg.sender] -= amount; // EFFECT before external call (bool ok, ) = payable(msg.sender).call{value: amount}(""); require(ok, "Transfer failed"); }
Reentrancy Guard
uint256 private _status = 1; modifier nonReentrant() { require(_status != 2, "Reentrant"); _status = 2; _; _status = 1; } // Transient storage version (0.8.24, EIP-1153 — cheaper) uint256 transient private _entered; modifier nonReentrant() { require(_entered == 0); _entered = 1; _; }
Read-Only Reentrancy
// View functions can also be re-entered. If another protocol reads your // "view" state mid-transaction, it can see inconsistent values. // Solution: use nonReentrant on state-changing functions, // or price oracles that are reentrancy-safe.
Integer Overflow / Underflow
Solidity 0.8+ reverts by default. For older contracts and libraries:
// Safe since 0.8.0 uint256 x = type(uint256).max; x += 1; // reverts — Panic(0x11) // If you use unchecked, overflow silently wraps: unchecked { x += 1; } // 0 — be certain this is intentional
Audit older contracts that use SafeMath — they were written for pre-0.8 and are safe. Do not remove SafeMath from pre-0.8 code you maintain.
Access Control
// VULNERABLE — anyone can call function setOwner(address newOwner) external { owner = newOwner; } // FIXED modifier onlyOwner() { require(msg.sender == owner, "Not owner"); _; } function setOwner(address newOwner) external onlyOwner { require(newOwner != address(0)); owner = newOwner; }
Role-Based Access Control (Pattern)
// OpenZeppelin AccessControl or custom: mapping(bytes32 => mapping(address => bool)) private _roles; bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); modifier onlyRole(bytes32 role) { require(_roles[role][msg.sender], "Missing role"); _; } function grantRole(bytes32 role, address account) external onlyRole(DEFAULT_ADMIN_ROLE) { _roles[role][account] = true; }
tx.origin Phishing
// VULNERABLE — tx.origin is the original EOA, not msg.sender function restricted() external { require(tx.origin == owner, "Not owner"); // A malicious contract can trick owner into calling it, // which then calls this function — tx.origin is still owner } // FIXED — use msg.sender function restricted() external { require(msg.sender == owner, "Not owner"); }
Front-Running
Transactions are public in the mempool before inclusion. Attackers can copy or sandwich your tx.
// Pattern: commit-reveal scheme mapping(address => bytes32) public commitments; function commit(bytes32 hash) external { commitments[msg.sender] = hash; } function reveal(uint256 value, bytes32 salt) external { require(commitments[msg.sender] == keccak256(abi.encode(value, salt))); // process value — it was hidden until now delete commitments[msg.sender]; } // Pattern: slippage protection function swap(uint256 amountIn, uint256 minAmountOut) external { uint256 out = _swap(amountIn); require(out >= minAmountOut, "Slippage too high"); }
Signature Replay
// VULNERABLE — same signature reusable across chains/contracts function execute(bytes32 hash, uint8 v, bytes32 r, bytes32 s) external { address signer = ecrecover(hash, v, r, s); require(signer == owner); _doAction(); } // FIXED — include chain ID, contract address, nonce (EIP-712) mapping(address => uint256) public nonces; bytes32 constant DOMAIN_SEPARATOR = keccak256(abi.encode( keccak256("EIP712Domain(string name,uint256 chainId,address verifyingContract)"), keccak256("MyContract"), block.chainid, address(this) )); function executeWithSig( address user, uint256 nonce, bytes32 action, uint8 v, bytes32 r, bytes32 s ) external { require(nonce == nonces[user]++, "Invalid nonce"); bytes32 digest = keccak256(abi.encodePacked( "\x19\x01", DOMAIN_SEPARATOR, keccak256(abi.encode(user, nonce, action)) )); require(ecrecover(digest, v, r, s) == user, "Bad signature"); _execute(action); }
Unchecked Low-Level Calls
// VULNERABLE — ignores failure addr.call{value: 1 ether}(""); // FIXED (bool ok, ) = addr.call{value: 1 ether}(""); require(ok, "Transfer failed");
Denial of Service (DoS)
// VULNERABLE — unbounded loop can hit block gas limit function payAll() external { for (uint i; i < recipients.length; ++i) { payable(recipients[i]).transfer(shares[i]); // can fail + reverts all } } // FIXED — pull payment pattern mapping(address => uint256) public owed; function claim() external { uint256 amount = owed[msg.sender]; owed[msg.sender] = 0; payable(msg.sender).transfer(amount); } // VULNERABLE — external call in loop, one failure blocks all // FIXED — use try/catch or pull pattern per recipient
Price Oracle Manipulation
// VULNERABLE — spot price from AMM (manipulable in same tx via flash loans) function getPrice() internal view returns (uint256) { (uint112 r0, uint112 r1,) = IUniswapV2Pair(pool).getReserves(); return uint256(r1) * 1e18 / uint256(r0); } // FIXED — use TWAP, Chainlink, or multi-block average function getTWAP() internal view returns (uint256) { // Use Uniswap V3 OracleLibrary.consult() or Chainlink AggregatorV3Interface }
Flash Loan Attacks
Flash loans enable instant borrowing of large amounts within one tx. Guard against:
- Oracle manipulation (use TWAPs)
- Governance attacks (snapshot voting power at previous block: block.number - 1)
- Single-block balance assumptions
delegatecall Risks
// delegatecall runs implementation code in THIS contract's storage // Storage layout MUST match between proxy and implementation // VULNERABLE — uninitialized proxy allows takeover contract Proxy { address public implementation; // slot 0 // if implementation has owner at slot 0 too: delegatecall can overwrite! } // FIXED — use EIP-1967 unstructured storage slots bytes32 constant IMPL_SLOT = bytes32(uint256(keccak256("eip1967.proxy.implementation")) - 1);
Griefing via selfdestruct / Forced Ether
Contracts can receive ether without receive or fallback via selfdestruct or coinbase rewards. Never assume address(this).balance == totalDeposited.
// VULNERABLE assumption require(address(this).balance == totalDeposited); // FIXED — track deposits separately uint256 public totalDeposited; function deposit() external payable { totalDeposited += msg.value; }
Short Address Attack
On-chain ABI decoding pads calldata — only matters when building raw calldata off-chain. Use typed ABI libraries (ethers.js, viem) always.
Rounding and Precision
// Division truncates in Solidity — rounding errors accumulate uint256 share = totalRewards * userBalance / totalSupply; // can round down // Round in favor of the protocol (never user) // Use WAD math (1e18 scaling) for percentages uint256 WAD = 1e18; uint256 fee = amount * feeBps / 10_000; // 30 BPS = 0.3% // Avoid division before multiplication // BAD: (a / b) * c — loses precision // GOOD: a * c / b — multiply first
Common Audit Checklist
| Check | Question |
|---|---|
| Reentrancy | Are all state updates before external calls? |
| Access control | Is every mutation guarded? Is admin a multisig? |
| Integer math | Are divisions ordered correctly? Can values exceed bounds? |
| Signature replay | Chain ID, contract address, nonce in every signed message? |
| Oracle | Is the price source manipulable in one tx? |
| DoS | Can a single bad actor block others? |
tx.origin | Never used for auth? |
| Low-level calls | Return value always checked? |
| Storage collision | Do proxy and implementation share layout? |
| Initialization | Are initialize() functions protected against re-init? |
| Ether accounting | Is balance checked correctly (accounting for forced sends)? |