Skip to main content

Bitfields

An EVM storage slot is 256 bits. Storing a single bool (1 bit) in a full slot wastes 255 bits and costs the same gas as storing a u256. Bitfields let you pack multiple small values into one slot.

Declaring a bitfield

bitfield ConfigFlags : u256 {
initialized: bool;
mode: u8;
retries: u8;
epoch: u16;
}

The : u256 specifies the backing type — a single 256-bit storage word. The fields are packed sequentially: initialized takes 1 bit, mode takes 8 bits, retries takes 8 bits, epoch takes 16 bits. Total: 33 bits out of 256.

Reading and writing fields

contract Config {
storage var flags: ConfigFlags;

pub fn configure(m: u8, r: u8, e: u16) {
let f: ConfigFlags = flags; // read the packed word
f.mode = m;
f.retries = r;
f.epoch = e;
f.initialized = true;
flags = f; // write the packed word back
}

pub fn getMode() -> u8 {
let f: ConfigFlags = flags;
return f.mode;
}
}

The pattern is: read the bitfield into a local, modify fields, write it back. Each field access compiles to shift-and-mask operations on the underlying word.

Gas savings

Without bitfields, storing four values requires four SSTORE operations (~80,000 gas). With a bitfield, it's one SSTORE (~20,000 gas) because all four values share a single slot.

The tradeoff: every field read/write involves shift-and-mask arithmetic (~10–20 gas), which is negligible compared to storage costs.

When to use bitfields

Use bitfields when:

  • You have multiple small values that are read/written together
  • Storage gas cost matters (it almost always does)
  • The values fit comfortably in 256 bits

Don't use bitfields when:

  • Values are large (multiple u256)
  • Values are updated independently by different functions (you'd always load/store the full word)
  • Readability matters more than gas optimization

The vault with bitfields

Adding packed configuration to the vault:

bitfield VaultConfig : u256 {
paused: bool;
frozen: bool;
tier: u8;
maxDeposit: u128;
}

contract Vault {
storage var config: VaultConfig;
storage var balances: map<address, u256>;

pub fn pause() {
let c: VaultConfig = config;
c.paused = true;
config = c;
}

pub fn isPaused() -> bool {
let c: VaultConfig = config;
return c.paused;
}

pub fn setTier(newTier: u8) {
let c: VaultConfig = config;
c.tier = newTier;
config = c;
}
}

The paused, frozen, tier, and maxDeposit fields all share one storage slot.

Further reading

  • Bitfield Types — full bitfield reference including explicit layout