learn ยท understand ยท why
whitehack flags where code lies about its own state. But a check without understanding is just a nag. Here is the why behind each one โ the real-world moment someone got hurt, and the lesson that makes you a more honest builder.
01 ยท silent-failure
catch { return 0 } ยท ?? 0 over a fetch
"I could not read this" becomes a confident wrong value. Zero and failed are indistinguishable.
A food delivery app checks your wallet balance. The database is down for two seconds. The code catches the error and returns 0. Your balance is $50. The app says $0. You can't order lunch. You stare at the screen thinking you're broke.
The system didn't crash. It lied. It told you a confident number when it should have said "I couldn't check." The user trusts the 0 because it looks like an answer. It's not โ it's a failure wearing a number's costume.
Developers write defensive code: "if something goes wrong, return a safe default." But 0 is not safe. 0 is a lie that looks like a number. The instinct to avoid crashes is right; the execution โ swallowing the error into a falsy value โ is wrong.
Throw, log, or return a visible error. Never let "could not read" and "the answer is zero" look the same. The user deserves to know the system couldn't check โ not a fake number.
Clear Standard #2 โ Visible failure. A failure degrades visibly. "I could not read this" must never quietly become a confident 0.
02 ยท cache-as-live
cached value returned with no freshness marker
"This is the truth right now" when it might be hours old.
A travel site caches flight prices. The cache is 4 hours old. You see "$320" and click book. The real price is $410 now. The page said nothing about when the price was checked โ it looked live. You trusted it. At checkout, the airline charges $410. You feel cheated.
The system didn't hack you. It served stale data as if it were fresh, and the difference between "cached" and "live" was invisible. The cache was fine. The lie was serving it without a timestamp.
Caching is good โ it makes systems fast. The problem isn't caching. The problem is serving cache as if it were live. Without a freshness marker, the caller can't tell the difference between "this was true 4 hours ago" and "this is true now." That difference matters.
Label freshness. Say asOf, cachedAt, fetchedAt. Let the caller decide whether to trust a stale value. The honesty isn't in avoiding cache โ it's in not pretending cache is live.
Clear Standard #4 โ Stated freshness. A cached value is not a live one; say how old it is.
03 ยท decision-without-why
score, fee, or flag rendered with no explanation
A decision is made about you, but you can't see why.
A lending platform shows your "trust score: 340." You're denied a loan. You ask why. There's no way to find out. The score is a black box โ it affects your life, and you have no way to inspect it.
This isn't just a UX problem. It's a justice problem. A decision made about a person that the person cannot inspect is a small act of tyranny. You don't need to agree with the score, but you deserve to know what went into it.
Systems make decisions all the time โ trust scores, fraud flags, fee tiers, risk ratings. The logic is often complex, proprietary, or machine-learned. The developer renders the result but doesn't render the reason. The decision reaches the user; the explanation doesn't.
Add a why-link, a methodology page, a tooltip โ anything that lets the affected person ask "why this number?" The decision can still be the same. But now it's inspectable.
Clear Standard #3 โ Inspectable decisions. A decision made about a person is inspectable by that person.
04 ยท float-money
parseFloat(price) ยท amount * 0.029
"This amount is exact" when floating-point arithmetic silently loses cents.
A cart totals your order: 19.99 + 29.99 + 9.99. You expect $59.97. But JavaScript gives 59.96999999999999. The display rounds it, so the customer sees $59.97. Internally, the system is off by a fraction of a cent. Over a million transactions, those fractions drift. Reconciliation fails. Auditors ask questions. The totals don't match.
Binary floating-point cannot represent 0.1 exactly. 0.1 + 0.2 is not 0.3. Every financial system that uses floats for money carries this lie. The amount looks right; it almost is right; and "almost" is a lie when you're handling money.
JavaScript has one number type: Number, which is a 64-bit float. It's the default. Using it for money feels natural โ it's what the language gives you. But floats were designed for scientific computing, not exact decimal arithmetic. The mismatch between "I want exact cents" and "the language gives me approximate floats" is where the lie lives.
Use integer minor units (cents, satoshi), BigInt, or a decimal library. Money is never a float. The fix is simple; the discipline to always use it is the hard part.
Clear Standard #1 โ Truth of state. The state the surface shows must match the state the system holds.
05 ยท stale-oracle
feed.latestRoundData() with no updatedAt check
"This is the current price" when the feed is hours old or frozen.
A DeFi lending protocol reads a Chainlink price feed. The oracle node goes down for maintenance. The feed stops updating. The protocol keeps reading the last price โ which is now hours old. The real market price drops 30%. The protocol still thinks the collateral is worth what it was hours ago.
Someone borrows against the stale price. When the feed resumes, the protocol discovers it's massively undercollateralized. Millions of dollars are gone. The oracle didn't lie โ the protocol lied by serving a stale oracle reading as if it were current.
Oracle calls return multiple values: the price, the round ID, the timestamp. Developers often destructure the price and drop the rest. The timestamp is the freshness โ without it, you can't tell whether the price is 5 seconds old or 5 hours old. Dropping the timestamp is the same as serving cache without a freshness marker, on-chain.
Always validate updatedAt / answeredInRound. If the feed is stale, revert or degrade visibly. Never serve a price without knowing when it was true.
Clear Standard #4 โ Stated freshness. A price from a moment ago is not the price now; say how old it is.
06 ยท unchecked-transfer
token.transfer(to, amount) โ bool result dropped
"The transfer happened" when the token returned false instead of reverting.
A protocol calls token.transfer(to, amount). The call doesn't revert. The protocol assumes success. But some ERC-20 tokens โ USDT is the famous one โ don't revert on failure. They return false. The protocol dropped the return value. The transfer failed silently. The tokens never moved. The protocol recorded "success."
The books don't balance. Someone is missing funds, and the system says everything went through. This is the blockchain version of silent failure โ on-chain, you can't catch exceptions, so dropping a bool is the same as swallowing an error.
The ERC-20 standard says transfer returns a bool. Most well-known tokens (USDC, DAI) revert on failure, so developers get used to not checking the return value. Then they integrate USDT โ which returns false instead โ and the assumption breaks. The lie isn't in the token; it's in the protocol that didn't check.
Check the return value (require(ok, "transfer failed")) or use OpenZeppelin's SafeERC20 wrapper. Never assume a transfer succeeded without verifying.
Clear Standard #2 โ Visible failure. A failed transfer must not look like a successful one.
07 ยท spot-price-as-fair
pair.getReserves() used as the fair price
"This is the fair market price" when it's a flash-loan-movable snapshot.
A protocol reads a DEX pool's reserves and computes: reserve0 / reserve1 = the price. It looks fair โ it's the pool's current state. But a flash loan borrows a massive amount, moves the reserves, changes the "price" to anything, and repays the loan in the same transaction.
The protocol reads the manipulated price and acts on it. The price was never "fair" โ it was a snapshot at a moment an attacker controlled. This is how flash loan attacks work. The protocol trusted a number that was only true for one block, and that block was controlled by someone who wanted the number to be wrong.
Reading pool reserves feels like reading the market price โ it's right there, it's a number, it's current. But "current" and "fair" are different things. A spot reading is a moment; a fair price is a range over time. Confusing the two lets an attacker choose the moment.
Use a time-weighted average price (TWAP) or an external oracle. Never use instantaneous reserves as the fair price. The spot price tells you what is; the TWAP tells you what was โ and "what was" is harder to manipulate.
Clear Standard #1 โ Truth of state. A manipulable snapshot is not a fair price; label what it actually is.
08 ยท silent-revert
require(x > 0) ยท revert() โ no reason string
"Your call was refused" with no explanation of why.
A user calls your contract. It reverts. The error says revert() โ nothing else. The user has no idea why. Was their input wrong? Was the contract paused? Did they lack permission? They can't tell. They can't fix what they can't see.
On-chain, a revert with no reason is a locked door with no sign. The contract made a decision โ to refuse the call โ and the caller can't inspect why. In regular software, you get a stack trace. On-chain, you get nothing โ unless the developer wrote a reason.
Writing require(x > 0) is shorter than require(x > 0, "amount must be positive"). The reason string costs a little gas. Developers optimize for brevity and gas, and the next person โ the user, the integrator, the auditor โ pays for it in confusion. The laziness is understandable; the consequence is a locked door.
Always include a reason string or named custom error. require(cond, "why") or revert NamedError(...). The gas cost is small; the debugging time saved is enormous.
Clear Standard #3 โ Inspectable decisions. A refusal the caller cannot understand is a decision made in the dark.
Every check catches the same thing in a different costume: the system claims something about its state that isn't true, and someone downstream trusts the claim.
The fix is always the same: say what you actually are. If you failed, say failed โ not zero. If you're cached, say cached โ not live. If you don't know why you decided, say that โ not "trust me." If you're stale, say stale โ not current.
Honesty is not perfection. You don't need to never fail. You need to never pretend you didn't fail. The lie isn't the bug โ the lie is presenting the bug as if it were the truth.
This is why whitehack exists. Not to find bugs โ to find lies. Not to make code perfect โ to make code honest.
๐ค