I struggled. I questioned myself. I almost quit.
All before I found a new approach to reviewing code. And a bug that could make Lido oracles crash.
Part 1: I'm an auditor
Lido v2 is tough. So many contracts, countless lines of Solidity code, roles everywhere, unstructured storage, oracles. You name it, it's there.
But I'm no stranger to such monsters. So I was eager to face this one. My plan: do what I do best. What I would do in my old days as an auditor.
Drawing from my past experiences, I began the security spotcheck reading each line of code. Meticulously. Trying to understand each function, from simpler to more complex ones. That would be my way to fully understand Lido and uncover a vulnerability.
Day 1: OK.
Day 2: OK?
Day 3: really, OK?
Day 4: hmm.
Day 5: not OK.
What was going on?
I was distracted. Puzzled. I couldn't craft, let alone follow, non-trivial trains of thought. Seldom did I struggle that much. Something was different this time. I was trudging through the code. My auditor weapons felt heavy and rusty. Meanwhile, the monster only grew more fearsome.
I tried switching contracts. Features. Functions. Anything that would shake me out of the rut. To no avail.
It wasn't long until the questioning began.
Part 2: Am I an auditor ?
I was tired and frustrated. So much that I dropped the thing altogether. Stopped auditing Lido v2, to spend a few days on other tasks at the guild. Hoping that the distance would favor me with a fresher perspective on the problem.
Why was the job feeling so grueling, so unstimulating? Was the auditor mindset helping me? Could I shift it?
Could I go from a rather passive reading approach to something more creative?
Would an auditor do that?
I always tell people that a security spotcheck is not a smart contract audit. Why, then, was I thinking and acting as an auditor?
Such questioning brought me to a realization. The simplest of the ideas crashed in my mind. If I'm not having a good time acting as an auditor, and this is not an audit...
I would stop being an auditor.
Part 3: I'm not an auditor
Sounds silly, doesn't it? It wasn't for me. It's not easy to let go of the costumes we're so used to wearing.
Now that I was in the questioning mood, I thought of using it to my advantage. The next day I was back at the code, with a new plan.
I would stop passively consuming lines of code. No more line-by-line reading. Instead, I would follow a dead-simple, 3-steps framework to review the code:
1) Ask a question.
2) Find the answer in the code.
3) Repeat.
I'd avoid questions that were too blurry and general. Otherwise I'd get too lost wandering for an answer. The more specific the question, the easier it'd be to stay laser-focused on it. I didn't care if my questions were too basic or complex. They were triggers. Gateways to exploring Lido in novel ways.
Intuition, curiosity and experience helped. I gradually started building a collection of questions and answers about specific parts of Lido. From the most general, to the more specific ones.
Look at these random samples:
As days passed, I felt real progress. My Q&A set was growing organically.
Each question helped shape the next ones, creating a trail of my review. Newer questions felt more mature than the early ones, and I felt more confident to dive deeper and deeper into the codebase.
I even went public sometimes.
I wasn't constrained by anything but my curiosity. Which soon led me out of the smart contracts. I began exploring, and understanding, withdrawals and exits of validators.
Here's me asking my friends about validators exiting:
That led me into a day-long rabbit hole into reading and comparing consensus nodes code to check whether they handled exited validators differently. Unrelated to Lido really, but hey, it was worthwhile.
Back to Lido, soon I was out of the smart contracts, looking at the oracles' off-chain code. Of course, asking more questions.
I had seen lots of potential reverts in the Solidity code. I wanted to learn how these errors were handled in the oracle's Python code.
There was a call that got me thinking. It used a try/catch to handle some errors, but I wondered whether that was enough.
I began listing each possible exception I could identify that must be handled. That led me into reading the source code of web3.py, the Python library used by the oracle.
The oracle was calling the function wait_for_transaction_receipt
. Which is defined here. That can potentially raise the TimeExhausted
exception.
Was TimeExhausted
handled by the oracle? Nope.
So that's a bug! After considering multiple scenarios, we decided it wasn't severe enough to require a private disclosure.
The unhandled exception would cause the oracle to crash. But only after 120 seconds of asking many times for the receipt without receiving it back. Not that unlikely, although didn't seem risky for Lido.
Therefore, we just submitted a public issue for the developers.
Is it big? I wouldn't say so.
But after so much questioning, it was rewarding.