Solidity Cheatsheet

Security Pitfalls

Use this Solidity reference while you build software engineering projects, review code, or refresh the syntax you reach for most.

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)?