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 tests
Injecting 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
Runtime Errors
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.