<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: haze</title>
    <description>The latest articles on DEV Community by haze (@0xhxxe).</description>
    <link>https://dev.to/0xhxxe</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4199268%2F550cf2a3-d898-4c43-9e84-a8977582e687.jpg</url>
      <title>DEV Community: haze</title>
      <link>https://dev.to/0xhxxe</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/0xhxxe"/>
    <language>en</language>
    <item>
      <title>5 smart contract bugs i still see in 2026 (and how to catch them)</title>
      <dc:creator>haze</dc:creator>
      <pubDate>Wed, 21 Oct 2026 23:31:49 +0000</pubDate>
      <link>https://dev.to/0xhxxe/5-smart-contract-bugs-i-still-see-in-2026-and-how-to-catch-them-2an6</link>
      <guid>https://dev.to/0xhxxe/5-smart-contract-bugs-i-still-see-in-2026-and-how-to-catch-them-2an6</guid>
      <description>&lt;h1&gt;
  
  
  5 smart contract bugs i still see in 2026 (and how to catch them)
&lt;/h1&gt;

&lt;p&gt;&lt;em&gt;the boring bugs that keep draining real money, and the checks that stop them&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;you'd think we'd be done with these by now. we're not. i read a lot of solidity: client code, contest code, forks of forks. the same five bugs keep showing up, usually in code that looks clean at first glance.&lt;/p&gt;

&lt;p&gt;here they are, with the smallest snippets i could write, and how i actually catch them.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. reentrancy, even when you "followed CEI"
&lt;/h2&gt;

&lt;p&gt;checks-effects-interactions is good advice. it's just not the whole story. CEI protects the function you wrote it in. it doesn't protect &lt;em&gt;other&lt;/em&gt; functions that read the same state while you're mid-call.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function withdraw(uint256 shares) external {
    uint256 amount = previewRedeem(shares);
    _burn(msg.sender, shares);            // effect
    (bool ok,) = msg.sender.call{value: amount}(""); // interaction
    require(ok);
    totalAssetsCached -= amount;          // effect AFTER the call
}

function price() external view returns (uint256) {
    return totalAssetsCached * 1e18 / totalSupply();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;shares are burned but &lt;code&gt;totalAssetsCached&lt;/code&gt; isn't updated yet. during the callback, &lt;code&gt;price()&lt;/code&gt; is inflated. any protocol that uses this as an oracle gets read-only reentrancy'd. also watch ERC777 / ERC721 / ERC1155 hooks (&lt;code&gt;onERC721Received&lt;/code&gt; is an external call too).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;catch it:&lt;/strong&gt; finish &lt;em&gt;every&lt;/em&gt; state update before any external call, not just "most". use a &lt;code&gt;nonReentrant&lt;/code&gt; guard and expose a reentrancy check on view functions other protocols consume. in review, list every external call and ask "what state is half-updated right now?"&lt;/p&gt;

&lt;h2&gt;
  
  
  2. rounding in share math (hi, ERC4626 inflation)
&lt;/h2&gt;

&lt;p&gt;classic first-depositor attack:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;attacker deposits 1 wei, gets 1 share&lt;/li&gt;
&lt;li&gt;attacker &lt;em&gt;donates&lt;/em&gt; 10,000 tokens directly to the vault&lt;/li&gt;
&lt;li&gt;victim deposits 9,999 tokens: &lt;code&gt;9999 * 1 / 10001&lt;/code&gt; rounds to 0 shares&lt;/li&gt;
&lt;li&gt;attacker redeems and walks off with the victim's deposit
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function convertToShares(uint256 assets) public view returns (uint256) {
    uint256 supply = totalSupply();
    return supply == 0 ? assets : assets * supply / totalAssets(); // rounds down
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the deeper lesson: every division picks a winner. it should always be the vault, never the user.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;catch it:&lt;/strong&gt; use virtual shares/assets with a decimals offset (OpenZeppelin's ERC4626 does this), or seed the vault with dead shares at deploy. round down on deposit/redeem, round up on mint/withdraw. and fuzz it: "a deposit &amp;gt; 0 must never mint 0 shares" is a one-line invariant.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. stale oracle prices
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(, int256 answer,,,) = feed.latestRoundData();
return uint256(answer);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;i see this weekly. no staleness check, no sign check, no L2 sequencer check. if the feed stops updating (or the sequencer goes down and comes back), you're lending against a price from hours ago.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(, int256 answer,, uint256 updatedAt,) = feed.latestRoundData();
require(answer &amp;gt; 0, "bad price");
require(block.timestamp - updatedAt &amp;lt;= HEARTBEAT, "stale");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;catch it:&lt;/strong&gt; set &lt;code&gt;HEARTBEAT&lt;/code&gt; per feed (they differ, don't hardcode one value everywhere). on L2s, check the sequencer uptime feed plus a grace period. and never use spot AMM reserves as a price, flash loans make that free to move.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. unchecked return values and fee-on-transfer tokens
&lt;/h2&gt;

&lt;p&gt;two bugs that look like one: you trust that a transfer did what you asked.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;token.transferFrom(msg.sender, address(this), amount); // USDT returns nothing, some tokens return false
balances[msg.sender] += amount;                         // credits the full amount anyway
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;some tokens return &lt;code&gt;false&lt;/code&gt; instead of reverting. some (USDT) return nothing at all, which breaks the &lt;code&gt;IERC20&lt;/code&gt; interface call. and fee-on-transfer tokens deliver less than &lt;code&gt;amount&lt;/code&gt;, so your accounting drifts until the last user can't withdraw.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uint256 before = token.balanceOf(address(this));
token.safeTransferFrom(msg.sender, address(this), amount);
uint256 received = token.balanceOf(address(this)) - before;
balances[msg.sender] += received;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;catch it:&lt;/strong&gt; &lt;code&gt;SafeERC20&lt;/code&gt; everywhere, no exceptions. measure balance deltas if you accept arbitrary tokens, or explicitly whitelist tokens and document that you don't support weird ones. rebasing tokens are their own headache, decide up front.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. unprotected initializers
&lt;/h2&gt;

&lt;p&gt;upgradeable contracts don't have constructors doing setup, they have &lt;code&gt;initialize()&lt;/code&gt;. if it's callable by anyone, it's callable by the first person to notice.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function initialize(address _owner) external {
    owner = _owner; // no initializer modifier, anyone can call, again and again
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;two flavors:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the proxy is deployed and initialized in separate txs, someone frontruns the init&lt;/li&gt;
&lt;li&gt;the &lt;em&gt;implementation&lt;/em&gt; contract is never initialized, so anyone can take it over (and in older UUPS setups, &lt;code&gt;selfdestruct&lt;/code&gt; it via an upgrade call)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;/// @custom:oz-upgrades-unsafe-allow constructor
constructor() { _disableInitializers(); }

function initialize(address _owner) external initializer {
    __Ownable_init(_owner);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;catch it:&lt;/strong&gt; &lt;code&gt;initializer&lt;/code&gt; / &lt;code&gt;reinitializer(n)&lt;/code&gt; modifiers, &lt;code&gt;_disableInitializers()&lt;/code&gt; in the implementation constructor, and deploy+init in a single tx (factory or proxy constructor data). in review, grep for every &lt;code&gt;initialize&lt;/code&gt; and every &lt;code&gt;onlyOwner&lt;/code&gt;-less setter.&lt;/p&gt;

&lt;h2&gt;
  
  
  how i actually review for these
&lt;/h2&gt;

&lt;p&gt;nothing fancy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;map the external calls.&lt;/strong&gt; every &lt;code&gt;.call&lt;/code&gt;, transfer, hook, and oracle read. that's where 1, 3 and 4 live.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;write the invariants down.&lt;/strong&gt; "total shares backed by total assets", "no deposit mints 0", "price is never older than X". then fuzz them with foundry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;run slither&lt;/strong&gt;, then ignore 80% of it and read the other 20% carefully.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;assume weird tokens and impatient attackers.&lt;/strong&gt; if a step can be frontrun or a token can misbehave, one will.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;none of these are new. that's the point. the bugs that drain protocols are rarely clever, they're just the ones nobody checked twice.&lt;/p&gt;




&lt;p&gt;building or auditing something? &lt;a href="https://hxxe.dev" rel="noopener noreferrer"&gt;hxxe.dev&lt;/a&gt;&lt;/p&gt;

</description>
      <category>solidity</category>
      <category>security</category>
      <category>web3</category>
      <category>ethereum</category>
    </item>
  </channel>
</rss>
