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() instead

  • By 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 the initialization() function. Without, function can be called multiple times

  • Always use upgradable versions of the libraries if your contract is upgradable.

  • Contract inherits OpenZeppelin's Initializable? If it's inherited, use onlyInitializing instead of initializer 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 cautious

  • Don’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 and token1 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 in mint(), and computing shares in withdraw().

    • Round down when calculating shares in deposit(), determining assets in redeem(), and implementing convertToAssets() and convertToShares().

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 🔥

Next
Next

How to build a secure project. Free Guide