juan23z

You built something. Tests pass. You're days from mainnet. Before you either skip security entirely (please don't) or spend weeks lining up a full audit, here's a self-check you can run in 20 minutes that catches the mistakes I see most often in first-time deployments.

I run security reviews for small and new protocols, and the same handful of issues come up again and again. None of these need a tool — just your eyes and this list.

1. Who can call what?

Open every external/public function that moves funds, mints, pauses, or upgrades. For each, ask: should a random address be able to call this? If not — is there an onlyOwner / onlyRole / require(msg.sender == ...) guarding it, in the function itself or in every internal function it calls?

The classic bug isn't a missing modifier. It's a function that looks unguarded but delegates to a guarded internal one (fine), or one that looks guarded but the guard is in a branch a caller can skip (not fine). Trace the call, don't trust the signature.

2. The first-depositor trap (if you have a vault)

If you mint shares from deposits (ERC-4626 or anything share-based), the first depositor can sometimes donate assets directly to the contract to inflate the share price, so the second depositor rounds down to zero shares and loses funds. Fix: virtual shares, a dead-shares mint at deploy, or a minimum-liquidity lock. OpenZeppelin's ERC-4626 handles this out of the box — a hand-rolled vault usually doesn't.

3. Reentrancy — but only the real kind

Not every external call is reentrancy. It's a bug when an attacker-controlled call can re-enter and corrupt shared storage before you've updated it. Quick checks:

  • Do you update state before the external transfer (checks-effects-interactions)?
  • Is there a nonReentrant on functions that move value?
  • Is the call target a trusted, immutable contract, or an arbitrary address the attacker supplies?

A call to a protocol-owned contract, or a memory/local variable written after the call, is usually not exploitable. Don't flag it just because Slither did.

4. Where do "rescue" funds go?

Got a sweep / rescue / emergencyWithdraw? Check the destination. If funds can only go to a fixed, stored address (treasury/owner), permissionless is fine — the caller can't redirect them. If the caller picks the destination, that's a drain waiting to happen. This one is a coin-flip in the reviews I do.

5. Oracle freshness (if you read a price)

Reading Chainlink? You need an updatedAt staleness check, not just answer > 0. But — and this is where half the "findings" you'll read online are wrong — plenty of protocols check it one layer up or rely on a heartbeat. Look at the whole path before you panic.

6. Upgradeable? Check the initializer.

Proxy pattern? Make sure initialize() has the initializer modifier, can't be called twice, and that the implementation contract itself can't be initialized-then-selfdestructed. A left-open initializer is one of the most common takeover bugs.


That's the 20-minute pass. It catches the obvious stuff — and honestly, most exploits of small protocols are the obvious stuff shipped in a hurry.

What it doesn't catch is the protocol-specific logic bug: the one where your staking math rounds the wrong way, or two functions interact in a way nobody drew on the whiteboard. That's what a human review is for.

My rule when I do those reviews: I only flag what's actually exploitable. I keep my scanner at zero false positives across the entire OpenZeppelin library — so when it (or I) stay silent on your code, that silence means something. A report that cries wolf 40 times trains you to ignore the one that matters.

If you want a free first pass before you ship, the scanner is open-source and runs in one command: https://github.com/juan23z/openclaw-audit. And if you'd rather have a person read the tricky parts — fast, honest, and I'll tell you plainly when your code is solid — that's what I do.

Good luck on mainnet. 🚀