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

CheckQuestion
ReentrancyAre all state updates before external calls?
Access controlIs every mutation guarded? Is admin a multisig?
Integer mathAre divisions ordered correctly? Can values exceed bounds?
Signature replayChain ID, contract address, nonce in every signed message?
OracleIs the price source manipulable in one tx?
DoSCan a single bad actor block others?
tx.originNever used for auth?
Low-level callsReturn value always checked?
Storage collisionDo proxy and implementation share layout?
InitializationAre initialize() functions protected against re-init?
Ether accountingIs balance checked correctly (accounting for forced sends)?