Solidity Cheatsheet

Mappings and Structs

Use this Solidity reference while you build software engineering projects, review code for technical interview prep, or polish examples for a software engineer resume.

Mappings

// Basic syntax
mapping(KeyType => ValueType) visibility name;

mapping(address => uint256)  public balances;
mapping(bytes32  => bool)    public seen;
mapping(uint256  => address) public idToOwner;

Key Types

Valid mapping keys: any value type (address, uint, bytes32, bool, enums, user-defined value types). Not allowed: reference types (bytes, string, struct, arrays).

Nested Mappings

mapping(address => mapping(address => uint256)) public allowance;
// ERC-20 pattern: allowance[owner][spender]

mapping(address => mapping(uint256 => bool)) public hasVotedOnProposal;

Reading and Writing

// Read — unset keys return the zero value (no key-not-found error)
uint256 b = balances[msg.sender]; // 0 if never set

// Write
balances[msg.sender] = 100;
balances[msg.sender] += 50;

// Delete — resets to zero value
delete balances[msg.sender];

// Nested write
allowance[owner][spender] = amount;

Limitations

  • Cannot iterate (no internal list of keys — they are hashed via keccak256).
  • Cannot copy to/from memory.
  • Cannot get .length.
  • Cannot be used as function return types directly (unless wrapped in a struct or ABI v2).

Enumerable Pattern

// Track keys manually alongside the mapping
mapping(address => uint256) public stakes;
address[] public stakers;           // enumerable list
mapping(address => bool) public isStaker; // existence check O(1)

function stake(uint256 amount) external {
    if (!isStaker[msg.sender]) {
        stakers.push(msg.sender);
        isStaker[msg.sender] = true;
    }
    stakes[msg.sender] += amount;
}

Mapping Storage Slot Calculation

The storage slot for mapping[key] is keccak256(abi.encode(key, slot)) where slot is the mapping's declared slot number. Useful for Foundry stdStorage / off-chain tooling.

Structs

struct Order {
    address buyer;
    uint128 price;    // pack alongside tokenId in one slot
    uint128 tokenId;
    uint256 quantity; // separate slot
    bool    filled;   // packs with future fields
    OrderStatus status;
}

enum OrderStatus { Open, Filled, Cancelled }

Declaring and Initializing

// Named fields (preferred — order-independent)
Order memory o = Order({
    buyer:    msg.sender,
    price:    1 ether,
    tokenId:  42,
    quantity: 1,
    filled:   false,
    status:   OrderStatus.Open
});

// Positional (must match declaration order exactly)
Order memory o2 = Order(msg.sender, 1 ether, 42, 1, false, OrderStatus.Open);

Storage vs Memory Pointers

mapping(uint256 => Order) public orders;

function fillOrder(uint256 id) external {
    // Storage pointer — mutations write directly to storage
    Order storage order = orders[id];
    order.filled = true;       // 1 SSTORE
    order.status = OrderStatus.Filled;

    // Memory copy — mutations do NOT affect storage
    Order memory snapshot = orders[id]; // copies entire struct
    snapshot.price = 0; // only changes local copy
}

Structs in Mappings and Arrays

mapping(uint256 => Order) public orders;
Order[] public orderBook;

// Add to array
orderBook.push(Order({ buyer: msg.sender, price: 1 ether, /* ... */ }));

// Modify via storage ref
orderBook[0].filled = true;

// Read into memory (cheap for view functions)
Order memory o = orderBook[0];

Nested Structs

struct Address {
    string street;
    string city;
}

struct User {
    string  name;
    Address addr;
    uint256[] tokenIds; // dynamic array inside struct
}

mapping(address => User) public users;

function setCity(string calldata city) external {
    users[msg.sender].addr.city = city;
}

Returning Structs

// ABI v2 (default since 0.8) allows structs as return types
function getOrder(uint256 id) external view returns (Order memory) {
    return orders[id];
}

// Return multiple structs
function getOrderAndUser(uint256 id)
    external
    view
    returns (Order memory order, User memory user)
{
    order = orders[id];
    user  = users[order.buyer];
}

Structs as Mapping Values (Pattern)

struct Stake {
    uint256 amount;
    uint40  since;    // packed with amount in one slot? no — 256 is full slot
    // use uint216 + uint40 to pack
}

// Better packing
struct StakePacked {
    uint216 amount; // bytes 0-26
    uint40  since;  // bytes 27-31 → same slot!
}

Storage Packing

The EVM stores data in 32-byte slots. Fields are packed right-to-left within a slot if they fit.

// BAD — 3 slots (each uint256 gets its own slot)
struct Wasteful {
    uint256 a;
    uint8   b;
    uint256 c;
}

// GOOD — 2 slots (b and d share a slot)
struct Efficient {
    uint256 a; // slot 0
    uint8   b; // slot 1, bytes 0
    uint8   c; // slot 1, bytes 1
    uint240 d; // slot 1, bytes 2-31 (240/8 = 30 bytes fits)
    uint256 e; // slot 2
}

Rules: - Order fields from small to large within a "group" to pack tightly. - A uint256 always starts a new slot. - Dynamic types (string, bytes, arrays) always occupy their own slot(s). - Mappings occupy one slot (the actual data is elsewhere via hash). - bool = 1 byte, packs with adjacent types.

delete Semantics

// Resets to zero value
delete balances[msg.sender];  // uint → 0
delete orders[id];             // struct → all fields zeroed
delete orderBook;              // array → length 0 (expensive if long)

// Does NOT free mapping keys (mappings can't be fully deleted)
delete allowance[owner];       // only outer key is zeroed; inner slots remain

Hashing Mappings and Structs (EIP-712)

bytes32 constant ORDER_TYPEHASH = keccak256(
    "Order(address buyer,uint128 price,uint128 tokenId,uint256 quantity)"
);

function hashOrder(Order memory o) internal pure returns (bytes32) {
    return keccak256(abi.encode(
        ORDER_TYPEHASH,
        o.buyer,
        o.price,
        o.tokenId,
        o.quantity
    ));
}