How to save 2k$ on smart contract audit. Checklist
After auditing dozens of smart contracts, I’ve noticed a recurring pattern—many projects keep making the same mistakes.
To help teams avoid these pitfalls, I’ve compiled a list of the most common bug patterns found in audits.
Taking the time to review this checklist and check your own project against it could save you $2,000 or more on audit costs.
Once you have reviewed and resolved the respective findings from the list below, ensure that you document them as issues you are aware of before any private or public audit.
It will prevent you from incurring any unnecessary costs.
Baby basics
Use safeLibraries on everything! OpenZeppelin ****is your friend. Don’t pay for such bugs.
Don’t use
.transfer
to send ETH. Multisig can never receive it. Use.call()
insteadBy the way, always use
.call()
with a reentrancy guard and strict input validation—it’s a must-have.One more, don’t transfer
msg.value
in a loop. Hope you know it.Beloved one. Reenterency, in ERC721, in ERC1155, in hooks. Remember about it!
For AMM’s, slippage and deadline must be defined.
Integrated tokens. Ensure they behave as expected. WeirdERC20 checklist might help
Send back the surplus to the user. Customers don’t want to loose money.
Ensure the system will not wrecked if the user is blacklisted by USDC(T)
Ensure all interfaces are supported, and call to it will not revert. One man made 60k$ on such bug.
A public
burn()
function? Be careful—you don’t want to mess up your tokenomics.Validate the 0 values input. Simple. But sometimes it can lead to disaster.
What if src==dst, caller == receiver? Be aware
Code asymmetry, my friend. Withdraw must undo the deposit, unstake must undo the stake
Separate initializer function from the constructor? Front-run is possible. Factory pattern or secure deployment can help.
1 day is casted to uint24, the overflow is possible. Be explicit with data types
Apply
initializer
modifier to theinitialization()
function. Without, function can be called multiple timesAlways use upgradable versions of the libraries if your contract is upgradable.
Contract inherits OpenZeppelin's
Initializable
? If it's inherited, useonlyInitializing
instead ofinitializer
to prevent re-initialization risks.
Some spicy stuff
Auditors like loops. DoS can easily happen there. Don’t make the array huge. Can attacker increase it?
balanceOf
is manipulative. Don’t rely on it. Or be cautiousDon’t simply implement (
ecrecover
), there are a lot of bugs here. Use OpenZeppelin library.Use nonce and block.chainid to avoid replay attacks. Follow EIP-712 standard.
If you create a Vault (ERC4626), send some funds there before anyone else to prevent an inflation attack.
Oracles for the price retrievals! No spot, no, no, no.
Chainlink oracles / Pyth oracles. There are a lot of possible issues here. Just DM me and i will send you them.
LayerZero. Oh man, the same as above. I just don’t want to spam this checklist.
CREATE *new opcode in the factory? Can be lost in a block reorg. Use CREATE2 instead
Are time calculations precise? Small precision losses can accumulate, causing significant time discrepancy.
Ensure the order of
token0
andtoken1
consistent across chains.Direct usage of pool.swap() can bypass important security checks. Move via router.
In math libraries, the “unchecked” must be used carefully.
pool.slot0 is easily manipulated. Don’t rely on it.
Different price feeds may have varying decimal precisions, leading to potential inaccuracies.
Ensure the compatibility with the zkSync Era. It is not so easy there.
Can the function be front-run?
Here's a quick rounding guide for ERC4626 vaults to prevent protocol losses:
Round up when calculating protocol fees, determining
assets
inmint()
, and computingshares
inwithdraw()
.Round down when calculating
shares
indeposit()
, determiningassets
inredeem()
, and implementingconvertToAssets()
andconvertToShares()
.
Brain burn
Gas. Hardcode it only when you know what you are doing. Very easy to mess up.
msg.sender as a contract can equal to tx.origin after the Pectra upgrade. Prepare for it.
Don’t charge the fees once your logic is paused. Simply.
If the collateral deposits are paused, ensure the user can’t be unfairly liquidated.
After the unpause, can it end up in the overdue liquidations? Take it into an account.
If the onBehalf repayment are possible, can it be exploited to affect someone’s position?
Prioritisation issue. Example: a user is waiting to withdraw and has both pending withdrawal and deposit amount, the withdrawal must be finalized first.
Can protocol logic that requires actions on the user be manipulated by an attacker to cause high gas usage, making it non-functional?
Internal calculation only! But be aware about dusting attack.
Account for the interest during LTV calculation. Or it can result in inaccurate assessments.
Withdrawals should be disabled within the same block, to prevent the flashloan attacks.
Ensure the self-liquidation can’t result in the unfair/unintended consequences. Test it!
Does system handle oracle reverts or misbehaviour? Pause the protocol so you don’t work with incorrect prices.
Merkle trees: 1 - Hash the leaf data to prevent length collision. 2 - Use Domain Separation between node & leaf. 3 - Validate the depth of merkle and root
Is the Oracle's TWAP period properly configured? If it's too short or poorly set, attackers could manipulate prices.
If you liked this checklist. Follow my newsletter. https://www.arsensecurity.com/newsletter
There is more interesting coming up 🔥