hidden inputs, hidden effects
Impure Functions
Randomness, global reads, and arguments mutated in place make a call's behavior depend on — and change — the world around it.
A function whose result depends on more than its arguments cannot be trusted or tested.
01in the wild
In the wild
Nondeterministic Randomness
A function that reaches for a random source gives a different answer on every run.
example.py
import random
# IMPURE: nondeterministic, untestable
def roll():
return random.randint(1, 6)
# PURE: inject the source of entropy
def roll(rng):
return rng.randint(1, 6)
roll(random.Random(42)) # repeatable in testsInjecting rng makes randomness reproducible: seed it in tests, use a real source in production.
// observed
impure: roll() differs each run pure: roll(Random(42)) is always the same
example.sol
// ENTROPY in the worst place: a smart contract.
// SMELL: block values are attacker-influenced "randomness"
function badRandom() public view returns (uint) {
return uint(keccak256(abi.encode(block.timestamp))) % 100;
}
// RIGHT: use a verifiable random function (Chainlink VRF)
// instead of block.timestamp / blockhash.On-chain, time and entropy are public and miner-influenced. Money bugs here are irreversible — fintech at scale means no backsies.
// observed
bad: miners can steer block.timestamp to win right: VRF gives entropy nobody can predict
Mutating Your Arguments
A function that edits the value passed in changes the caller's world behind its back.
example.js
// SMELL: sort() mutates the array it is called on
function topThree(scores) {
return scores.sort((a, b) => b - a).slice(0, 3);
}
const data = [3, 1, 2];
topThree(data);
data; // [3, 2, 1] <-- caller's array was reordered!
// RIGHT: copy before you transform
const topThree = (s) => [...s].sort((a, b) => b - a).slice(0, 3);Array.sort sorts in place and returns the same array. Spread into a fresh array so the caller's data is untouched.
// observed
mutating: data becomes [3, 2, 1] pure: data stays [3, 1, 2]
example.py
# SMELL: the helper mutates the list it was handed
def normalize(items):
items.sort()
return items
data = [3, 1, 2]
normalize(data)
data # [1, 2, 3] <-- surprise side effect
# RIGHT: return a new, sorted list
def normalize(items):
return sorted(items)list.sort() mutates in place; sorted() returns a new list and leaves the argument alone.
// observed
mutating: data becomes [1, 2, 3] pure: data stays [3, 1, 2]
02cross-pollination
Where this compounds
Nondeterminism
- Stale-Cache Heisenbug × Cache Invalidation
- Pooled-Resource Heisenbug × Resource Leaks & Improper Shutdown
- Hash-Seed Serialization Divergence × Cache Invalidation
Runtime Errors
- Integration Crash on Unexpected Input × Lack of Input Validation
- Config / Environment Drift Crash × Version & Library Mismanagement
03weakness catalog
Mapped weaknesses (CWE)
On its own, this defect is catalogued by MITRE as one or more of these weaknesses. The exploitable vulnerability usually appears only when it chains or combines with another.