21 days that changed my audit approach
Most auditors can fail at deep bugs before they even begin to hunt
Very often - it’s not auditor mistake.
But mistake of the project to-be-audited.
You can’t find deep bugs in a buggy codebase.
Your brain never gets the chance to enter a deep flow.
The moment you start thinking about state transitions, money flow, or actor behavior, you already see 1–3 obvious bugs. It feels good — like progress — but it’s fake.
You’re not “thinking deeply.” You’re cleaning up the mess of a junior developer.
Here’s the truth you must accept:
If you train on poorly written systems, you will stay a poorly performing auditor.
- If you never audit codebases you don’t fully understand…
- If you avoid the complex ones…
- If you run every time you feel dumb or stuck…
You will never grow.
A single critical bug that can kill a protocol is worth more than a hundred medium findings that give you a dopamine hit.
A Real Example
My biggest shift as an auditor happened during the contest where I earned my first $10K.
It was my 7th month of grinding. I had failed many times before. But I’ve never give up
Three weeks.
Tight codebase.
$200K pot.
Cross-chain.
You can already imagine the pressure.
Context
The project rate-limits token transfers between chains.
Each token has a bucket on every chain:
- capacity → the maximum amount allowed
- tokens → the current remaining amount
- lastUpdated → when the bucket was last refilled
Buckets refill over time according to:
$newTokens = oldTokens + rate * timeDiff$
So if block.timestamp moves forward, the bucket refills.
If time doesn’t move, the bucket never refills.
Cross-chain transfers require that:
- Ethereum bucket deducts
- Arbitrum bucket deducts
- Both refill according to their timestamps
Project design assumes that if a cross-chain transfer fails due to some errors, the node can retry it for up to 8 hours.
This only works if the error is catchable.
You can already sniff the context of the vulnerability.
block.timestamps- Look for an error that cross-chain logic doesn’t catch
It looks easy now.
But during that audit. I’ve simply grind.
No approach. No invariants abstraction. No precise strategy.
Here’s how it went:
Week 1: I barely understood the system. Disappointed, angry, feeling dumb
Week 2: I already get the logic. Zero bugs yet. Difficult, but curiosity didn’t die. I isolated the core flow. Continue grinding. Continue hunting.
Week 3: I sat on the flow that catch errors for seven straight days — eight hours per day — 150 lines of code max.
I dreamed about those lines.
No vulnerabilities…
21 days of non stop audit already passed…
I’ve constantly asked myself
What error that cross-chain logic doesn’t catch?
And then, last day — eight hours before the deadline — I asked myself.
“Wait. Each chain behaves differently. What happens if the L2 we integrate with doesn’t move time forward the same way as Mainnet?”
I dug into Arbitrum docs.
And here’s what I found

Boooom!
That’s it.
I finally get the assumption.
If I’d find this assumption earlier….
I’d catch issue earlier.
Entire protocol relies on one assumption:
Both chains update time in a predictable way.
Now look what happens when this assumption breaks.
There’s a lane: Ethereum → Arbitrum.
- Ethereum bucket capacity = 150e18
- User sends 100e18
- Ethereum deducts → 50e18 left
- Arbitrum receives the tx → deducts → also 50e18 left
lastUpdatedtimestamp is stored on both sides
So far everything is fine.
Then Arbitrum sequencer stalls.
- No batches posted
- No blocks produced
- No timestamp updates
Meanwhile on Ethereum, time moves normally, so the bucket refills back to 150e18.
User now sees 150e18 capacity on Ethereum and sends another full transfer.
The tx reaches Arbitrum.
But because Arbitrum didn’t move time:
timeDiff = block.timestamp - lastUpdated = 0
So the bucket never refills on Arbitrum.
It’s still stuck at 50e18.
The protocol tries to deduct 150e18 from 50e18 and hits:
TokenRateLimitReached
And here’s the real problem:
If the sequencer stalls for more than 8 hours, cross-chain protocol can’t catch the error → it can’t retry → and the user permanently loses funds.
This is exactly the kind of cross-assumption bug most auditors NEVER look for.
On Ethereum the basket would fill.
On Arbitrum it wouldn’t.
The whole design assumed chains behave the same.
That single oversight produced the only Medium in the entire codebase.
And it taught me:
- Tight codebases make you a real attacker.
- They force you to think about vital flow non-stop
- They expose your weaknesses in an approach.
- They turn you into someone who hunts assumptions — not convenient mistakes.
My core mistake was that:
- I’ve understood the code
- Carefully reviewed each line
- Mapped everything
But I didn’t look deeper.
I haven’t taken out the most crucial assumptions that resided between the code lines.
I was sitting on the flow, asking the wrong questions.
Instead of asking
What error that cross-chain logic doesn’t catch?
I should ask:
What assumption logic has regarding chains it integrates with?
Small question. Huge difference.
Remember. You must audit complex code constantly.
- You get angry.
- You feel desperate.
- You want to quit.
- You want to take an early break.
- You feel dumb.
Here’s the feelings you need to look for.
This is the edge where growth happens.
You won’t become a deeper auditor by staying in comfortable codebases.
You won’t find complex bugs by sticking to ecosystems you already understand.
You won’t build attacker-level intuition by reading surface-level Solodit findings.
You grow by doing the opposite.
You always have a choice:
- Read another simple post-mortem…or pick one complex exploit and spend a week dissecting it line-by-line.
- Audit a codebase you already “kind of get”…or dive into a brand-new ecosystem where you feel like an idiot.
- Stay in familiar languages…or attack a system that forces you to rebuild your mental models.
Every small decision compounds.
At this point in time, auditing this cross-chain project was difficult for me.
However, this exact codebase has taught me more, than couple medium complexity audits in a row