- Alex Builds
- Posts
- The hardest part about auditing is...
The hardest part about auditing is...
After almost 3 years of coquetting with the idea of auditing, I finally did my first audit. Found 1H and 1M too. What stopped me for so long?
This will be a relatively short letter.
I’ve been sort of trying to become an auditor since December 2022 when I interviewed Pashov. Heavy emphasis on “sort of” because I never took it as seriously as I should have.
I’ve tried auditing in various contests, like code4rena. I’m even signed up for Sherlock and Cantina. Yet I never found one single bug. Not one.
Not for lack of skill or knowledge. I review complex PRs regularly and have done so for years now. I also read bug reports for fun.
Not for lack of work ethic either. I wouldn’t be a tech lead by now in crypto if I didn’t have it.
But for something far more unexpected…
The emotional feeling of being overwhelmed.
Unlike a PR at the company where you work as a dev, you don’t know the codebase when auditing. A literal sea of code hits you on day 1 and you have a limited time, not only to understand it but, to find bugs.
What’s more, with a contest, there’s always the risk that you find a bug, but so did everyone else and your rewards can amount to little.
For some of us this combination is truly overwhelming. It was for me for a long time.
This time it was different… Owen from Guardian Audits asked me to help audit a Solana vault that implements LayerZero for cross-chain deposits/withdrawals.
I told him “I’m a dev, not an auditor, I’m not sure I can provide value”. He still felt confident about it and said (I quote) “I'm totally happy to invest in the relationship”. So, I offered a low-risk deal: let me take a shot at it and we figure out money at the end based on what value I actually offered.
This was the kick in the butt that I needed so much. I now *HAD* to push through the codebase because I didn’t want to disappoint someone that gave me an opportunity.
And it went great. I found 1 high (specific users losing funds without any mechanism for recovery) and 1 medium (possible DOS under specific circumstances) and a few useful infos.
It’s a thing I’ll probably thank Owen for the rest of my life. I genuinely believe, and I’ve told him this, he has been one of the top ~5 most influential people in my career just by giving me this shot and reviving this dream.
I don’t want you to take from this story that you have to wait for such an opportunity. I was lucky. I want you to understand that this feeling of being overwhelmed is normal. And I want you to push through.
I’ll be doing more audits for Guardian from now on. You can, of course, also DM me for audits. I’ll be focusing on Rust/Solana for now. As you remember from my previous post, I’ve grown to like Rust.
I might look into NEAR/CosmWasm after I feel confident with my Solana auditing skills, but we’ll see about that.
Let’s talk about underflow/overflow bugs in Rust
As always, I want you to learn something new when reading my newsletter.
A simple (but potentially big) bug in Rust code (and in Solana programs) is underflow/overflow.
Any normal math like let c = a - b; can underflow/overflow.
That's both signed and unsigned integers btw. The only difference between them is that signed ones can be both positive and negative while unsigned ones can only be 0 or positive.
To make things simple, let's focus on i8 (min -128 and max 127) and u8 (min 0 and max 255).
If your operation with a u8 is let result = 245 + 20, that will result in result == 9 (there’s a 0 to pass there too, that’s why it’s not 10).
The number wraps around itself, like an odometer. Not sure if that wording makes sense, so let's do one more.
If your operation with a u8 is let result = 10 - 20, that will result in result == 245.
This is likely not the behaviour you want from your code. Normally you won't run into this sort of bug because you'll likely mostly use i64 or u64 which have far larger min/max numbers.
But, even tho you can generally avoid small integer types and it is in theory harder to make bigger integer types underflow/overflow, they can still do it.
I actually found an underflow in the audit I did: let amount_to_transfer = withdraw_params.token_amount - withdraw_params.fee.
In theory, an attacker could cause an underflow that will make the amount_to_transfer be larger than their owned tokens and potentially drain the system.
In reality, this was only an info bug because the numbers here were provided by the system and validated at another entry point. The devs still fixed this though and implemented the best practice, just to be safe.
What is the best practice? Well, you can prevent this in two ways. One is checked_add/checked_sub/checked_div/checked_mul and the other is saturating_add/saturating_sub/saturating_mul/saturating_div.
The problem with saturating_* is that it's saturating. That means if an operation would overflow, it just maxes out at the maximum number your integer can take (and vice-versa for underflowing). So let result = 10 - 20 becomes result == 0. This can be good, or it can be unexpected as well.
Generally the checked_* family is more recommended (at least in Solana programs) because it returns None if an underflow/overflow happens, disallowing such cases to play out in execution flows.
Another important point about this class of bugs is that in Rust, if you compile in debug mode, the code actually panics on underflow/overflow. In release build it wouldn’t. It would let the underflow/overflow happen.
This makes them particularly sneaky and I’ve actually had a dev on the audit tell me “but it panics for me” precisely because they didn’t know about this build difference.
I’ll think of more bugs to present to you in the future. Hope you liked reading this one!