<?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: PandaSecurity</title>
    <description>The latest articles on DEV Community by PandaSecurity (@pandasecurity).</description>
    <link>https://dev.to/pandasecurity</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%2F915214%2Fcfc5c970-b102-4be4-9ab2-9c71b0d38065.jpg</url>
      <title>DEV Community: PandaSecurity</title>
      <link>https://dev.to/pandasecurity</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pandasecurity"/>
    <language>en</language>
    <item>
      <title>Sui Bridge Claim Bot Sent $392K to the Cetus Attacker</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Fri, 16 Oct 2026 23:21:06 +0000</pubDate>
      <link>https://dev.to/pandasecurity/sui-bridge-claim-bot-sent-392k-to-the-cetus-attacker-1ikb</link>
      <guid>https://dev.to/pandasecurity/sui-bridge-claim-bot-sent-392k-to-the-cetus-attacker-1ikb</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/sui-bridge-stale-withdrawals-cetus-attacker/" rel="noopener noreferrer"&gt;Sui Bridge Claim Bot Sent $392K to the Cetus Attacker&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;16 October 2026&lt;/strong&gt;, between 04:39 and 04:40 UTC, an automated bot finalized a batch of old, unclaimed Sui Bridge withdrawals on Ethereum. Most of them belonged to ordinary users. Eight of them did not. They had been sent from the Sui address of the &lt;strong&gt;Cetus exploiter&lt;/strong&gt;, and finalizing them delivered &lt;strong&gt;86 ETH, 109,000 USDT and 0.9 WBTC&lt;/strong&gt; to the attacker's Ethereum wallet, around &lt;strong&gt;$392K&lt;/strong&gt; by the bot operator's own estimate.&lt;/p&gt;

&lt;p&gt;No contract was broken. Nobody forged a signature. The bridge did exactly what it was built to do. That is what makes this incident worth studying: a perfectly correct, permissionless code path turned dormant attacker funds into spendable ones, because the off-chain automation around it never asked &lt;em&gt;whose&lt;/em&gt; money it was moving.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;What happened:&lt;/strong&gt; a "rescue" bot completed stale Sui Bridge withdrawals in three Ethereum transactions. Eight of the withdrawals had the Cetus attacker's Sui address as sender and its Ethereum address as recipient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Assets delivered to the attacker:&lt;/strong&gt; 86 ETH, 109,000 USDT and 0.9 WBTC, which the operator put at roughly $392K.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause:&lt;/strong&gt; not a contract bug. &lt;code&gt;transferBridgedTokensWithSignatures()&lt;/code&gt; lets anyone submit a committee-signed message, and the bot submitted every valid message it found without screening the sender or recipient.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Status at the time of writing:&lt;/strong&gt; the 109,000 USDT and 0.9 WBTC were still in the attacker's wallet, and USDT's on-chain blacklist did not yet include the address. The operator has asked for help getting the USDT frozen.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Background: Cetus and the Leftover Withdrawals
&lt;/h2&gt;

&lt;p&gt;Cetus, the largest concentrated-liquidity DEX on Sui, was drained of roughly &lt;strong&gt;$220 million&lt;/strong&gt; on 22 May 2025 through an arithmetic overflow in its liquidity math. Sui validators froze about &lt;strong&gt;$162 million&lt;/strong&gt; on the Sui side within the first hour, while part of the haul had already left for Ethereum. Public analyses from several research teams list the attacker's Sui address as &lt;code&gt;0xe28b50…8ff06&lt;/code&gt; and the Ethereum address that received bridged funds as &lt;code&gt;0x89012a55…24919b&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The 16 October &lt;code&gt;TokensClaimed&lt;/code&gt; events, which we decoded ourselves, show that a set of Sui-to-Ethereum bridge messages from that same Sui address had been signed but &lt;strong&gt;never claimed&lt;/strong&gt; on Ethereum. On a lock-and-claim bridge, that leaves the funds sitting in the Ethereum-side vault indefinitely. We could not find a public explanation of why they were left unclaimed, and we won't guess at one. What matters is that they were still claimable, and that claiming them pays out to the recipient fixed in the message, which here was the attacker.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the Sui Bridge Claim Works
&lt;/h2&gt;

&lt;p&gt;The Ethereum side of Sui Bridge is open source. A transfer from Sui is finalized on Ethereum by calling &lt;code&gt;transferBridgedTokensWithSignatures()&lt;/code&gt; with the message and enough committee signatures. The function checks the signatures, checks that the nonce hasn't been processed, enforces a 24-hour rate limit, and then pays the &lt;strong&gt;recipient encoded in the message&lt;/strong&gt;. It does not care who &lt;code&gt;msg.sender&lt;/code&gt; is.&lt;/p&gt;

&lt;p&gt;The simplified sketch below is &lt;strong&gt;our illustration&lt;/strong&gt; of that flow, condensed from the public &lt;code&gt;SuiBridge.sol&lt;/code&gt;, not a verbatim copy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Simplified illustration of a permissionless bridge claim (not verbatim SuiBridge code)
function transferBridgedTokensWithSignatures(
    bytes[] memory signatures,
    Message memory message
) external nonReentrant verifyMessageAndSignatures(message, signatures) {
    require(!isTransferProcessed[message.nonce], "already processed");

    TokenTransferPayload memory p = decodeTokenTransferPayload(message.payload);
    require(p.targetChain == chainID, "wrong chain");

    // Funds always go to the recipient chosen on the source chain.
    // The caller cannot redirect them, and the caller is never checked.
    _transferTokensFromVault(message.chainID, p.tokenID, p.recipientAddress, p.amount);

    isTransferProcessed[message.nonce] = true;
    emit TokensClaimed(message.chainID, message.nonce, chainID, p.tokenID,
                       p.amount, p.senderAddress, p.recipientAddress);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is a sound, common design. Because the caller cannot change where the money goes, letting anyone relay a message is safe for the user, and it is good for liveness: a user who lost interest, or a relayer that went offline, does not leave funds stranded forever. Many bridges work the same way.&lt;/p&gt;

&lt;p&gt;The catch is that "safe for the user" assumes the recipient &lt;em&gt;should&lt;/em&gt; receive the funds. When the recipient is an exploiter, a permissionless claim is a one-way valve that anyone can open.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the Bot Did, Step by Step
&lt;/h2&gt;

&lt;p&gt;The three claim transactions came from the same externally owned account, &lt;code&gt;0x7f2e…6671&lt;/code&gt;, each one a batched &lt;code&gt;Multicall3&lt;/code&gt; call that finalized many withdrawals at once. We decoded every &lt;code&gt;TokensClaimed&lt;/code&gt; event emitted by the Sui Bridge contract in those transactions. The withdrawals sent to the attacker were:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tx&lt;/th&gt;
&lt;th&gt;Sui nonce&lt;/th&gt;
&lt;th&gt;Asset&lt;/th&gt;
&lt;th&gt;Amount&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x89ca33…dde89&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7025, 7027, 7030&lt;/td&gt;
&lt;td&gt;ETH&lt;/td&gt;
&lt;td&gt;28 ETH each (84 ETH)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x89ca33…dde89&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7028&lt;/td&gt;
&lt;td&gt;WBTC&lt;/td&gt;
&lt;td&gt;0.9 WBTC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0x89ca33…dde89&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7031&lt;/td&gt;
&lt;td&gt;USDT&lt;/td&gt;
&lt;td&gt;93,000 USDT&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0xce494e…36ac&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7020, 7022&lt;/td&gt;
&lt;td&gt;USDT&lt;/td&gt;
&lt;td&gt;8,000 USDT each&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;0xe63cb0…1fc8&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;7021&lt;/td&gt;
&lt;td&gt;ETH&lt;/td&gt;
&lt;td&gt;2 ETH&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Every one of these events carries the same 32-byte sender, &lt;code&gt;0xe28b50cef1d633ea43d3296a3f6b67ff0312a5f1a99f0af753c85b8b5de8ff06&lt;/code&gt;, the Cetus attacker's Sui address, and the same recipient, &lt;code&gt;0x89012a55cd6b88e407c9d4ae9b3425f55924919b&lt;/code&gt;. The two signals that could have stopped the claim were sitting in the event data the whole time.&lt;/p&gt;

&lt;p&gt;The same batches also finalized legitimate withdrawals for other users, including one WBTC and two ETH withdrawals on Sui Bridge, plus pending withdrawals on other protocols' contracts. In other words, this was a general-purpose "finish what others left unfinished" bot, and the attacker's messages simply looked like more stale funds.&lt;/p&gt;

&lt;h3&gt;
  
  
  The on-chain exchange that followed
&lt;/h3&gt;

&lt;p&gt;About half an hour later, a user signing as &lt;strong&gt;tanuki42&lt;/strong&gt; sent an on-chain message to the bot's address pointing out that roughly $390K had gone to "an address publicly associated with the Cetus exploit" and suggesting it screen recipients before "rescuing" funds. Defimon Alerts relayed the message on X.&lt;/p&gt;

&lt;p&gt;The operator, signing as &lt;strong&gt;lostandfound.eth&lt;/strong&gt;, replied on-chain at 05:38 UTC: &lt;em&gt;"you're right, our mistake. We didn't know 0x89012a…919b was the Cetus attacker when our automated run completed those Sui Bridge withdrawals."&lt;/em&gt; The reply lists the three claim transactions, estimates the total at about $392K, says the 109,000 USDT had not moved, asks for the case to be routed to Tether and Cetus for a freeze, and says the bot now screens every recipient against known exploit and sanctioned addresses before claiming.&lt;/p&gt;

&lt;p&gt;When we checked the wallet after these messages, it still held 109,000 USDT and 0.9 WBTC, and USDT's &lt;code&gt;getBlackListStatus()&lt;/code&gt; for the address returned &lt;code&gt;false&lt;/code&gt;. The ETH, of course, cannot be frozen by anyone.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters Beyond One Bot
&lt;/h2&gt;

&lt;p&gt;It is tempting to file this under "operator error" and move on. We think there are three broader lessons.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Dormant is not the same as safe
&lt;/h3&gt;

&lt;p&gt;After an exploit, incident response focuses on funds that are moving. Messages that were signed but never executed get far less attention, yet they are live liabilities. Anyone, at any time, can turn them into spendable assets. The same applies to queued withdrawals, unexecuted governance actions, pending limit orders and approvals that never expire. We made a similar point about stale state in our &lt;a href="https://www.panda-security.xyz/blog/1003/" rel="noopener noreferrer"&gt;NEAR Intents exploit analysis&lt;/a&gt;, where balances that looked inert turned out to be withdrawable.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Permissionless relaying moves the screening burden off-chain
&lt;/h3&gt;

&lt;p&gt;When a protocol lets anyone finalize anything, the protocol itself does no screening, so whatever screening exists lives in the relayers. A careful relayer can be undone by a careless one: it takes one bot, one person running a script, or one MEV searcher looking for gas refunds to complete the transfer. Protocol teams should not assume "nobody will bother" is a control.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Recovery tooling is security-sensitive software
&lt;/h3&gt;

&lt;p&gt;Tools that find stuck bridge funds have drawn a lot of attention lately, and the well-known checkers are read-only: they tell users what is claimable and leave the claim to them. A bot that &lt;em&gt;executes&lt;/em&gt; claims on others' behalf is a different thing: it is a payment system with no compliance checks. It deserves the same threat model as any contract that moves value.&lt;/p&gt;

&lt;h2&gt;
  
  
  Mitigations
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For bridge and protocol teams
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Treat unclaimed attacker messages as part of incident response.&lt;/strong&gt; After an exploit, list every signed-but-unexecuted outbound message from attacker addresses and decide how to handle it, whether through a governance or committee action, an emergency pause path, or coordination with token issuers, before someone else decides for you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consider a sender and recipient hold list on the claim path.&lt;/strong&gt; A narrowly scoped, governance-controlled list that delays (rather than blocks forever) claims from known exploit addresses keeps permissionless relaying for everyone else. It is a trade-off against censorship resistance and must be governed transparently.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Expose message provenance clearly.&lt;/strong&gt; Make the source-chain sender easy to read in events and explorer views so relayers and monitors can screen it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  For relayers and recovery bots
&lt;/h3&gt;

&lt;p&gt;Screen both ends of the transfer, cap what can be claimed without human review, and fail closed when a screening source is unavailable. An off-chain check is enough, but a wrapper contract makes the rule enforceable. The following is a &lt;strong&gt;simplified illustration&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Simplified illustration: a relayer wrapper that refuses flagged transfers
contract ScreenedClaimRelayer {
    ISuiBridge public immutable bridge;
    IScreeningList public immutable screening; // maintained off-chain, updated by a multisig
    uint256 public immutable reviewThresholdUsd;

    function claim(bytes[] calldata sigs, Message calldata m, uint256 quotedUsd) external {
        TokenTransferPayload memory p = decodeTokenTransferPayload(m.payload);

        require(!screening.isFlaggedSui(p.senderAddress), "flagged sender");
        require(!screening.isFlaggedEvm(p.recipientAddress), "flagged recipient");
        require(quotedUsd &amp;lt; reviewThresholdUsd, "needs manual review");

        bridge.transferBridgedTokensWithSignatures(sigs, m);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  For auditors
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Map every permissionless finalize, claim, execute or settle function&lt;/strong&gt; and ask what dormant inputs it can consume. If an exploit happened tomorrow, which of those inputs would belong to the attacker?&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check that incident runbooks cover pending messages&lt;/strong&gt;, not just pausing contracts. A pause that blocks new deposits but not old claims may leave the most important door open.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Review recovery and keeper bots&lt;/strong&gt; with the same rigor as on-chain code when they move user funds. Our &lt;a href="https://www.panda-security.xyz/blog/smart-contract-security-checklist/" rel="noopener noreferrer"&gt;smart contract security checklist&lt;/a&gt; and our overview of &lt;a href="https://www.panda-security.xyz/blog/top-security-risks/" rel="noopener noreferrer"&gt;top DeFi security risks&lt;/a&gt; cover access control and operational risks that apply here as well.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Key Addresses and Transactions
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Claim tx 1: &lt;a href="https://etherscan.io/tx/0x89ca33e27e8a8b409a202b2c5710d6e1bb0accd505b6c34e95c24f35a14dde89" rel="noopener noreferrer"&gt;&lt;code&gt;0x89ca33e27e8a8b409a202b2c5710d6e1bb0accd505b6c34e95c24f35a14dde89&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Claim tx 2: &lt;a href="https://etherscan.io/tx/0xce494e04a3fe01292c7edb8b1be228c943926e920bdb3737786e6062670d36ac" rel="noopener noreferrer"&gt;&lt;code&gt;0xce494e04a3fe01292c7edb8b1be228c943926e920bdb3737786e6062670d36ac&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Claim tx 3: &lt;a href="https://etherscan.io/tx/0xe63cb0903e6784fd2343c1052ba190fe37c85d8b1d9442b9b54c1155b2e41fc8" rel="noopener noreferrer"&gt;&lt;code&gt;0xe63cb0903e6784fd2343c1052ba190fe37c85d8b1d9442b9b54c1155b2e41fc8&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Claim bot (signed as lostandfound.eth): &lt;a href="https://etherscan.io/address/0x7f2ee458d58e61345ad61a70e8072511bf9f6671" rel="noopener noreferrer"&gt;&lt;code&gt;0x7f2ee458d58e61345ad61a70e8072511bf9f6671&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cetus attacker, Ethereum: &lt;a href="https://etherscan.io/address/0x89012a55cd6b88e407c9d4ae9b3425f55924919b" rel="noopener noreferrer"&gt;&lt;code&gt;0x89012a55cd6b88e407c9d4ae9b3425f55924919b&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Cetus attacker, Sui: &lt;code&gt;0xe28b50cef1d633ea43d3296a3f6b67ff0312a5f1a99f0af753c85b8b5de8ff06&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Sui Bridge contract on Ethereum: &lt;code&gt;0xda3bd1fe1973470312db04551b65f401bc8a92fd&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Ethereum transaction receipts for the three claim transactions above, decoded by our team (&lt;code&gt;TokensClaimed&lt;/code&gt; events and token transfers)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://x.com/DefimonAlerts/status/2110961162099568834" rel="noopener noreferrer"&gt;Defimon Alerts on X: on-chain message from tanuki42&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://x.com/DefimonAlerts/status/2110968667676537104" rel="noopener noreferrer"&gt;Defimon Alerts on X: on-chain reply from lostandfound.eth&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://telem.news/security/bridge-protocol-accidentally-withdrew-392k-usdt-cetus-exploit-attacker-b75f51e1" rel="noopener noreferrer"&gt;Telem: bridge operator sent ~$392K USDT to the Cetus attacker&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://telem.news/security/chain-analysis-reveals-390k-stale-sui-bridge-withdrawals-sent-b4638229" rel="noopener noreferrer"&gt;Telem: ~$390K in stale Sui Bridge withdrawals sent to a Cetus-linked address&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/MystenLabs/sui/blob/main/bridge/evm/contracts/SuiBridge.sol" rel="noopener noreferrer"&gt;SuiBridge.sol in the MystenLabs/sui repository&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://onchainattack.org/document/examples/2025-05-cetus/" rel="noopener noreferrer"&gt;OAK: Cetus Protocol concentrated-liquidity overflow&lt;/a&gt; and &lt;a href="https://research.blockscope.co/cetus-protocol-exploit-analysis" rel="noopener noreferrer"&gt;Blockscope: Cetus exploit analysis&lt;/a&gt;: background and attacker addresses&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;The Sui Bridge contract behaved correctly, and the bot operator acknowledged the mistake within about half an hour of being flagged and changed its process. Still, a well-meaning script handed an exploiter close to $400K that had been sitting unclaimed in the bridge vault. If your protocol has any function that anyone can call to finalize something, your security model has to include the question of who that something belongs to.&lt;/p&gt;

&lt;p&gt;Running a bridge, a keeper network or a recovery tool that moves other people's funds? &lt;a href="https://www.panda-security.xyz/contact/" rel="noopener noreferrer"&gt;Contact Panda Security&lt;/a&gt; for a design and code review, or see our &lt;a href="https://www.panda-security.xyz/pricing/" rel="noopener noreferrer"&gt;audit pricing&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>security</category>
      <category>blockchain</category>
      <category>web3</category>
      <category>ethereum</category>
    </item>
    <item>
      <title>KolanHook Exploit: Unguarded Uniswap v4 Buyback Drained ETH</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Fri, 16 Oct 2026 14:38:20 +0000</pubDate>
      <link>https://dev.to/pandasecurity/kolanhook-exploit-unguarded-uniswap-v4-buyback-drained-eth-2ijc</link>
      <guid>https://dev.to/pandasecurity/kolanhook-exploit-unguarded-uniswap-v4-buyback-drained-eth-2ijc</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/kolan-hook-exploit-analysis/" rel="noopener noreferrer"&gt;KolanHook Exploit: Unguarded Uniswap v4 Buyback Drained ETH&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On &lt;strong&gt;15 October 2026&lt;/strong&gt;, an attacker took about &lt;strong&gt;2.31 ETH&lt;/strong&gt; out of &lt;strong&gt;KolanHook&lt;/strong&gt;, a Uniswap v4 hook on Ethereum mainnet, and kept roughly &lt;strong&gt;1.28 ETH (around $3K)&lt;/strong&gt; as profit after repaying a flash loan. The dollar amount is small. The pattern is not. A hook that spends its own reserves on a swap, at whatever the pool's spot price happens to be, triggered by a function anyone can call, is an exploit waiting for a caller.&lt;/p&gt;

&lt;p&gt;This post-mortem walks through what Kolan does, why the buyback path was exploitable, how the attack was sequenced, and what auditors and hook developers should take away from it.&lt;/p&gt;

&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protocol:&lt;/strong&gt; Kolan, a Uniswap v4 "window" protocol. Hook: &lt;code&gt;KolanHook&lt;/code&gt;, token: &lt;code&gt;KLN&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Chain:&lt;/strong&gt; Ethereum mainnet.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Loss:&lt;/strong&gt; about 2.31 ETH left the hook; attacker net profit about 1.28 ETH.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root cause:&lt;/strong&gt; &lt;code&gt;accept(id)&lt;/code&gt; lets anyone close an expired window. Closing a window runs a buyback swap with the hook's own ETH, and that swap used a price limit of &lt;code&gt;MIN_SQRT_PRICE + 1&lt;/code&gt;, which in practice means no slippage protection at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enabler:&lt;/strong&gt; a 6.7 WETH flash loan from Balancer to push the KLN spot price up before forcing the buyback.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What Kolan Is
&lt;/h2&gt;

&lt;p&gt;According to Kolan's own documentation, the system is two contracts: &lt;code&gt;KolanHook&lt;/code&gt;, which holds the pool liquidity and the "windows", and &lt;code&gt;KolanToken&lt;/code&gt;, a fixed-supply ERC-20 (1,000,000 KLN) minted once to the hook. There is no owner, no proxy and no pause, and the hook refuses outside liquidity. The docs also state plainly that the contracts are tested but &lt;strong&gt;not audited&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A "window" is attached to a purchase. A buyer pays a premium on top of the ETH they spend, and for a period of time they can undo the purchase. Once a window expires it can no longer be undone, only kept, and the docs describe closing it as something anyone can do after expiry. When a window is closed, the hook uses ETH it holds to &lt;strong&gt;buy back KLN and burn it&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That design gives the hook two jobs at once: it is the market's liquidity and it is a trader that periodically buys from that market. Whenever a contract trades on its own behalf, the question an auditor must ask is simple: &lt;em&gt;who decides when it trades, and at what price?&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause: A Permissionless Trigger Meets an Unbounded Swap
&lt;/h2&gt;

&lt;p&gt;Two properties, each defensible on its own, combined into the bug.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Anyone can close an expired window
&lt;/h3&gt;

&lt;p&gt;Making &lt;code&gt;accept()&lt;/code&gt; permissionless after expiry is a reasonable liveness choice. Nobody wants windows stuck open because the buyer went offline. But it also means the &lt;strong&gt;timing&lt;/strong&gt; of the buyback is chosen by whoever calls it, including someone who has just moved the price.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The buyback accepts any price
&lt;/h3&gt;

&lt;p&gt;Per Defimon's analysis, the call chain is &lt;code&gt;accept → _closeWindow → _buyBack()&lt;/code&gt;, and the buyback swaps the hook's ETH into the pool with &lt;code&gt;sqrtPriceLimitX96 = MIN_SQRT_PRICE + 1&lt;/code&gt;. In Uniswap v4 that limit is the furthest price the swap is allowed to move to. Setting it to the extreme bound tells the PoolManager "fill me at any price".&lt;/p&gt;

&lt;p&gt;The simplified sketch below is &lt;strong&gt;our own illustration&lt;/strong&gt; of the pattern, not Kolan's verified source:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Simplified illustration of the vulnerable pattern (not verbatim Kolan code)
function accept(uint256 id) external {
    Window storage w = windows[id];
    require(block.timestamp &amp;gt;= w.expiry || msg.sender == w.owner, "WindowStillOpen");
    _closeWindow(id);
}

function _closeWindow(uint256 id) internal {
    uint256 ethForBuyback = windows[id].premium / 2;
    delete windows[id];
    _buyBack(ethForBuyback);
}

function _buyBack(uint256 ethIn) internal {
    // ETH -&amp;gt; KLN, exact input, burn the output
    BalanceDelta delta = poolManager.swap(
        key,
        IPoolManager.SwapParams({
            zeroForOne: true,
            amountSpecified: -int256(ethIn),
            sqrtPriceLimitX96: TickMath.MIN_SQRT_PRICE + 1 // no real limit
        }),
        ""
    );
    _settleAndBurn(delta);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing in this path compares the current spot price with a reference price, a TWAP or a minimum output. The amount of KLN the hook receives is whatever the pool gives it at that moment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Attack, Step by Step
&lt;/h2&gt;

&lt;p&gt;The figures below come from Defimon's alert and the transaction breakdown published by ChainBounty; we have rounded them.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Flash loan.&lt;/strong&gt; The attacker borrows &lt;strong&gt;6.7 WETH&lt;/strong&gt; from Balancer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pump the spot price.&lt;/strong&gt; The borrowed ETH buys KLN, pushing the KLN price in the pool up sharply. The attacker now holds a pile of KLN bought before the buyback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Force the buyback.&lt;/strong&gt; The attacker calls &lt;code&gt;accept(10)&lt;/code&gt; and &lt;code&gt;accept(42)&lt;/code&gt; on two expired windows. Each one closes a window and triggers &lt;code&gt;_buyBack()&lt;/code&gt;, which spends the hook's ETH buying KLN at the inflated price and burns it. In total about &lt;strong&gt;2.31 ETH&lt;/strong&gt; leaves the hook.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dump into the richer pool.&lt;/strong&gt; The pool now holds the attacker's ETH plus the hook's buyback ETH. The attacker sells the KLN from step 2 back into it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Repay and keep the difference.&lt;/strong&gt; The flash loan is repaid inside the same transaction, and roughly &lt;strong&gt;1.28 ETH&lt;/strong&gt; is left over as profit.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The gap between the hook's 2.31 ETH outflow and the 1.28 ETH profit is worth spelling out. ChainBounty's write-up rightly separates the two: one is what the hook lost, the other is what the attacker kept. The rest went to swap fees, the token's 1% tax and price impact. Reporting "2.31 ETH stolen" or "1.28 ETH lost" alone would both be misleading.&lt;/p&gt;

&lt;p&gt;This is the classic &lt;strong&gt;sandwich of a protocol-owned trade&lt;/strong&gt;. Usually MEV bots sandwich users; here the victim was the protocol's own treasury logic, and the attacker didn't even need to wait for it to trade. They could make it trade on demand.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Keeps Happening in Uniswap v4 Hooks
&lt;/h2&gt;

&lt;p&gt;We covered how hooks work in &lt;a href="https://dev.to/blog/1012/"&gt;Uniswap v4 Hooks: How They Work and Where They Break&lt;/a&gt;, and two recent incidents on this blog, the &lt;a href="https://dev.to/blog/1008/"&gt;GoldPesa GPXHooks rebalance drain&lt;/a&gt; and the &lt;a href="https://dev.to/blog/1009/"&gt;one-wei share vault exploit&lt;/a&gt;, point at the same theme: once a hook holds value and acts on its own, it inherits every risk of an on-chain trader.&lt;/p&gt;

&lt;p&gt;Hooks make this easy to get wrong for three reasons:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Spot price is one call away.&lt;/strong&gt; &lt;code&gt;getSlot0()&lt;/code&gt; and an unbounded swap are the path of least resistance. A safe price needs extra work.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flash loans are cheap on Ethereum.&lt;/strong&gt; Moving a thin pool costs only the fees, and a new token pool is almost always thin.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"Permissionless" is treated as a feature,&lt;/strong&gt; without checking what the permissionless caller can make the contract do at that moment.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Impact
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Direct loss:&lt;/strong&gt; about 2.31 ETH of hook reserves, roughly $5.4K at the time per Defimon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attacker profit:&lt;/strong&gt; about 1.28 ETH, roughly $3.0K.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scope:&lt;/strong&gt; the hook is ownerless and immutable, so there is no admin fix. Any remaining expired window can be closed the same way as long as reserves and a manipulable pool remain. At the time of writing we found no public remediation statement from Kolan.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Mitigations and Audit Lessons
&lt;/h2&gt;

&lt;h3&gt;
  
  
  For hook developers
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Never let protocol-owned swaps run at an unbounded price.&lt;/strong&gt; Derive &lt;code&gt;sqrtPriceLimitX96&lt;/code&gt; from a reference price (a TWAP, an oracle, or the price recorded when the window opened) and revert if the spot price is too far away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enforce a minimum output.&lt;/strong&gt; Compute the expected KLN for the ETH spent and require the swap to return at least, say, 98% of it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate "close" from "spend".&lt;/strong&gt; Let anyone mark a window as expired, but queue the buyback and execute it in batches, or only when the spot price is close to the TWAP.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Check for same-block manipulation.&lt;/strong&gt; If the pool price has moved more than a set threshold in the current block, skip or defer the buyback.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A hardened version of the buyback could look like this (again illustrative):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function _buyBack(uint256 ethIn) internal {
    uint160 ref = _twapSqrtPrice(30 minutes);              // manipulation-resistant reference
    uint160 limit = uint160(uint256(ref) * 9_900 / 10_000); // allow ~1% adverse move
    (uint160 spot,,,) = poolManager.getSlot0(key.toId());
    if (spot &amp;lt; limit) {
        pendingBuyback += ethIn; // defer instead of trading into a manipulated pool
        return;
    }
    BalanceDelta delta = poolManager.swap(
        key,
        IPoolManager.SwapParams({
            zeroForOne: true,
            amountSpecified: -int256(ethIn),
            sqrtPriceLimitX96: limit
        }),
        ""
    );
    require(uint128(delta.amount1()) &amp;gt;= _minOut(ethIn, ref), "Slippage");
    _settleAndBurn(delta);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  For auditors
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;List every place the contract trades its own funds&lt;/strong&gt; and, for each one, write down who can trigger it and what price it accepts. If the answers are "anyone" and "any", it is a finding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Grep for extreme price limits.&lt;/strong&gt; &lt;code&gt;MIN_SQRT_PRICE + 1&lt;/code&gt;, &lt;code&gt;MAX_SQRT_PRICE - 1&lt;/code&gt; and &lt;code&gt;amountOutMin = 0&lt;/code&gt; are fine in tests and in user-facing routers where the user sets the bound. Inside protocol logic they are red flags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Write the flash-loan test.&lt;/strong&gt; A Foundry test that borrows, moves the pool, calls the permissionless function and checks the hook's balance takes under an hour and would have caught this.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Our &lt;a href="https://dev.to/blog/smart-contract-security-checklist/"&gt;smart contract security checklist&lt;/a&gt; includes these slippage and price-source checks for any contract that swaps.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key Addresses
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Attack transaction: &lt;a href="https://etherscan.io/tx/0x7b131926e6f06491ae1aead3e419b208c8aa70ed7898e8c5a33d9f52e40915a9" rel="noopener noreferrer"&gt;&lt;code&gt;0x7b131926e6f06491ae1aead3e419b208c8aa70ed7898e8c5a33d9f52e40915a9&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Attacker: &lt;a href="https://etherscan.io/address/0x8df735014eab81905e76b87ef0c069bd8e0ae382" rel="noopener noreferrer"&gt;&lt;code&gt;0x8df735014eab81905e76b87ef0c069bd8e0ae382&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Victim hook (KolanHook): &lt;a href="https://etherscan.io/address/0x0136fecf49e0217bfd2d9551e8e7b05f174ba8ec" rel="noopener noreferrer"&gt;&lt;code&gt;0x0136feCf49E0217BFd2D9551E8e7b05f174Ba8eC&lt;/code&gt;&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Token (KLN): &lt;code&gt;0x2CADc4F2660Bb77a118b0e74fC9E48E935b3a1bF&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;a href="https://x.com/DefimonAlerts/status/2110776621087047818" rel="noopener noreferrer"&gt;Defimon Alerts on X&lt;/a&gt;: loss figures, call chain and transaction&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://community.chainbounty.io/ko/posts/01a144a5-cf8a-7961-88b0-95ac61a121dd" rel="noopener noreferrer"&gt;ChainBounty: KolanHook transaction breakdown&lt;/a&gt;: exact ETH flows&lt;/li&gt;
&lt;li&gt;&lt;a href="https://telem.news/security/kolan-protocol-s-uniswap-v4-hook-exploited-flash-loan-a322fc84" rel="noopener noreferrer"&gt;Telem incident summary&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://kolan.c0llback.com/docs/contracts" rel="noopener noreferrer"&gt;Kolan contract reference&lt;/a&gt; and &lt;a href="https://kolan.c0llback.com/docs/technical-faq" rel="noopener noreferrer"&gt;technical FAQ&lt;/a&gt;: contract design and addresses&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;KolanHook lost a few thousand dollars, but its design is not unusual. Plenty of new hooks buy back, rebalance or compound with their own funds, and many of them let anyone trigger it. If your hook trades, the price it accepts is part of your security model, not a gas optimization you can skip.&lt;/p&gt;

&lt;p&gt;Building a Uniswap v4 hook or a token with automated buybacks? &lt;a href="https://dev.to/contact/"&gt;Contact Panda Security&lt;/a&gt; for a review before launch, or see our &lt;a href="https://dev.to/pricing/"&gt;audit pricing&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>defi</category>
      <category>security</category>
      <category>uniswap</category>
      <category>solidity</category>
    </item>
    <item>
      <title>Smart Contract Security Checklist for Web3 Developers</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Fri, 16 Oct 2026 01:32:36 +0000</pubDate>
      <link>https://dev.to/pandasecurity/smart-contract-security-checklist-for-web3-developers-34kn</link>
      <guid>https://dev.to/pandasecurity/smart-contract-security-checklist-for-web3-developers-34kn</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/smart-contract-security-checklist/" rel="noopener noreferrer"&gt;Smart Contract Security Checklist for Web3 Developers&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;A smart contract can look simple in a code editor and still hold millions of dollars in user funds.&lt;/p&gt;

&lt;p&gt;That is one of the uncomfortable realities of Web3 development. A function may only be 20 lines long, but a small mistake in accounting, permissions, token handling, or price calculations can become an exploitable path once the contract is deployed.&lt;/p&gt;

&lt;p&gt;The numbers make the risk difficult to ignore.&lt;/p&gt;

&lt;p&gt;Immunefi reported more than &lt;strong&gt;$1.46 billion lost to hacks across 192 incidents in 2024&lt;/strong&gt;. CertiK reported &lt;strong&gt;$3.35 billion in losses across Web3 security incidents in 2025&lt;/strong&gt;, although the 2025 figure was heavily affected by several exceptionally large attacks.&lt;/p&gt;

&lt;p&gt;This does not mean every smart contract needs an enormous security team.&lt;/p&gt;

&lt;p&gt;It does mean that security should be considered during development rather than added immediately before deployment.&lt;/p&gt;

&lt;p&gt;This checklist covers the areas Web3 developers should review before putting a smart contract into production.&lt;/p&gt;




&lt;h2&gt;
  
  
  Smart Contract Security Checklist
&lt;/h2&gt;

&lt;p&gt;Before deployment, make sure you can answer "yes" to the following questions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Are all privileged functions properly protected?&lt;/li&gt;
&lt;li&gt;[ ] Can an attacker manipulate an oracle or price source?&lt;/li&gt;
&lt;li&gt;[ ] Are external calls handled safely?&lt;/li&gt;
&lt;li&gt;[ ] Have reentrancy paths been considered?&lt;/li&gt;
&lt;li&gt;[ ] Are token balances and accounting calculated correctly?&lt;/li&gt;
&lt;li&gt;[ ] Are ERC-20 edge cases handled?&lt;/li&gt;
&lt;li&gt;[ ] Are flash-loan-assisted attacks possible?&lt;/li&gt;
&lt;li&gt;[ ] Are upgrade and initialization functions protected?&lt;/li&gt;
&lt;li&gt;[ ] Are signatures and replay protection implemented correctly?&lt;/li&gt;
&lt;li&gt;[ ] Are emergency functions restricted and tested?&lt;/li&gt;
&lt;li&gt;[ ] Have important invariants been tested?&lt;/li&gt;
&lt;li&gt;[ ] Has the contract been tested with malicious inputs?&lt;/li&gt;
&lt;li&gt;[ ] Have dependencies and compiler versions been reviewed?&lt;/li&gt;
&lt;li&gt;[ ] Is the deployment configuration secure?&lt;/li&gt;
&lt;li&gt;[ ] Is there a monitoring and incident-response plan?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The sections below explain why each item matters.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Review Access Control First
&lt;/h2&gt;

&lt;p&gt;Access control is one of the highest-impact areas in smart contract security.&lt;/p&gt;

&lt;p&gt;OWASP's 2025 Smart Contract Top 10 places &lt;strong&gt;Access Control Vulnerabilities at #1&lt;/strong&gt;. Its analysis of 2024 incidents attributed approximately &lt;strong&gt;$953.2 million in losses&lt;/strong&gt; to access-control vulnerabilities.&lt;/p&gt;

&lt;p&gt;That makes permission checks one of the first things developers should review.&lt;/p&gt;

&lt;p&gt;Start by identifying every function that can change important protocol state.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function mint(address to, uint256 amount) external {
    _mint(to, amount);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This may look harmless, but if &lt;code&gt;mint()&lt;/code&gt; is supposed to be an administrative operation, anyone can call it.&lt;/p&gt;

&lt;p&gt;A safer implementation might be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function mint(address to, uint256 amount)
    external
    onlyRole(MINTER_ROLE)
{
    _mint(to, amount);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But adding &lt;code&gt;onlyRole()&lt;/code&gt; is not the end of the review.&lt;/p&gt;

&lt;p&gt;You also need to understand:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who receives the role?&lt;/li&gt;
&lt;li&gt;Who can grant the role?&lt;/li&gt;
&lt;li&gt;Who can revoke it?&lt;/li&gt;
&lt;li&gt;Can the role administrator be changed?&lt;/li&gt;
&lt;li&gt;Can the owner replace the role manager?&lt;/li&gt;
&lt;li&gt;Can an upgrade bypass the access-control system?&lt;/li&gt;
&lt;li&gt;Is the privileged account an EOA or multisig?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Ethereum's security documentation recommends carefully designing access control and notes that role-based access control and multisig administration can reduce single points of failure.&lt;/p&gt;

&lt;h3&gt;
  
  
  Access-control checklist
&lt;/h3&gt;

&lt;p&gt;Review every function that can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Mint or burn tokens&lt;/li&gt;
&lt;li&gt;Transfer protocol-owned funds&lt;/li&gt;
&lt;li&gt;Change fees&lt;/li&gt;
&lt;li&gt;Change oracle addresses&lt;/li&gt;
&lt;li&gt;Change implementation contracts&lt;/li&gt;
&lt;li&gt;Pause or unpause the protocol&lt;/li&gt;
&lt;li&gt;Change collateral parameters&lt;/li&gt;
&lt;li&gt;Change liquidation parameters&lt;/li&gt;
&lt;li&gt;Modify trusted contracts&lt;/li&gt;
&lt;li&gt;Withdraw accumulated fees&lt;/li&gt;
&lt;li&gt;Upgrade contracts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A useful audit question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What happens if an attacker obtains this role?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If the answer is "the attacker can drain the protocol", that role deserves particularly strong protection.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Check Upgradeability Carefully
&lt;/h2&gt;

&lt;p&gt;Upgradeable contracts introduce another layer of security assumptions.&lt;/p&gt;

&lt;p&gt;A proxy system may separate:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;User
  ↓
Proxy
  ↓
Implementation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That means the security of the application depends not only on the implementation contract but also on the proxy and upgrade mechanism.&lt;/p&gt;

&lt;p&gt;Check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Who controls the proxy?&lt;/li&gt;
&lt;li&gt;Who can upgrade the implementation?&lt;/li&gt;
&lt;li&gt;Is the upgrade authority a multisig?&lt;/li&gt;
&lt;li&gt;Is there a timelock?&lt;/li&gt;
&lt;li&gt;Can the implementation be initialized again?&lt;/li&gt;
&lt;li&gt;Can an attacker call initialization directly?&lt;/li&gt;
&lt;li&gt;Are storage layouts compatible?&lt;/li&gt;
&lt;li&gt;Can an upgrade change critical permissions?&lt;/li&gt;
&lt;li&gt;Is there an emergency upgrade mechanism?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A common mistake is focusing only on the business logic while ignoring the upgrade path.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function upgradeTo(address newImplementation)
    external
{
    implementation = newImplementation;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this function is not properly protected, the entire security model of the protocol can collapse.&lt;/p&gt;

&lt;p&gt;OpenZeppelin specifically recommends treating upgrade mechanisms and administrative privileges as important parts of the security architecture.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Protect Initializers
&lt;/h2&gt;

&lt;p&gt;Upgradeable contracts often use an initializer instead of a constructor.&lt;/p&gt;

&lt;p&gt;For example:&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 admin) external {
    owner = admin;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If this function can be called more than once, an attacker may be able to take control of the contract.&lt;/p&gt;

&lt;p&gt;A common pattern is:&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 admin)
    external
    initializer
{
    owner = admin;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But developers should still verify the complete initialization flow.&lt;/p&gt;

&lt;p&gt;Check:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can initialization happen twice?&lt;/li&gt;
&lt;li&gt;Can an attacker initialize an implementation contract?&lt;/li&gt;
&lt;li&gt;Is the proxy initialized immediately after deployment?&lt;/li&gt;
&lt;li&gt;Are all inherited contracts initialized?&lt;/li&gt;
&lt;li&gt;Can initialization be front-run?&lt;/li&gt;
&lt;li&gt;Are initialization parameters validated?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Initialization bugs are especially dangerous because they can effectively turn deployment into an ownership race.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Review Reentrancy
&lt;/h2&gt;

&lt;p&gt;Reentrancy is one of the classic smart contract vulnerabilities, and it remains relevant today.&lt;/p&gt;

&lt;p&gt;The basic problem occurs when a contract makes an external call before updating its own state.&lt;/p&gt;

&lt;p&gt;Consider:&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 amount) external {
    require(balance[msg.sender] &amp;gt;= amount);

    payable(msg.sender).call{value: amount}("");

    balance[msg.sender] -= amount;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An attacker-controlled contract can potentially call &lt;code&gt;withdraw()&lt;/code&gt; again before the balance is updated.&lt;/p&gt;

&lt;p&gt;The safer pattern is generally:&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 amount) external {
    require(balance[msg.sender] &amp;gt;= amount);

    balance[msg.sender] -= amount;

    (bool success, ) =
        payable(msg.sender).call{value: amount}("");

    require(success);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This follows the &lt;strong&gt;Checks-Effects-Interactions&lt;/strong&gt; pattern.&lt;/p&gt;

&lt;p&gt;Ethereum's official security documentation still recommends this approach when dealing with external calls and reentrancy.&lt;/p&gt;

&lt;h3&gt;
  
  
  But don't only search for &lt;code&gt;call()&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;Modern reentrancy reviews should also consider:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;ERC-777 hooks&lt;/li&gt;
&lt;li&gt;ERC-721 callbacks&lt;/li&gt;
&lt;li&gt;ERC-1155 callbacks&lt;/li&gt;
&lt;li&gt;ERC-4626 interactions&lt;/li&gt;
&lt;li&gt;Flash-loan callbacks&lt;/li&gt;
&lt;li&gt;DEX callbacks&lt;/li&gt;
&lt;li&gt;Cross-contract calls&lt;/li&gt;
&lt;li&gt;Read-only reentrancy&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A contract does not need to explicitly call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;msg.sender.call(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;to have a reentrancy problem.&lt;/p&gt;

&lt;p&gt;The important question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can control flow leave the current contract before critical state is finalized?"&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  5. Review External Calls
&lt;/h2&gt;

&lt;p&gt;External calls are trust boundaries.&lt;/p&gt;

&lt;p&gt;Whenever your contract interacts with another contract, ask:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;What contract am I calling?
Who controls it?
What can it call back?
What assumptions am I making about its return value?
Can it revert?
Can it consume excessive gas?
Can it manipulate state before returning?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;token.transfer(user, amount);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;is not necessarily equivalent to:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;SafeERC20.safeTransfer(token, user, amount);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ERC-20 implementations are not always perfectly consistent.&lt;/p&gt;

&lt;p&gt;Some tokens return &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Some return no value.&lt;/p&gt;

&lt;p&gt;Some have unusual transfer behavior.&lt;/p&gt;

&lt;p&gt;Using battle-tested libraries reduces the amount of token-specific behavior your protocol needs to handle itself. Ethereum's security guidance recommends using well-tested libraries rather than repeatedly implementing common primitives from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  6. Don't Assume Every ERC-20 Behaves the Same
&lt;/h2&gt;

&lt;p&gt;One of the most common mistakes in DeFi development is treating ERC-20 as if every token follows exactly the same behavior.&lt;/p&gt;

&lt;p&gt;Your contract may interact with tokens that have:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Fee-on-transfer mechanics&lt;/li&gt;
&lt;li&gt;Rebasing&lt;/li&gt;
&lt;li&gt;Transfer restrictions&lt;/li&gt;
&lt;li&gt;Blacklists&lt;/li&gt;
&lt;li&gt;Pausable transfers&lt;/li&gt;
&lt;li&gt;Non-standard return values&lt;/li&gt;
&lt;li&gt;Decimals other than 18&lt;/li&gt;
&lt;li&gt;Upgradeable implementations&lt;/li&gt;
&lt;li&gt;Callback-like behavior&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Consider:&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.transferFrom(
    msg.sender,
    address(this),
    amount
);

uint256 received = amount;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The contract assumes it received exactly &lt;code&gt;amount&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That assumption may not always hold.&lt;/p&gt;

&lt;p&gt;A safer accounting model may measure the actual balance difference:&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;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whether this approach is appropriate depends on the protocol's intended token model, but the important point is to make the assumption explicit.&lt;/p&gt;




&lt;h2&gt;
  
  
  7. Check Oracle Security
&lt;/h2&gt;

&lt;p&gt;Oracle manipulation has become one of the most important DeFi attack surfaces.&lt;/p&gt;

&lt;p&gt;OWASP's 2025 Smart Contract Top 10 ranks &lt;strong&gt;Price Oracle Manipulation #2&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Any protocol using prices should answer:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Where does the price come from?&lt;/li&gt;
&lt;li&gt;Can users influence the price?&lt;/li&gt;
&lt;li&gt;Is the source based on a single pool?&lt;/li&gt;
&lt;li&gt;What liquidity does the pool have?&lt;/li&gt;
&lt;li&gt;Is the price based on spot or historical data?&lt;/li&gt;
&lt;li&gt;Is a TWAP being used?&lt;/li&gt;
&lt;li&gt;What happens when the oracle becomes stale?&lt;/li&gt;
&lt;li&gt;What happens if the oracle returns zero?&lt;/li&gt;
&lt;li&gt;What happens if the price moves significantly in one block?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A dangerous pattern is relying directly on a low-liquidity AMM spot price.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uint256 price =
    reserve1 * 1e18 / reserve0;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an attacker can temporarily move the reserves, the protocol may use a manipulated price.&lt;/p&gt;

&lt;p&gt;The problem becomes much more serious when the manipulated price controls:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Borrowing capacity&lt;/li&gt;
&lt;li&gt;Liquidation prices&lt;/li&gt;
&lt;li&gt;Collateral valuation&lt;/li&gt;
&lt;li&gt;Share minting&lt;/li&gt;
&lt;li&gt;Redemption amounts&lt;/li&gt;
&lt;li&gt;LP token valuation&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  8. Review Flash-Loan Attack Paths
&lt;/h2&gt;

&lt;p&gt;Flash loans do not create vulnerabilities by themselves.&lt;/p&gt;

&lt;p&gt;They make existing vulnerabilities much easier to exploit.&lt;/p&gt;

&lt;p&gt;An attacker can borrow a large amount of capital, manipulate temporary protocol state, execute an exploit, and repay the loan within the same transaction.&lt;/p&gt;

&lt;p&gt;When reviewing a DeFi protocol, ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"What can an attacker do if they temporarily have $100 million?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Then test:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Price manipulation&lt;/li&gt;
&lt;li&gt;Governance voting&lt;/li&gt;
&lt;li&gt;Collateral deposits&lt;/li&gt;
&lt;li&gt;Liquidations&lt;/li&gt;
&lt;li&gt;Reward calculations&lt;/li&gt;
&lt;li&gt;Share-price manipulation&lt;/li&gt;
&lt;li&gt;LP token pricing&lt;/li&gt;
&lt;li&gt;Reserve manipulation&lt;/li&gt;
&lt;li&gt;Temporary liquidity attacks&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OWASP includes &lt;strong&gt;Flash Loan Attacks&lt;/strong&gt; in its 2025 Smart Contract Top 10.&lt;/p&gt;

&lt;p&gt;The important thing is not simply checking whether the protocol supports flash loans.&lt;/p&gt;

&lt;p&gt;The real question is whether protocol logic incorrectly assumes that capital must come from the attacker's own wallet.&lt;/p&gt;




&lt;h2&gt;
  
  
  9. Check Accounting Invariants
&lt;/h2&gt;

&lt;p&gt;Many serious DeFi vulnerabilities are not obvious "Solidity bugs".&lt;/p&gt;

&lt;p&gt;They are accounting bugs.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;shares = assets * totalShares / totalAssets;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;may look mathematically correct.&lt;/p&gt;

&lt;p&gt;But what happens when:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;totalAssets = 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;totalShares = 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;assets * totalShares
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;overflows?&lt;/p&gt;

&lt;p&gt;Even when Solidity prevents arithmetic overflow, incorrect ordering and rounding can still create exploitable economic behavior.&lt;/p&gt;

&lt;p&gt;For every financial function, define the invariant first.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total user balances &amp;lt;= protocol assets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Shares minted &amp;lt;= proportional contribution
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Debt cannot become negative
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then test whether an attacker can violate the invariant.&lt;/p&gt;

&lt;p&gt;This approach is often more productive than simply searching for known vulnerability patterns.&lt;/p&gt;




&lt;h2&gt;
  
  
  10. Pay Special Attention to ERC-4626 Vaults
&lt;/h2&gt;

&lt;p&gt;ERC-4626 vaults deserve their own checklist.&lt;/p&gt;

&lt;p&gt;Review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deposit()
mint()
withdraw()
redeem()
previewDeposit()
previewMint()
previewWithdraw()
previewRedeem()
convertToShares()
convertToAssets()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then compare the accounting between these functions.&lt;/p&gt;

&lt;p&gt;Important questions include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Can the first depositor manipulate the share price?&lt;/li&gt;
&lt;li&gt;Can donations change the exchange rate?&lt;/li&gt;
&lt;li&gt;Are previews consistent with execution?&lt;/li&gt;
&lt;li&gt;Are rounding directions correct?&lt;/li&gt;
&lt;li&gt;Can assets be lost due to rounding?&lt;/li&gt;
&lt;li&gt;Are fees included consistently?&lt;/li&gt;
&lt;li&gt;Can &lt;code&gt;totalAssets()&lt;/code&gt; be manipulated?&lt;/li&gt;
&lt;li&gt;Can external yield change the accounting unexpectedly?&lt;/li&gt;
&lt;li&gt;Are pending withdrawals handled correctly?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For vaults, it is often useful to test sequences rather than individual calls:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deposit
→ donate
→ deposit
→ withdraw
→ redeem
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Many accounting vulnerabilities only appear after several state transitions.&lt;/p&gt;




&lt;h2&gt;
  
  
  11. Validate All User Inputs
&lt;/h2&gt;

&lt;p&gt;Never assume users will provide reasonable parameters.&lt;/p&gt;

&lt;p&gt;Check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require(amount &amp;gt; 0);
require(receiver != address(0));
require(deadline &amp;gt;= block.timestamp);
require(price &amp;gt; 0);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;depending on the application.&lt;/p&gt;

&lt;p&gt;Look for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero addresses&lt;/li&gt;
&lt;li&gt;Zero amounts&lt;/li&gt;
&lt;li&gt;Extremely large values&lt;/li&gt;
&lt;li&gt;Duplicate IDs&lt;/li&gt;
&lt;li&gt;Invalid enum values&lt;/li&gt;
&lt;li&gt;Expired deadlines&lt;/li&gt;
&lt;li&gt;Invalid signatures&lt;/li&gt;
&lt;li&gt;Invalid array lengths&lt;/li&gt;
&lt;li&gt;Unexpected array ordering&lt;/li&gt;
&lt;li&gt;Unsupported tokens&lt;/li&gt;
&lt;li&gt;Unsupported chains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OWASP's 2025 ranking places &lt;strong&gt;Lack of Input Validation at #4&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A good rule is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Validate assumptions at the boundary where the value enters the system.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Don't allow invalid state to propagate through several contracts and only fail later.&lt;/p&gt;




&lt;h2&gt;
  
  
  12. Check Signature Verification
&lt;/h2&gt;

&lt;p&gt;Protocols using off-chain signatures should review:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Who signed?
What exactly was signed?
Which chain?
Which contract?
What nonce?
What deadline?
Can the signature be replayed?
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For example, a signed message should generally bind itself to the intended domain.&lt;/p&gt;

&lt;p&gt;Important parameters often include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;chainId
verifyingContract
nonce
deadline
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Without replay protection, a valid signature can potentially be reused.&lt;/p&gt;

&lt;p&gt;Also review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;EIP-712 domain separation&lt;/li&gt;
&lt;li&gt;ERC-1271 contract signatures&lt;/li&gt;
&lt;li&gt;Signature malleability&lt;/li&gt;
&lt;li&gt;Nonce management&lt;/li&gt;
&lt;li&gt;Deadline handling&lt;/li&gt;
&lt;li&gt;Cross-chain replay&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Do not assume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ecrecover(...)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;alone provides a complete signature-security model.&lt;/p&gt;




&lt;h2&gt;
  
  
  13. Be Careful With &lt;code&gt;tx.origin&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Avoid using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require(tx.origin == owner);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;for authorization.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;tx.origin&lt;/code&gt; represents the original transaction initiator, not necessarily the contract directly interacting with your contract.&lt;/p&gt;

&lt;p&gt;For access control, &lt;code&gt;msg.sender&lt;/code&gt; is normally the appropriate primitive:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;require(msg.sender == owner);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even then, the overall permission architecture still needs to be reviewed.&lt;/p&gt;




&lt;h2&gt;
  
  
  14. Review Denial-of-Service Risks
&lt;/h2&gt;

&lt;p&gt;Security is not only about stealing money.&lt;/p&gt;

&lt;p&gt;An attacker may also make an important function unusable.&lt;/p&gt;

&lt;p&gt;Common examples include:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;for (uint256 i = 0; i &amp;lt; users.length; i++) {
    ...
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;users.length&lt;/code&gt; can grow without bounds, eventually the function may exceed the block gas limit.&lt;/p&gt;

&lt;p&gt;Review:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Unbounded loops&lt;/li&gt;
&lt;li&gt;Large arrays&lt;/li&gt;
&lt;li&gt;User-controlled iteration&lt;/li&gt;
&lt;li&gt;Failed external calls&lt;/li&gt;
&lt;li&gt;DoS through reverting receivers&lt;/li&gt;
&lt;li&gt;Forced ETH&lt;/li&gt;
&lt;li&gt;Storage growth&lt;/li&gt;
&lt;li&gt;Queue processing&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;OWASP includes &lt;strong&gt;Denial of Service&lt;/strong&gt; in its smart contract security categories.&lt;/p&gt;




&lt;h2&gt;
  
  
  15. Don't Ignore &lt;code&gt;selfdestruct&lt;/code&gt;, ETH Transfers, and Forced ETH
&lt;/h2&gt;

&lt;p&gt;Developers sometimes assume:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;address(this).balance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;exactly represents the ETH deposited through the protocol.&lt;/p&gt;

&lt;p&gt;That assumption can be wrong.&lt;/p&gt;

&lt;p&gt;ETH can be forced into a contract through mechanisms outside the normal deposit flow.&lt;/p&gt;

&lt;p&gt;Therefore, accounting should generally not depend blindly on:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;address(this).balance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;when exact internal accounting is required.&lt;/p&gt;

&lt;p&gt;Instead, distinguish between:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Protocol accounting
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Actual blockchain balance
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and explicitly decide how unexpected funds should be handled.&lt;/p&gt;




&lt;h2&gt;
  
  
  16. Review Compiler and Dependency Versions
&lt;/h2&gt;

&lt;p&gt;The Solidity version matters.&lt;/p&gt;

&lt;p&gt;Before deployment, check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Solidity version
OpenZeppelin version
External libraries
Compiler settings
Optimizer settings
Proxy tooling
Build configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Do not blindly copy an old contract and change:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma solidity ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;without understanding the differences between compiler versions.&lt;/p&gt;

&lt;p&gt;Also avoid unnecessary custom implementations of common primitives.&lt;/p&gt;

&lt;p&gt;For example, if a well-tested library already provides:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ERC20
AccessControl
SafeERC20
ReentrancyGuard
ECDSA
Math
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;there is usually little value in rewriting them unless there is a specific reason.&lt;/p&gt;

&lt;p&gt;Ethereum's security guidance similarly recommends well-tested libraries and dependency management.&lt;/p&gt;




&lt;h2&gt;
  
  
  17. Write Tests for Attackers, Not Just Users
&lt;/h2&gt;

&lt;p&gt;A normal unit test might look like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Alice deposits 100 ETH
Alice withdraws 100 ETH
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That proves the happy path works.&lt;/p&gt;

&lt;p&gt;Security tests should look more like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Attacker deposits 1 wei
Attacker manipulates price
Attacker calls function twice
Attacker uses a flash loan
Attacker passes address(0)
Attacker reenters callback
Attacker uses an expired signature
Attacker calls function from an unauthorized account
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is important.&lt;/p&gt;

&lt;p&gt;A good security test suite should deliberately try to break the protocol.&lt;/p&gt;




&lt;h2&gt;
  
  
  18. Test Invariants and Properties
&lt;/h2&gt;

&lt;p&gt;Unit tests answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Does this example work?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Invariant tests answer:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can this important property ever become false?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sum(userBalances) &amp;lt;= totalAssets
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;totalSupply cannot decrease unexpectedly
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;only authorized addresses can modify protocol parameters
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Tools such as fuzzing and invariant testing can explore many combinations that manually written test cases will never cover.&lt;/p&gt;

&lt;p&gt;Ethereum's security guidance specifically recommends extensive testing and tools such as Slither, Echidna, and Manticore as part of the security process.&lt;/p&gt;




&lt;h2&gt;
  
  
  19. Run Static Analysis — But Don't Stop There
&lt;/h2&gt;

&lt;p&gt;Static analyzers are useful for catching common patterns.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;slither &lt;span class="nb"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;can identify issues involving:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Reentrancy&lt;/li&gt;
&lt;li&gt;Dangerous calls&lt;/li&gt;
&lt;li&gt;Access control&lt;/li&gt;
&lt;li&gt;Unused return values&lt;/li&gt;
&lt;li&gt;Shadowing&lt;/li&gt;
&lt;li&gt;Incorrect Solidity patterns&lt;/li&gt;
&lt;li&gt;Uninitialized variables&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But a clean Slither report does &lt;strong&gt;not&lt;/strong&gt; mean the protocol is secure.&lt;/p&gt;

&lt;p&gt;Static analysis is much better at finding:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;known code patterns
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;than:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;incorrect business logic
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A contract can pass static analysis and still have a critical economic vulnerability.&lt;/p&gt;




&lt;h2&gt;
  
  
  20. Perform Manual State-Transition Analysis
&lt;/h2&gt;

&lt;p&gt;This is where experienced security review becomes especially valuable.&lt;/p&gt;

&lt;p&gt;Instead of reading functions independently, follow how state changes.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deposit()
    ↓
mint shares
    ↓
accrue yield
    ↓
borrow()
    ↓
liquidate()
    ↓
repay()
    ↓
withdraw()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;"Can I reach an unexpected state by combining these functions?"&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Many vulnerabilities exist not inside one function, but in the interaction between several apparently safe functions.&lt;/p&gt;

&lt;p&gt;This is especially important for:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lending protocols&lt;/li&gt;
&lt;li&gt;Staking systems&lt;/li&gt;
&lt;li&gt;Vaults&lt;/li&gt;
&lt;li&gt;AMMs&lt;/li&gt;
&lt;li&gt;Restaking protocols&lt;/li&gt;
&lt;li&gt;Governance systems&lt;/li&gt;
&lt;li&gt;Cross-chain bridges&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  21. Review Deployment Configuration
&lt;/h2&gt;

&lt;p&gt;A secure contract can still be deployed insecurely.&lt;/p&gt;

&lt;p&gt;Before mainnet deployment, verify:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Correct chain ID&lt;/li&gt;
&lt;li&gt;Correct token addresses&lt;/li&gt;
&lt;li&gt;Correct oracle addresses&lt;/li&gt;
&lt;li&gt;Correct admin address&lt;/li&gt;
&lt;li&gt;Correct proxy implementation&lt;/li&gt;
&lt;li&gt;Correct initialization parameters&lt;/li&gt;
&lt;li&gt;Correct fee configuration&lt;/li&gt;
&lt;li&gt;Correct multisig&lt;/li&gt;
&lt;li&gt;Correct ownership transfer&lt;/li&gt;
&lt;li&gt;Correct pause configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A surprisingly common failure mode is deploying correct code with incorrect parameters.&lt;/p&gt;

&lt;p&gt;Create a deployment checklist and verify every address before sending the transaction.&lt;/p&gt;




&lt;h2&gt;
  
  
  22. Verify Contracts on the Block Explorer
&lt;/h2&gt;

&lt;p&gt;After deployment:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Deploy
↓
Initialize
↓
Verify source
↓
Check ownership
↓
Check implementation
↓
Check roles
↓
Check configuration
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Source verification makes the deployed bytecode easier to inspect and allows other researchers to review the actual production contract.&lt;/p&gt;

&lt;p&gt;Do not assume the deployment script succeeded simply because the transaction succeeded.&lt;/p&gt;

&lt;p&gt;Verify the resulting state on-chain.&lt;/p&gt;




&lt;h2&gt;
  
  
  23. Monitor the Contract After Deployment
&lt;/h2&gt;

&lt;p&gt;Security does not end when the contract is deployed.&lt;/p&gt;

&lt;p&gt;Monitor events and important state changes such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;OwnershipTransferred
RoleGranted
RoleRevoked
Upgraded
Paused
Unpaused
OracleUpdated
FeeUpdated
Large withdrawals
Large deposits
Unexpected transfers
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ethereum's security guidance also recommends monitoring deployed contracts and having an incident-response plan.&lt;/p&gt;

&lt;p&gt;A vulnerability discovered quickly is much easier to contain than one discovered after an attacker has drained the protocol.&lt;/p&gt;




&lt;h2&gt;
  
  
  24. Keep an Emergency Response Plan
&lt;/h2&gt;

&lt;p&gt;Every protocol handling meaningful value should know what happens after an exploit.&lt;/p&gt;

&lt;p&gt;For example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Suspicious transaction
        ↓
Alert
        ↓
Security team investigation
        ↓
Pause if appropriate
        ↓
Identify affected contracts
        ↓
Assess attacker access
        ↓
Contact infrastructure / exchanges / security teams
        ↓
Prepare remediation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact process depends on the protocol.&lt;/p&gt;

&lt;p&gt;The important thing is to decide &lt;strong&gt;before&lt;/strong&gt; the incident happens.&lt;/p&gt;




&lt;h2&gt;
  
  
  A Practical Pre-Deployment Checklist
&lt;/h2&gt;

&lt;p&gt;Here is a shorter version you can keep next to your codebase.&lt;/p&gt;

&lt;h2&gt;
  
  
  Architecture
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Attack surface documented&lt;/li&gt;
&lt;li&gt;[ ] Trust assumptions documented&lt;/li&gt;
&lt;li&gt;[ ] External dependencies identified&lt;/li&gt;
&lt;li&gt;[ ] Upgrade architecture reviewed&lt;/li&gt;
&lt;li&gt;[ ] Admin architecture reviewed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Access Control
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] All privileged functions identified&lt;/li&gt;
&lt;li&gt;[ ] &lt;code&gt;onlyOwner&lt;/code&gt; / roles verified&lt;/li&gt;
&lt;li&gt;[ ] Role administrators reviewed&lt;/li&gt;
&lt;li&gt;[ ] Ownership transfer tested&lt;/li&gt;
&lt;li&gt;[ ] Multisig configuration verified&lt;/li&gt;
&lt;li&gt;[ ] Timelock considered for sensitive operations&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  DeFi Logic
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Oracle manipulation tested&lt;/li&gt;
&lt;li&gt;[ ] Flash-loan attack paths tested&lt;/li&gt;
&lt;li&gt;[ ] Liquidation logic tested&lt;/li&gt;
&lt;li&gt;[ ] Interest calculations tested&lt;/li&gt;
&lt;li&gt;[ ] Share accounting tested&lt;/li&gt;
&lt;li&gt;[ ] Rounding reviewed&lt;/li&gt;
&lt;li&gt;[ ] Donation attacks considered&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Token Handling
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] ERC-20 return values handled&lt;/li&gt;
&lt;li&gt;[ ] Fee-on-transfer behavior considered&lt;/li&gt;
&lt;li&gt;[ ] Rebasing tokens considered&lt;/li&gt;
&lt;li&gt;[ ] Decimals handled correctly&lt;/li&gt;
&lt;li&gt;[ ] Zero address checks implemented&lt;/li&gt;
&lt;li&gt;[ ] Token approval flow reviewed&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  External Calls
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Reentrancy reviewed&lt;/li&gt;
&lt;li&gt;[ ] Callbacks reviewed&lt;/li&gt;
&lt;li&gt;[ ] Return values checked&lt;/li&gt;
&lt;li&gt;[ ] Untrusted contracts identified&lt;/li&gt;
&lt;li&gt;[ ] DoS through external calls considered&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Upgradeability
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Proxy admin secured&lt;/li&gt;
&lt;li&gt;[ ] Implementation verified&lt;/li&gt;
&lt;li&gt;[ ] Initializer protected&lt;/li&gt;
&lt;li&gt;[ ] Storage layout checked&lt;/li&gt;
&lt;li&gt;[ ] Upgrade authorization tested&lt;/li&gt;
&lt;li&gt;[ ] Upgrade events monitored&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Testing
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Unit tests&lt;/li&gt;
&lt;li&gt;[ ] Fuzz tests&lt;/li&gt;
&lt;li&gt;[ ] Invariant tests&lt;/li&gt;
&lt;li&gt;[ ] Negative tests&lt;/li&gt;
&lt;li&gt;[ ] Access-control tests&lt;/li&gt;
&lt;li&gt;[ ] Reentrancy tests&lt;/li&gt;
&lt;li&gt;[ ] Economic attack tests&lt;/li&gt;
&lt;li&gt;[ ] Static analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Deployment
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;[ ] Correct chain&lt;/li&gt;
&lt;li&gt;[ ] Correct addresses&lt;/li&gt;
&lt;li&gt;[ ] Correct oracle&lt;/li&gt;
&lt;li&gt;[ ] Correct admin&lt;/li&gt;
&lt;li&gt;[ ] Correct initialization&lt;/li&gt;
&lt;li&gt;[ ] Contract verified&lt;/li&gt;
&lt;li&gt;[ ] Roles verified&lt;/li&gt;
&lt;li&gt;[ ] Monitoring enabled&lt;/li&gt;
&lt;li&gt;[ ] Incident response prepared&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The Most Important Part of Smart Contract Security
&lt;/h2&gt;

&lt;p&gt;A checklist is useful, but it should not become a substitute for understanding the protocol.&lt;/p&gt;

&lt;p&gt;The most dangerous bugs are often not obvious Solidity mistakes.&lt;/p&gt;

&lt;p&gt;They are usually assumptions such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"This price cannot be manipulated."

"This function can only be called by the frontend."

"The token always transfers exactly the requested amount."

"Nobody can borrow enough money to do this."

"The owner would never abuse this function."

"This callback cannot reenter."

"This value can never be zero."

"This contract will always be initialized correctly."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An attacker does not need to respect these assumptions.&lt;/p&gt;

&lt;p&gt;They only need to find one that the code fails to enforce.&lt;/p&gt;

&lt;p&gt;That is why a strong smart contract security review combines automated analysis, adversarial testing, economic reasoning, and manual code review.&lt;/p&gt;




&lt;h2&gt;
  
  
  Final Thoughts
&lt;/h2&gt;

&lt;p&gt;Smart contract security is not a single step at the end of development.&lt;/p&gt;

&lt;p&gt;It starts with architecture, continues through implementation and testing, and remains important after deployment.&lt;/p&gt;

&lt;p&gt;The current security data makes that clear. OWASP's 2025 Smart Contract Top 10 puts access control, oracle manipulation, logic errors, input validation, and reentrancy among the major smart contract risks.&lt;/p&gt;

&lt;p&gt;For developers, the practical approach is straightforward:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Define the security assumptions.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn those assumptions into invariants.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Try to break those invariants.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Then verify the implementation and deployment configuration.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;A smart contract should not be considered secure simply because it compiles, passes unit tests, or has no obvious static-analysis warnings.&lt;/p&gt;

&lt;p&gt;The real question is much harder:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Can an untrusted user make the protocol enter a state that its designers never intended?&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  That is the question a good security review should answer.
&lt;/h2&gt;

&lt;h2&gt;
  
  
  References
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;OWASP Smart Contract Top 10 — 2025&lt;/li&gt;
&lt;li&gt;Ethereum Foundation — Smart Contract Security&lt;/li&gt;
&lt;li&gt;OpenZeppelin Contracts — Access Control&lt;/li&gt;
&lt;li&gt;Immunefi — Crypto Losses 2024 Report&lt;/li&gt;
&lt;li&gt;CertiK — Hack3d Web3 Security Report 2025&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>security</category>
      <category>solidity</category>
      <category>web3</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Inside the Likwid Exploit: One Missing pairDelta, 74 BNB Gone</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Fri, 16 Oct 2026 01:32:01 +0000</pubDate>
      <link>https://dev.to/pandasecurity/inside-the-likwid-exploit-one-missing-pairdelta-74-bnb-gone-oha</link>
      <guid>https://dev.to/pandasecurity/inside-the-likwid-exploit-one-missing-pairdelta-74-bnb-gone-oha</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/one-missing-pairdelta/" rel="noopener noreferrer"&gt;Inside the Likwid Exploit: One Missing pairDelta, 74 BNB Gone&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Another day, another nine-figure headline — except this one wasn't nine figures. Likwid lost &lt;strong&gt;74.31 BNB&lt;/strong&gt;, which is small by exploit standards, and that's exactly what makes it worth studying. This wasn't a flash-loan-oracle masterpiece or a governance takeover. It was &lt;strong&gt;one struct field that nobody assigned&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;At Panda Security, we keep saying the same thing: the most expensive bugs are rarely the cleverest ones. They're the boring ones that sit in plain sight because everyone assumes someone else already checked. Likwid is case in point.&lt;/p&gt;

&lt;p&gt;Let's break it down.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Likwid Actually Is
&lt;/h2&gt;

&lt;p&gt;Before we talk about the bug, it helps to understand the machine. Likwid runs a leveraged margin trading system on BNB Chain, split into two core contracts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;LikwidVault&lt;/strong&gt; — the central ledger. It custodies all tokens and tracks several different "reserves" per pool:

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;realReserves&lt;/code&gt; — the actual tokens held.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;pairReserves&lt;/code&gt; — the AMM pair reserves, used for pricing swaps.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;mirrorReserves&lt;/code&gt; — the borrow/demand side.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;lendReserves&lt;/code&gt; — the lend/collateral supply side.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;truncatedReserves&lt;/code&gt; — a smoothed baseline for price movement.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;LikwidMarginPosition&lt;/strong&gt; — the user-facing position manager. Users open leverage, borrow, repay, close, and liquidate here. It doesn't hold funds; it computes a set of deltas and hands them to the vault.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key architectural detail: &lt;strong&gt;the vault doesn't calculate anything about a margin operation on its own.&lt;/strong&gt; The position manager computes a &lt;code&gt;MarginBalanceDelta&lt;/code&gt; and passes it in. The vault applies it. That's a clean separation of concerns — and, as we'll see, a sharp knife.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;struct MarginBalanceDelta {
    MarginActions action;
    bool marginForOne;
    uint128 marginTotal;
    uint256 marginFeeAmount;
    uint256 swapFeeAmount;
    BalanceDelta marginDelta;
    BalanceDelta mirrorDelta;
    BalanceDelta pairDelta;   // &amp;lt;- the field that matters
    BalanceDelta lendDelta;
    BalanceDelta fundsDelta;
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've audited anything in this style — Uniswap v4 hooks, AMM accounting engines, delta-based vaults — alarm bells should already be ringing. When a caller supplies the price-moving deltas, every single one of them has to be present on every path. Miss one and you've built an arbitrage printer.&lt;/p&gt;

&lt;p&gt;Likwid missed one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Attack Path, Short Version
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;The attacker repeatedly called &lt;code&gt;LikwidMarginPosition.margin&lt;/code&gt;, forcing the &lt;strong&gt;leverage=0 (collateral + borrow)&lt;/strong&gt; branch.&lt;/li&gt;
&lt;li&gt;That branch caps the borrow amount with &lt;code&gt;SwapMath.getAmountOut(pairReserves, ...)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;But the borrow &lt;strong&gt;never updates &lt;code&gt;pairReserves&lt;/code&gt;&lt;/strong&gt; — the swap's price impact is never written back to the pool.&lt;/li&gt;
&lt;li&gt;Every one of the 14 cycles received the &lt;strong&gt;same fixed quote (4.7857 BNB)&lt;/strong&gt;: borrowing at the first trade's marginal price, with zero price impact.&lt;/li&gt;
&lt;li&gt;Roughly &lt;strong&gt;211.8M TOKEN&lt;/strong&gt; was settled at that stale price, draining BNB out of &lt;code&gt;realReserves&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Profit: &lt;strong&gt;74.31 BNB&lt;/strong&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The vault's reserve consistency check passed the entire time. Collateral flowed into &lt;code&gt;lendReserves&lt;/code&gt;, debt flowed into &lt;code&gt;mirrorReserves&lt;/code&gt;, and the pair reserves sat flat. On paper, the books balanced.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause: The Leverage=0 Branch Forgot pairDelta
&lt;/h2&gt;

&lt;p&gt;Here is the vulnerable function, essentially verbatim from &lt;code&gt;src/LikwidMarginPosition.sol&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function _executeAddCollateralAndBorrow(
    IMarginPositionManager.MarginParams memory params,
    PoolState memory poolState,
    MarginPosition.State storage position,
    MarginBalanceDelta memory delta,
    uint256 minBorrowLevel
) internal returns (uint256 borrowAmount) {
    (uint256 borrowMaxAmount,) = SwapMath.getAmountOut(
        poolState.pairReserves, poolState.lpFee, !position.marginForOne, params.marginAmount
    );
    if (minBorrowLevel &amp;gt; PerLibrary.ONE_MILLION) {
        borrowMaxAmount = Math.mulDiv(borrowMaxAmount, PerLibrary.ONE_MILLION, minBorrowLevel);
    }
    uint256 borrowRealReserves = poolState.realReserves.reserve01(!position.marginForOne);
    borrowMaxAmount = Math.min(borrowMaxAmount, borrowRealReserves * 20 / 100);
    if (params.borrowAmount == type(uint256).max) params.borrowAmount = borrowMaxAmount.toUint128();
    if (params.borrowAmount &amp;gt; borrowMaxAmount) BorrowTooMuch.selector.revertWith();
    borrowAmount = params.borrowAmount;

    // ... cumulative values, mirror ratio checks, position.update ...

    int128 amount = -params.marginAmount.toInt128();

    if (position.marginForOne) {
        amount1Delta = amount;
        amount0Delta = borrowAmount.toInt128();
        delta.lendDelta = toBalanceDelta(0, amount);
        delta.mirrorDelta = toBalanceDelta(-borrowAmount.toInt128(), 0);
    } else {
        amount0Delta = amount;
        amount1Delta = borrowAmount.toInt128();
        delta.lendDelta = toBalanceDelta(amount, 0);
        delta.mirrorDelta = toBalanceDelta(0, -borrowAmount.toInt128());
    }
    delta.marginDelta = toBalanceDelta(amount0Delta, amount1Delta);
    // NOTE: delta.pairDelta is never assigned here.
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Notice what's set: &lt;code&gt;lendDelta&lt;/code&gt;, &lt;code&gt;mirrorDelta&lt;/code&gt;, &lt;code&gt;marginDelta&lt;/code&gt;. Notice what's missing: &lt;strong&gt;&lt;code&gt;pairDelta&lt;/code&gt;&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;MarginBalanceDelta&lt;/code&gt; is a memory struct, &lt;code&gt;pairDelta&lt;/code&gt; defaults to zero. And the vault does exactly what it's told:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// LikwidVault -&amp;gt; Pool.margin
ReservesLibrary.UpdateParam[] memory deltaParams = new ReservesLibrary.UpdateParam[](4);
deltaParams[0] = ReservesLibrary.UpdateParam(ReservesType.REAL, marginDelta + protocolDelta);
deltaParams[1] = ReservesLibrary.UpdateParam(ReservesType.PAIR, params.pairDelta + protocolDelta);
deltaParams[2] = ReservesLibrary.UpdateParam(ReservesType.LEND, params.lendDelta);
deltaParams[3] = ReservesLibrary.UpdateParam(ReservesType.MIRROR, params.mirrorDelta);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With &lt;code&gt;pairDelta == 0&lt;/code&gt;, the pair reserves never move. The next call to &lt;code&gt;getAmountOut(pairReserves, ...)&lt;/code&gt; reads the exact same reserves and returns the exact same quote. Repeat 14 times. That's the whole exploit.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Tell: Asymmetric Assignment Across Branches
&lt;/h2&gt;

&lt;p&gt;This is the part that makes the bug almost embarrassing — and genuinely instructive.&lt;/p&gt;

&lt;p&gt;Look at the &lt;strong&gt;leverage &amp;gt; 0&lt;/strong&gt; branch in the same file:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (position.marginForOne) {
    delta.marginDelta = toBalanceDelta(0, amount);
    delta.pairDelta = toBalanceDelta(-borrowAmount.toInt128(), marginWithoutFee.toInt128());
    delta.lendDelta = toBalanceDelta(0, lendAmount);
    delta.mirrorDelta = toBalanceDelta(-borrowAmount.toInt128(), 0);
} else {
    delta.marginDelta = toBalanceDelta(amount, 0);
    delta.pairDelta = toBalanceDelta(marginWithoutFee.toInt128(), -borrowAmount.toInt128());
    delta.lendDelta = toBalanceDelta(lendAmount, 0);
    delta.mirrorDelta = toBalanceDelta(0, -borrowAmount.toInt128());
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;_executeAddLeverage&lt;/code&gt; sets &lt;code&gt;pairDelta&lt;/code&gt;. The &lt;code&gt;close&lt;/code&gt;, &lt;code&gt;repay&lt;/code&gt;, and &lt;code&gt;liquidateBurn&lt;/code&gt; paths set &lt;code&gt;pairDelta&lt;/code&gt;. Only &lt;code&gt;_executeAddCollateralAndBorrow&lt;/code&gt; — the leverage=0 case — does not.&lt;/p&gt;

&lt;p&gt;We checked the repository history: this omission existed from at least &lt;strong&gt;October 2025&lt;/strong&gt; through the latest &lt;code&gt;main&lt;/code&gt; (&lt;strong&gt;January 2026&lt;/strong&gt;). It was never a regression or a merge conflict. It was simply never written.&lt;/p&gt;

&lt;p&gt;Our honest take: this is what happens when a codebase grows branch by branch. Each new path copies its neighbors, and the one field that isn't required by the compiler to be set gets silently dropped. Solidity won't warn you. Tests won't warn you unless someone wrote the exact assertion for this exact path. And a delta struct has no invariant baked in that says "if you borrow against pair pricing, you must move pair reserves."&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the Vault's Safety Check Didn't Save It
&lt;/h2&gt;

&lt;p&gt;Likwid's vault does have a consistency check. &lt;code&gt;_updateReservesConsistent&lt;/code&gt; enforces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;realReserves + mirrorReserves == pairReserves + lendReserves - insuranceFunds
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So why didn't it revert? Because the leverage=0 deltas happen to satisfy it. Let's verify the change form for &lt;code&gt;marginForOne == false&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;marginDelta = (-marginAmount, +borrowAmount)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;mirrorDelta = (0, -borrowAmount)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;lendDelta = (-marginAmount, 0)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pairDelta = 0&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The required identity reduces to &lt;code&gt;marginDelta + mirrorDelta == lendDelta&lt;/code&gt;, and:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;(-marginAmount, +borrowAmount) + (0, -borrowAmount) = (-marginAmount, 0) = lendDelta  ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The books balance. The check is doing its job — it's just checking the wrong thing. It verifies that the &lt;em&gt;totals&lt;/em&gt; reconcile, not that the &lt;em&gt;AMM that determines price&lt;/em&gt; was updated. Those are different invariants, and conflating them is a common, dangerous shortcut in delta-based designs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons for Auditors and Builders
&lt;/h2&gt;

&lt;p&gt;We'll say it plainly because it's the whole point of this post:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Every delta field needs a per-path checklist.&lt;/strong&gt; If a struct field influences price, enumerate every function that should set it and diff them against every function that does. This is mechanical, boring, and exactly how you'd have caught Likwid.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Asymmetric branches are red flags.&lt;/strong&gt; When &lt;code&gt;if (leverage &amp;gt; 0)&lt;/code&gt; and the &lt;code&gt;else&lt;/code&gt; branch set different subsets of fields, that asymmetry deserves a written justification. There wasn't one here.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Conservation checks are not price checks.&lt;/strong&gt; A vault that balances to the wei can still be drained if the oracle or AMM reserves it reads are stale. Validate both.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pricing reserves and custody must move atomically.&lt;/strong&gt; Likwid used &lt;code&gt;pairReserves&lt;/code&gt; to quote while letting the actual transfer flow through &lt;code&gt;realReserves&lt;/code&gt;. The moment those decouple, you have an infinite-arbitrage primitive.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust caller-supplied deltas blindly — or, if you must, make them provably complete.&lt;/strong&gt; The vault is only as safe as the weakest manager that can call it.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  How Panda Security Thinks About This
&lt;/h2&gt;

&lt;p&gt;At &lt;strong&gt;Panda Security&lt;/strong&gt;, our reviews don't stop at "does the accounting balance." We build &lt;strong&gt;invariant maps&lt;/strong&gt; for delta-based systems: for each state-changing entry point, we list which reserves must move, then we fuzz the paths that &lt;em&gt;shouldn't&lt;/em&gt; move them and watch for profitable divergence. A single missing assignment like Likwid's is precisely the kind of bug that falls straight out of that process — no exotic tooling, just disciplined enumeration plus a fuzzer pointed at the right invariant.&lt;/p&gt;

&lt;p&gt;We also like to state the uncomfortable truth: audits reduce risk, they don't delete it. Likwid had remediations and audit fixes in its commit history, and this path still slipped through. That's not a reason to skip audits — it's a reason to demand &lt;strong&gt;continuous&lt;/strong&gt; review and on-chain monitoring, not a one-time PDF.&lt;/p&gt;

&lt;h2&gt;
  
  
  Timeline of Addresses
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Attacker EOA: &lt;code&gt;0x90bde1e0bb16b3deeb9d638acf8d01f19fd2f31e&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Attack Contract: &lt;code&gt;0xc63fb27f52ed8d06673c60c3075b2d3bd26cf4aa&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Vulnerable Contract (LikwidMarginPosition): &lt;code&gt;0x6bec0c1dc4898484b7f094566ddf8bc82ed7abe8&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Victim Contract (LikwidVault): &lt;code&gt;0x065d449ec9d139740343990b7e1cf05fa830e4ba&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Closing Thoughts
&lt;/h2&gt;

&lt;p&gt;74 BNB is a rounding error to most funds. But the bug behind it is a masterclass in why "it compiles" and "the books balance" are not security arguments. A struct field defaulted to zero, a branch that forgot to fill it in, and an AMM that priced every borrow off reserves nobody updated. That's it.&lt;/p&gt;

&lt;p&gt;If your protocol moves value with caller-supplied deltas, sit down with a spreadsheet today and map every field to every code path. The alternative is writing this exact post-mortem about your own contract — and Panda Security would rather help you before that happens than after.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Found this useful? Follow Panda Security for more Web3 incident breakdowns and practical smart contract audit write-ups.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>defi</category>
      <category>security</category>
      <category>solidity</category>
      <category>blockchain</category>
    </item>
    <item>
      <title>Uniswap v4 Hooks: How They Work and Where They Break</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Fri, 16 Oct 2026 01:31:28 +0000</pubDate>
      <link>https://dev.to/pandasecurity/uniswap-v4-hooks-how-they-work-and-where-they-break-44ae</link>
      <guid>https://dev.to/pandasecurity/uniswap-v4-hooks-how-they-work-and-where-they-break-44ae</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/1012/" rel="noopener noreferrer"&gt;Uniswap v4 Hooks: How They Work and Where They Break&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Uniswap v4 went live on January 31, 2025. Nine independent audits covered the core and periphery contracts, and a bug bounty of up to $15.5 million sat on top of that. Four months later, a protocol built on v4 hooks lost roughly $11 million in one exploit chain.&lt;/p&gt;

&lt;p&gt;That gap is the whole story of v4. The core was built carefully. The extension surface was handed to everyone, and nobody was forced to build it carefully.&lt;/p&gt;

&lt;p&gt;This article walks through how v4 and hooks work from the user's click to final settlement, why the PoolManager exists, what actually changed since v3, and why hook security deserves its own discipline. We'll use the Cork and Bunni exploits as the case studies, because they show two very different ways a hook can fail. At PandaSecurity we've spent a lot of time on this class of bugs, so we'll also say where we think the industry's habits are wrong.&lt;/p&gt;

&lt;h2&gt;
  
  
  How Uniswap v4 and hooks work, end to end
&lt;/h2&gt;

&lt;p&gt;Start from the user's side. You sign one transaction, and it doesn't go to a pool. It goes to a router. That contract calls &lt;code&gt;PoolManager.unlock(data)&lt;/code&gt;, and the PoolManager immediately calls back into &lt;code&gt;unlockCallback(data)&lt;/code&gt; on the same contract. Everything interesting happens inside that callback.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..%2F..%2Fassets%2Funiswap_v4_overview.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..%2F..%2Fassets%2Funiswap_v4_overview.png" alt="Flowchart of the Uniswap v4 unlock, callback and delta settlement flow" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Inside the callback, the router does roughly this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Decodes what the user asked for.&lt;/li&gt;
&lt;li&gt;Runs one or more &lt;strong&gt;liquidity-accessing operations&lt;/strong&gt;: &lt;code&gt;swap&lt;/code&gt;, &lt;code&gt;modifyLiquidity&lt;/code&gt;, or &lt;code&gt;donate&lt;/code&gt;. Each one moves pool state and creates a delta.&lt;/li&gt;
&lt;li&gt;If the pool was created with a hook, the PoolManager calls that hook before and after the operation, depending on which callbacks the hook opted into.&lt;/li&gt;
&lt;li&gt;Runs &lt;strong&gt;delta-resolving operations&lt;/strong&gt;: &lt;code&gt;settle&lt;/code&gt;, &lt;code&gt;take&lt;/code&gt;, &lt;code&gt;mint&lt;/code&gt;, &lt;code&gt;burn&lt;/code&gt;, &lt;code&gt;clear&lt;/code&gt;. These are how the router pays what it owes or collects what it's owed.&lt;/li&gt;
&lt;li&gt;Loops back to step 2 as many times as it wants.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;When the callback returns, the PoolManager looks at every delta. If any of them is non-zero, the whole transaction reverts with &lt;code&gt;CurrencyNotSettled&lt;/code&gt;. Otherwise everyone has paid or been paid correctly, and the result goes back to the user.&lt;/p&gt;

&lt;p&gt;Deltas follow a simple sign convention from &lt;a href="https://developers.uniswap.org/docs/protocols/v4/guides/unlock-callback-and-deltas" rel="noopener noreferrer"&gt;Uniswap's own docs&lt;/a&gt;: a negative delta means the PoolManager is owed tokens, a positive one means the PoolManager owes tokens to someone. A swap typically leaves you negative on the token you sold and positive on the token you bought. Adding liquidity leaves you negative on both sides.&lt;/p&gt;

&lt;p&gt;Hooks sit inside this loop at fixed points.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..%2F..%2F%2Fassets%2Funiswap_v4_hooks.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/..%2F..%2F%2Fassets%2Funiswap_v4_hooks.png" alt="Hook call points inside swap and modifyLiquidity" width="800" height="400"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;For a swap, &lt;code&gt;beforeSwap&lt;/code&gt; runs against pre-swap state and can return a delta of its own plus a fee override. &lt;code&gt;afterSwap&lt;/code&gt; runs against post-swap state and can return an adjustment too. Liquidity changes follow the same shape with before and after callbacks for add and remove, and initialize and donate get their own pairs. Whatever deltas the hook returns are folded into the same ledger and have to net to zero by the end, like everything else.&lt;/p&gt;

&lt;p&gt;One detail that matters later: which callbacks a hook implements isn't configured in a settings file. It's encoded in the bits of the hook's deployed address. The PoolManager reads those bits to decide what to call. Keep that in mind, because it's a recurring source of bugs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the PoolManager exists, and what really changed from v3
&lt;/h2&gt;

&lt;p&gt;In Uniswap v3, every pool is its own contract, deployed by a factory. A swap that routes through three pools touches three contracts, and tokens physically move between them along the way. Every hop costs gas, and every integration has to reason about which contract holds what at which moment.&lt;/p&gt;

&lt;p&gt;v4 replaces that with a &lt;strong&gt;singleton&lt;/strong&gt;. One contract, the PoolManager, holds the state for every pool. A "pool" is no longer a deployment; it's an entry in the PoolManager's storage, keyed by the two currencies, the fee, the tick spacing and the hook address. Creating a pool becomes a storage write instead of a contract deployment, which is why Uniswap Labs says pool creation is up to 99.99% cheaper than before.&lt;/p&gt;

&lt;p&gt;The PoolManager also does something a pile of separate pool contracts never could: it keeps a running ledger. That's &lt;strong&gt;flash accounting&lt;/strong&gt;. Every balance-changing operation updates a net delta per currency and per address, and the deltas live in transient storage (the EIP-1153 feature that arrived with the Dencun upgrade). Real token transfers only happen for the net result at the end. According to Uniswap's docs, a multi-hop swap needs just two transfers regardless of how many hops it has, the input token in and the output token out.&lt;/p&gt;

&lt;p&gt;Here's our opinion, and we'll defend it: &lt;strong&gt;the biggest difference between v3 and v4 is not hooks.&lt;/strong&gt; It's that v4 changed who you talk to and when money moves. In v3 you talk to a pool and tokens move on every call. In v4 you talk to a ledger, and tokens move once. Hooks are the feature people tweet about, but they're only practical because of this accounting model. If every hook had to move real tokens at every step across many pools, nobody would be able to afford using them, and the reentrancy surface would be unmanageable.&lt;/p&gt;

&lt;p&gt;The tradeoff is real, though. All pools now share one vault. That makes the core's correctness more valuable, and it makes every hook that touches the ledger a little more dangerous, because it speaks directly to the same shared accounting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What v4 actually innovates
&lt;/h2&gt;

&lt;p&gt;Strip away the marketing and v4 brings six genuinely new things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hooks.&lt;/strong&gt; Contracts that run at defined points in a pool's lifecycle. There are 14 permission flags in a hook address, covering before and after callbacks for initialize, add and remove liquidity, swap and donate, plus return-delta flags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Flash accounting.&lt;/strong&gt; Net deltas in transient storage, settled once per unlock.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Custom accounting.&lt;/strong&gt; A hook can return deltas that change what the swap or liquidity change costs. A &lt;code&gt;BeforeSwapDelta&lt;/code&gt; that consumes the whole specified amount turns the swap into a no-op against the pool's own curve, so the hook becomes the AMM. This is how people build custom curves on top of v4.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native ETH.&lt;/strong&gt; ETH pairs without wrapping, which saves gas on a very common path.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ERC-6909 claims.&lt;/strong&gt; &lt;code&gt;mint&lt;/code&gt; and &lt;code&gt;burn&lt;/code&gt; let you hold balances inside the PoolManager instead of transferring tokens out, which helps when the same funds will be used again soon.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dynamic fees.&lt;/strong&gt; Hooks can override the LP fee, which opens up volatility-aware and time-aware pricing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The periphery adds a &lt;strong&gt;subscriber&lt;/strong&gt; mechanism too: a position owner can register a contract that gets notified when liquidity or ownership changes, so staking or incentive programs don't require handing over the position NFT.&lt;/p&gt;

&lt;p&gt;What does it add up to? Our reading is that Uniswap stopped being a product and became a platform. The liquidity layer is the protocol. The strategy layer is yours. That's a powerful idea, and Uniswap Labs said more than 150 hooks already existed at launch, across ten chains.&lt;/p&gt;

&lt;p&gt;It's also a transfer of risk. Uniswap audited the platform. It can't audit what people build on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The security bill for all that flexibility
&lt;/h2&gt;

&lt;p&gt;Let's be precise about where the losses have landed. As far as public incident reports go, the large v4-related losses so far came from &lt;strong&gt;hooks and integrations&lt;/strong&gt;, not from the PoolManager itself. That's a credit to the core. It's also an uncomfortable fact for anyone who thinks "built on audited v4" is a security statement. It isn't. It's a statement about the floor of the building, not the rooms people built on top.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case 1: Cork Protocol, May 28, 2025 (about $11 million)
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://dedaub.com/blog/the-11m-cork-protocol-hack-a-critical-lesson-in-uniswap-v4-hook-security/" rel="noopener noreferrer"&gt;Dedaub's write-up&lt;/a&gt; and &lt;a href="https://blocksec.com/blog/cork-protocol-incident-two-independent-flaws-combine-into-one-devastating-exploit-chain" rel="noopener noreferrer"&gt;BlockSec's analysis&lt;/a&gt; give the clearest picture. Reported figures range from about $11 million to $12 million of wstETH depending on who's counting.&lt;/p&gt;

&lt;p&gt;Cork's hook had a &lt;code&gt;beforeSwap&lt;/code&gt; function with no check on who was calling it. In normal operation only the PoolManager should call a hook callback. Here, anyone could call it directly, with any parameters they liked, including a pool ID of their own choosing and crafted &lt;code&gt;hookData&lt;/code&gt;. The hook also didn't maintain an allowlist of pools it was supposed to serve.&lt;/p&gt;

&lt;p&gt;The attacker used that to steer the hook into a path that credited a deposit that never happened, minting tokens out of nothing. A second weakness, a manipulable price ratio near expiry, let them turn those tokens into real wstETH. BlockSec describes it as two independent flaws that combined into one chain.&lt;/p&gt;

&lt;p&gt;According to Dedaub, four audit firms worked on Cork, but three of them didn't cover the hook contracts. The fourth likely had the hook in scope and made suggestions that weren't enough to prevent the exploit.&lt;/p&gt;

&lt;p&gt;What stands out to us isn't how clever the exploit was. It's how basic the root cause was. "Only the PoolManager may call hook callbacks" is the first thing you write on the whiteboard when you design a hook, and a missing modifier turned it into an eight-figure loss. The audit scoping story is equally telling: the hook was the highest-risk code in the system and got the least attention.&lt;/p&gt;

&lt;h3&gt;
  
  
  Case 2: Bunni v2, September 2, 2025 (about $8.4 million)
&lt;/h3&gt;

&lt;p&gt;The Bunni incident is the opposite lesson. There was no missing access control and no forged call. Every transaction settled cleanly, every delta netted to zero, and the PoolManager had no objection to anything. The books inside the hook were simply wrong.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://blocksec.com/blog/bunni-incident-repeated-small-withdrawals-compound-a-rounding-error-into-an-8.4m-drain" rel="noopener noreferrer"&gt;BlockSec's breakdown&lt;/a&gt; is the best public analysis we've seen. Bunni v2 is an AMM built as a v4 hook that adds rebalancing and rehypothecation on top of concentrated liquidity. It tracks an "idle balance" for each pool. When liquidity is withdrawn, the idle-balance update used a rounding direction that overstated the idle amount. That made the pool's computed available balance of a nearly empty token too small, and the liquidity function then reported less effective liquidity than the pool really had, which makes swaps move the price much further.&lt;/p&gt;

&lt;p&gt;The attacker first pushed the pool into a state where one token's active balance was down to 28 wei. They then made 44 tiny withdrawals that dragged it to 4 wei, an 85.7% drop, while burning a negligible share of LP tokens. After that, a pair of swaps in opposite directions exploited the undervalued liquidity and drained value from two pools, a USDC/USDT pool on Ethereum and a weETH/ETH pool on Unichain.&lt;/p&gt;

&lt;p&gt;This is the lesson we most want builders to absorb: &lt;strong&gt;delta settlement proves that the hook didn't print tokens at the PoolManager boundary. It says nothing about whether the hook's internal accounting is correct.&lt;/strong&gt; Trail of Bits makes the same point in their &lt;a href="https://blog.trailofbits.com/2026/07/30/building-secure-uniswap-v4-hooks/" rel="noopener noreferrer"&gt;guide to building secure v4 hooks&lt;/a&gt;: settlement checks that deltas net to zero, not that your sign conventions, rounding or balance buckets are right. A hook with a rounding bug will settle perfectly, right up to the moment it bleeds.&lt;/p&gt;

&lt;h3&gt;
  
  
  The other failure patterns
&lt;/h3&gt;

&lt;p&gt;Cork and Bunni are the big ones, but the same Trail of Bits write-up lists patterns that keep showing up in reviews:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Permission bits that don't match the code.&lt;/strong&gt; Because callbacks are encoded in the address, a hook can implement a function whose bit isn't set (never called) or set a bit with no implementation (every call reverts). A missing return-delta bit makes the PoolManager treat the hook's returned delta as zero. One real example in their list: a missing &lt;code&gt;afterSwapReturnDelta&lt;/code&gt; bit made every swap revert with &lt;code&gt;CurrencyNotSettled&lt;/code&gt; once a dynamic protocol fee was switched on.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;User-supplied &lt;code&gt;PoolKey&lt;/code&gt;.&lt;/strong&gt; Pool creation is permissionless, so an attacker can attach your hook to a pool they control, with tokens that run malicious code. A points-farming hook in their examples let deposits route through a malicious pool because it accepted a caller-specified key.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Logic in the wrong callback.&lt;/strong&gt; &lt;code&gt;beforeSwap&lt;/code&gt; sees pre-swap state and &lt;code&gt;afterSwap&lt;/code&gt; sees post-swap state. A just-in-time liquidity penalty hook was bypassed because the penalty was computed after the user had already collected fees through a tiny separate liquidity increase.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Shared state and reentrancy.&lt;/strong&gt; One hook serves many pools, and callbacks run inside a swap. Scratch storage that isn't keyed per pool and caller is an invitation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Upgradeable hooks.&lt;/strong&gt; The permission bits are fixed in the address but the implementation behind a proxy can change. Your upgrade admin is part of the trust model.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Denial of service.&lt;/strong&gt; A revert in optional logic, like reward distribution in &lt;code&gt;afterRemoveLiquidity&lt;/code&gt;, can lock users in.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What we check first when we review a v4 hook
&lt;/h2&gt;

&lt;p&gt;Our review of a hook-based system doesn't start with the swap math. It starts with the boundaries:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Who can call each callback?&lt;/strong&gt; Every one must be restricted to the PoolManager, and &lt;code&gt;unlockCallback&lt;/code&gt; must verify the caller too.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Which pools does this hook serve?&lt;/strong&gt; If the answer is "any pool that points at it", that's a finding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do the address permission bits match what the contract implements?&lt;/strong&gt; Both directions, including return-delta flags.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What are the conservation invariants?&lt;/strong&gt; No output the accounting didn't charge for, no value from a same-transaction round trip, internal books equal to real balances, and rounding that always favors the pool.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Is every input hostile?&lt;/strong&gt; &lt;code&gt;PoolKey&lt;/code&gt;, &lt;code&gt;hookData&lt;/code&gt;, fee inputs and token behavior, including fee-on-transfer and rebasing tokens.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What happens at the extremes?&lt;/strong&gt; Near-empty balances, dust-size operations repeated many times (Bunni's trick), and manipulated prices.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Who can change the code?&lt;/strong&gt; Proxies, admin keys and timelocks.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Fuzzing against adversarial pools and tokens belongs on that list too, because several of these bugs are only reachable through sequences no unit test would try.&lt;/p&gt;

&lt;p&gt;One more opinion. Teams often ask whether their hook is "safe because Uniswap is audited". We tell them to flip the question. Treat the hook as a standalone protocol that happens to be wired into an AMM. It holds funds, it has an admin, it has its own accounting, and it faces an attacker who can create pools, choose tokens and send dust. That's a full protocol audit scope, and the Cork story shows what happens when it's treated as an appendix.&lt;/p&gt;

&lt;h2&gt;
  
  
  Key takeaways
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;v4's real shift is architectural: one PoolManager ledger and flash accounting, with hooks built on top.&lt;/li&gt;
&lt;li&gt;The core has held up in public reports so far. Losses have come from hooks.&lt;/li&gt;
&lt;li&gt;Cork was a basic access-control failure in code that was under-scoped in audits.&lt;/li&gt;
&lt;li&gt;Bunni was an accounting failure that settled perfectly, which is why zero deltas can't be treated as correctness.&lt;/li&gt;
&lt;li&gt;Hook security is protocol security. Scope it that way.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're building on v4 and want a second pair of eyes on your hook before it holds real money, that's what the PandaSecurity team does. Bring us in before launch, not after the post-mortem.&lt;/p&gt;

&lt;h2&gt;
  
  
  FAQ
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What is the PoolManager in Uniswap v4?&lt;/strong&gt;&lt;br&gt;
It's the singleton contract that stores the state of every v4 pool and keeps the ledger of what each address owes or is owed during an unlock session. It replaces the one-contract-per-pool design of v3.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What is the biggest difference between Uniswap v3 and v4?&lt;/strong&gt;&lt;br&gt;
Architecture. v4 uses a single PoolManager and flash accounting, so tokens move once per unlock instead of at every step. Hooks, native ETH and custom accounting build on that foundation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Are Uniswap v4 hooks safe?&lt;/strong&gt;&lt;br&gt;
Not automatically. The core contracts were heavily audited, but each hook is separate code with its own accounting and access control. The Cork and Bunni exploits both came from hook-level flaws, not from the PoolManager.&lt;/p&gt;

&lt;h2&gt;
  
  
  Sources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://blog.uniswap.org/uniswap-v4-is-here" rel="noopener noreferrer"&gt;Uniswap v4 launch announcement&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://developers.uniswap.org/docs/protocols/v4/guides/unlock-callback-and-deltas" rel="noopener noreferrer"&gt;Unlock Callback &amp;amp; Deltas, Uniswap docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.uniswap.org/contracts/v4/concepts/flash-accounting" rel="noopener noreferrer"&gt;Flash accounting, Uniswap docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.uniswap.org/contracts/v4/concepts/v4-vs-v3" rel="noopener noreferrer"&gt;v4 vs v3, Uniswap docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dedaub.com/blog/the-11m-cork-protocol-hack-a-critical-lesson-in-uniswap-v4-hook-security/" rel="noopener noreferrer"&gt;Dedaub: The $11M Cork Protocol hack&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blocksec.com/blog/cork-protocol-incident-two-independent-flaws-combine-into-one-devastating-exploit-chain" rel="noopener noreferrer"&gt;BlockSec: Cork Protocol incident&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blocksec.com/blog/bunni-incident-repeated-small-withdrawals-compound-a-rounding-error-into-an-8.4m-drain" rel="noopener noreferrer"&gt;BlockSec: Bunni incident&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://blog.trailofbits.com/2026/07/30/building-secure-uniswap-v4-hooks/" rel="noopener noreferrer"&gt;Trail of Bits: Building secure Uniswap v4 hooks&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

</description>
      <category>defi</category>
      <category>security</category>
      <category>uniswap</category>
      <category>solidity</category>
    </item>
    <item>
      <title>MCN Labs Exploit Analysis: How a Reward Accounting Bug Led to a $92.6K Loss</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Fri, 16 Oct 2026 01:30:37 +0000</pubDate>
      <link>https://dev.to/pandasecurity/mcn-labs-exploit-analysis-how-a-reward-accounting-bug-led-to-a-926k-loss-505j</link>
      <guid>https://dev.to/pandasecurity/mcn-labs-exploit-analysis-how-a-reward-accounting-bug-led-to-a-926k-loss-505j</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/1001/" rel="noopener noreferrer"&gt;MCN Labs Exploit Analysis: How a Reward Accounting Bug Led to a $92.6K Loss&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;MCN Labs reportedly suffered a loss of approximately &lt;strong&gt;$92.6K&lt;/strong&gt; due to an accounting flaw in its &lt;code&gt;LPBonus&lt;/code&gt; reward contract.&lt;/p&gt;

&lt;p&gt;The vulnerability came from a subtle inconsistency:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The contract used one MSN reserve value when creating the FIST reward index, but another reserve value when calculating how much reward a user could claim.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;By manipulating the reserve between these two operations, the attacker was able to receive more FIST than the amount that should have been distributed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incident Overview
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Protocol:&lt;/strong&gt; MCN Labs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Estimated Loss:&lt;/strong&gt; ~$92.6K&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Vulnerable Contract:&lt;/strong&gt; &lt;code&gt;0x52272524a22f941f5489c1233732797314bb054b&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Attacker:&lt;/strong&gt; &lt;code&gt;0xb6fff29dd2b5423a159e50877fc4af7a54e76f7a&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Root Cause:&lt;/strong&gt; Inconsistent reserve values in LP reward accounting&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;According to the reported attack data, the attacker reduced the MSN reserve to approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;89.327788899759978606 MSN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;during reward accrual.&lt;/p&gt;

&lt;p&gt;Before triggering LP removal, the reserve was increased to approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;491.113095162589329323 MSN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This difference allowed one LP holder to claim approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1,442,165.713011 FIST
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;even though the relevant reward increment was funded by only approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;940,041.612768 FIST
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  The Vulnerable Code
&lt;/h2&gt;

&lt;p&gt;The main issue can be found in the way &lt;code&gt;oneshareFIST&lt;/code&gt; is updated.&lt;/p&gt;

&lt;p&gt;Inside &lt;code&gt;AddFistFee&lt;/code&gt;, the contract swaps MSN into FIST and calculates the newly received FIST:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uint256 balanebefore = IBEP20(fist).balanceOf(address(this));

router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
    amountofmsn,
    0,
    path,
    address(this),
    1e40
);

uint256 balanceafter = IBEP20(fist).balanceOf(address(this));

uint256 amount = balanceafter.sub2(balanebefore);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It then updates the global reward index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;oneshareFIST += amount.mul(1e18).div(reservethis);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In simplified form:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reward Index Increase = FIST Reward / MSN Reserve
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This means that when the MSN reserve becomes smaller, the increase in &lt;code&gt;oneshareFIST&lt;/code&gt; becomes larger.&lt;/p&gt;

&lt;p&gt;The problem appears when rewards are later calculated.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reward Calculation
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;CalcPendingUser&lt;/code&gt; calculates a user's LP weight using the &lt;strong&gt;current reserve&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uint256 newLpbalance = IBEP20(lpaddress).balanceOf(user);
uint256 totalsupplylp = IBEP20(lpaddress).totalSupply();

uint256 poolshare = userAddAmount[user];

if(reserverthis &amp;gt; 0 &amp;amp;&amp;amp; totalsupplylp &amp;gt; 0)
    poolshare = newLpbalance.mul(reserverthis).div(totalsupplylp);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The pending reward is then calculated as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;return oneshareFIST
    .sub2(takedFIST[user])
    .mul(poolshare)
    .div(1e18);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The issue is that these two calculations do not necessarily use the same reserve.&lt;/p&gt;

&lt;p&gt;During reward accrual:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;oneshareFIST ∝ 1 / Reserve_A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;During reward claiming:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;poolshare ∝ Reserve_B
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So the final reward effectively contains:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reserve_B / Reserve_A
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If an attacker can make:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reserve_A small
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;and later make:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reserve_B large
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the calculated reward can be amplified.&lt;/p&gt;

&lt;h2&gt;
  
  
  Attack Flow
&lt;/h2&gt;

&lt;p&gt;The attack can be simplified into five steps.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Establish an LP Position
&lt;/h3&gt;

&lt;p&gt;The attacker creates or controls an LP position that participates in the &lt;code&gt;LPBonus&lt;/code&gt; reward system.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Reduce the MSN Reserve
&lt;/h3&gt;

&lt;p&gt;The attacker manipulates the liquidity state and reduces the relevant MSN reserve.&lt;/p&gt;

&lt;p&gt;The reported reserve reached approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;89.3278 MSN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Trigger Reward Accrual
&lt;/h3&gt;

&lt;p&gt;The protocol receives FIST rewards and updates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;oneshareFIST += amount * 1e18 / reservethis;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because the reserve is small, the resulting increase in &lt;code&gt;oneshareFIST&lt;/code&gt; becomes unusually large.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Increase the Reserve
&lt;/h3&gt;

&lt;p&gt;The attacker then changes the liquidity state again.&lt;/p&gt;

&lt;p&gt;The reserve reportedly increased to approximately:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;491.1131 MSN
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  5. Trigger LP Removal
&lt;/h3&gt;

&lt;p&gt;When &lt;code&gt;UserRemoveLp&lt;/code&gt; is executed, the protocol calculates the user's reward weight using the new, larger reserve.&lt;/p&gt;

&lt;p&gt;The previously inflated &lt;code&gt;oneshareFIST&lt;/code&gt; is now multiplied by a larger &lt;code&gt;poolshare&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The contract therefore calculates a FIST reward significantly larger than what should have been distributed.&lt;/p&gt;

&lt;h2&gt;
  
  
  Root Cause
&lt;/h2&gt;

&lt;p&gt;The root cause is not simply "AMM reserve manipulation."&lt;/p&gt;

&lt;p&gt;AMM reserves naturally change as users trade and liquidity moves.&lt;/p&gt;

&lt;p&gt;The real problem is that the protocol used &lt;strong&gt;different reserve snapshots for different parts of the same reward calculation&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The reward index was created using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reserve at reward accrual
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;while the user's reward weight was calculated using:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Reserve at reward settlement
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There was no guarantee that these values would remain consistent.&lt;/p&gt;

&lt;p&gt;This created an attacker-controlled multiplier inside the reward calculation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why This Matters
&lt;/h2&gt;

&lt;p&gt;This is a good example of why DeFi audits need to look beyond individual Solidity functions.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;AddFistFee&lt;/code&gt; alone does not immediately look catastrophic.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;CalcPendingUser&lt;/code&gt; alone also looks relatively simple.&lt;/p&gt;

&lt;p&gt;The vulnerability only becomes obvious when we follow the complete accounting flow:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Manipulate Reserve
        ↓
Accumulate FIST Reward
        ↓
Update oneshareFIST
        ↓
Change Reserve Again
        ↓
Trigger UserRemoveLp
        ↓
Calculate Reward Using New Reserve
        ↓
Receive Excess FIST
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The problem exists across multiple state transitions.&lt;/p&gt;

&lt;h2&gt;
  
  
  How It Could Be Fixed
&lt;/h2&gt;

&lt;p&gt;The reward system should avoid using independently changing AMM reserve values for reward accrual and settlement.&lt;/p&gt;

&lt;p&gt;A more robust design would distribute rewards using a consistent accounting unit such as LP shares:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;rewardPerShare +=
    rewardAmount * 1e18 / totalEligibleShares;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;User rewards could then be calculated as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pending =
    userShares
    * (rewardPerShare - userRewardDebt)
    / 1e18;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important principle is simple:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The denominator used when rewards are distributed and the unit used when rewards are claimed should represent the same accounting quantity.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;If reserve-based accounting is required, the protocol should use consistent snapshots rather than reading mutable reserve values at different stages.&lt;/p&gt;

&lt;h2&gt;
  
  
  Security Lesson
&lt;/h2&gt;

&lt;p&gt;One useful invariant for reward contracts is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Total newly created reward claims
&amp;lt;=
Total rewards funded
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If depositing 940,000 FIST into a reward system can create more than 1.4 million FIST of new claims, something is fundamentally wrong with the accounting.&lt;/p&gt;

&lt;p&gt;When auditing DeFi protocols, special attention should be paid whenever reward calculations depend on mutable external state such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AMM reserves&lt;/li&gt;
&lt;li&gt;token balances&lt;/li&gt;
&lt;li&gt;LP total supply&lt;/li&gt;
&lt;li&gt;vault share prices&lt;/li&gt;
&lt;li&gt;exchange rates&lt;/li&gt;
&lt;li&gt;oracle prices&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The important question is not only:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can this value be manipulated?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;It is also:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can the protocol use different versions of this value when creating and later settling the same accounting obligation?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That distinction is exactly what made this vulnerability dangerous.&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;The MCN Labs incident demonstrates how a relatively small accounting inconsistency can turn into a real loss.&lt;/p&gt;

&lt;p&gt;The protocol calculated its FIST reward index using one MSN reserve value and later calculated the user's reward weight using another.&lt;/p&gt;

&lt;p&gt;By reducing the reserve during reward accrual and increasing it before settlement, the attacker was able to amplify the reward calculation and extract excess FIST.&lt;/p&gt;

&lt;p&gt;At &lt;strong&gt;PandaSecurity&lt;/strong&gt;, we believe these bugs are particularly important because they often do not look dangerous when individual functions are reviewed in isolation.&lt;/p&gt;

&lt;p&gt;The Solidity code may execute exactly as intended.&lt;/p&gt;

&lt;p&gt;The problem is that the economic equation itself is wrong.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;PandaSecurity Team — Smart Contract Security, DeFi Exploit Research, and On-Chain Incident Analysis.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>defi</category>
      <category>security</category>
      <category>solidity</category>
      <category>web3</category>
    </item>
    <item>
      <title>How One Wei of Shares Drained 14,838 DAI: A Uniswap V4 Vault Exploit</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Fri, 16 Oct 2026 01:30:34 +0000</pubDate>
      <link>https://dev.to/pandasecurity/how-one-wei-of-shares-drained-14838-dai-a-uniswap-v4-vault-exploit-34m3</link>
      <guid>https://dev.to/pandasecurity/how-one-wei-of-shares-drained-14838-dai-a-uniswap-v4-vault-exploit-34m3</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://www.panda-security.xyz/blog/1009/" rel="noopener noreferrer"&gt;How One Wei of Shares Drained 14,838 DAI: A Uniswap V4 Vault Exploit&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;On October 9, 2026, a transaction on Polygon exposed a simple but dangerous accounting flaw in a liquidity vault. The attacker held only &lt;strong&gt;one minimal share unit&lt;/strong&gt;, yet withdrew approximately &lt;strong&gt;14,838.47 DAI&lt;/strong&gt; from the contract.&lt;/p&gt;

&lt;p&gt;This was not a reentrancy exploit, nor did it depend on an obscure arithmetic trick. The failure sat inside what looked like a harmless fallback branch: whenever the strategy reported zero liquidity, the vault transferred its entire token balance to the current redeemer.&lt;/p&gt;

&lt;p&gt;Panda Security’s review of the transaction and source code found a clean four-step path. The attacker acquired one share unit, manipulated the pool so that &lt;code&gt;reposition()&lt;/code&gt; produced zero liquidity, then called &lt;code&gt;withdraw()&lt;/code&gt;. Instead of calculating a proportional payout, the vault emptied its DAI and USDC balances into the attacker’s contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  Incident overview
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Item&lt;/th&gt;
&lt;th&gt;Details&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Network&lt;/td&gt;
&lt;td&gt;Polygon PoS&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exploit time&lt;/td&gt;
&lt;td&gt;October 9, 2026, 02:23:42 UTC&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Attacker EOA&lt;/td&gt;
&lt;td&gt;&lt;a href="https://polygonscan.com/address/0x59Ae3c8dd50063429D86BdaEBbC350fB64e9884B" rel="noopener noreferrer"&gt;&lt;code&gt;0x59Ae3c8d...B64e9884B&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Affected vault&lt;/td&gt;
&lt;td&gt;&lt;a href="https://polygonscan.com/address/0x048E88311ca84876aeA086125825492e2AF2e5A0" rel="noopener noreferrer"&gt;&lt;code&gt;0x048E8831...e2AF2e5A0&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Exploit contract&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0xc2C6F4F7E1425bE1DCB996175Cf7852bF53c23E2&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Key transaction&lt;/td&gt;
&lt;td&gt;&lt;a href="https://polygonscan.com/tx/0x5b795dc5c5e1665b712c7ccf478c1d8c59a162d698f4e0bfb40bd303e04a7ff2" rel="noopener noreferrer"&gt;&lt;code&gt;0x5b795dc5...e04a7ff2&lt;/code&gt;&lt;/a&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Amount sent by the vault&lt;/td&gt;
&lt;td&gt;14,838.466608989135865711 DAI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Final transfer to attacker EOA&lt;/td&gt;
&lt;td&gt;Approximately 14,813.548248 USDC&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The amount removed from the vault and the attacker’s final proceeds are not identical. The vault paid out roughly 14,838.47 DAI. After swaps, repayment of temporary liquidity, and execution costs, approximately 14,813.55 USDC reached the attacker’s EOA.&lt;/p&gt;

&lt;h2&gt;
  
  
  The vulnerable code
&lt;/h2&gt;

&lt;p&gt;The flaw is located in the zero-liquidity branch of &lt;code&gt;_withdraw()&lt;/code&gt;:&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 shareAmount,
    address recipient,
    bool addDust
) internal {
    if (liquidity == 0) {
        uint256 daiBal = DAI.balanceOf(address(this));
        uint256 usdcBal = USDC.balanceOf(address(this));

        if (daiBal &amp;gt; 0) DAI.transfer(recipient, daiBal);
        if (usdcBal &amp;gt; 0) USDC.transfer(recipient, usdcBal);

        emit Withdrawn(recipient, shareAmount, daiBal, usdcBal);
        return;
    }

    // The normal path calculates a proportional withdrawal.
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Under normal conditions, the vault calculates how much liquidity belongs to the redeemer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uint256 currentTotalShares = totalShares + shareAmount;

uint128 toRemove = uint128(
    uint256(liquidity) * shareAmount / currentTotalShares
);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That proportional calculation disappears when &lt;code&gt;liquidity == 0&lt;/code&gt;. The vault simply reads its DAI and USDC balances and transfers everything to the caller.&lt;/p&gt;

&lt;p&gt;The code incorrectly treats these two states as equivalent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;No capital is currently deployed as liquidity
                    !=
The current redeemer owns every idle asset
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That mistaken assumption is the root cause of the loss.&lt;/p&gt;

&lt;h2&gt;
  
  
  How the attack worked
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Acquire one share with one wei of DAI
&lt;/h3&gt;

&lt;p&gt;The transaction’s &lt;code&gt;Deposited&lt;/code&gt; event records:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;daiAmount    = 1
sharesIssued = 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;DAI uses 18 decimals. The value &lt;code&gt;1&lt;/code&gt; therefore means one wei of DAI, not one full DAI.&lt;/p&gt;

&lt;p&gt;The economic cost was negligible, but this was enough to satisfy the withdrawal checks:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (shareAmount == 0) revert ZeroAmount();
if (shares[msg.sender] &amp;lt; shareAmount) revert InsufficientShares();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 2: Move the pool price and unlock permissionless repositioning
&lt;/h3&gt;

&lt;p&gt;The vault allows an ordinary account to call &lt;code&gt;reposition()&lt;/code&gt; when the current tick has moved outside the active range:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (msg.sender != minter) {
    int24 ct = _getCurrentTick();
    if (ct &amp;gt;= tickLower &amp;amp;&amp;amp; ct &amp;lt; tickUpper) {
        revert PositionInRange();
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Using temporary liquidity, the attacker executed a large swap in the Uniswap V4 pool and pushed the current tick outside the vault’s existing range. That made the public &lt;code&gt;reposition()&lt;/code&gt; path available.&lt;/p&gt;

&lt;p&gt;Price manipulation was only the trigger. The deeper problem was that an externally influenced strategy operation could place the vault into a state that its withdrawal accounting did not handle safely.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Make &lt;code&gt;reposition()&lt;/code&gt; store zero liquidity
&lt;/h3&gt;

&lt;p&gt;The internal &lt;code&gt;_reposition()&lt;/code&gt; function removes the old position, leaves the withdrawn assets in the vault, and calculates the liquidity for a new position:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;uint128 newLiquidity;

if (required1 &amp;lt;= amount1) {
    newLiquidity = liq0;
} else {
    newLiquidity = LiquidityAmounts.getLiquidityForAmount1(
        s.sqrtPriceLower,
        s.sqrtPriceX96,
        amount1
    );
}

if (newLiquidity &amp;gt; 0) {
    _modifyLiquidity(
        s.newTickLower,
        s.newTickUpper,
        int128(newLiquidity)
    );
}

liquidity = newLiquidity;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the exploit transaction, the &lt;code&gt;Repositioned&lt;/code&gt; event recorded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;newTickLower = 276324
newTickUpper = 276327
newLiquidity = 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The old position had already been removed, leaving roughly 14,838 DAI at the vault address. No replacement position was created. The vault was now in the exact state required by the vulnerable branch:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;liquidity == 0
vault DAI balance &amp;gt; 14,838 DAI
attacker shares == 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Step 4: Redeem one share and empty the vault
&lt;/h3&gt;

&lt;p&gt;The exploit contract immediately redeemed the single minimal share unit.&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;liquidity&lt;/code&gt; was zero, &lt;code&gt;_withdraw()&lt;/code&gt; transferred the entire DAI balance to the exploit contract. The on-chain &lt;code&gt;Withdrawn&lt;/code&gt; event shows:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sharesBurned = 1
daiOut       = 14838466608989135865711
usdcOut      = 0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With DAI’s 18 decimals, that is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;14,838.466608989135865711 DAI
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The attacker paid one wei of DAI for the vault share and received idle assets that belonged collectively to all shareholders.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this bug is easy to miss
&lt;/h2&gt;

&lt;p&gt;Read in isolation, the branch looks like a sensible convenience: if no LP position exists, return the tokens held by the vault.&lt;/p&gt;

&lt;p&gt;The flaw appears only when the full state transition is considered:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;code&gt;reposition()&lt;/code&gt; can remove all liquidity from the old position.&lt;/li&gt;
&lt;li&gt;The new-liquidity calculation can return zero.&lt;/li&gt;
&lt;li&gt;The withdrawn assets remain at the vault address.&lt;/li&gt;
&lt;li&gt;Existing user shares remain valid.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;_withdraw()&lt;/code&gt; treats zero strategy liquidity as permission to pay one user the entire balance.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;At Panda Security, we view this as a state-accounting failure rather than a single bad &lt;code&gt;if&lt;/code&gt; statement. Vault reviews must track whether assets, shares, and deployed positions preserve the same ownership relationship across every state transition.&lt;/p&gt;

&lt;p&gt;This vault effectively maintained three ledgers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the shareholder ledger: &lt;code&gt;shares&lt;/code&gt; and &lt;code&gt;totalShares&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;the asset ledger: idle DAI and USDC balances;&lt;/li&gt;
&lt;li&gt;the strategy ledger: &lt;code&gt;liquidity&lt;/code&gt; deployed in Uniswap V4.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;During the exploit, the strategy ledger fell to zero while both the asset ledger and shareholder ledger remained non-zero. The withdrawal function selected its payout logic using only the strategy ledger, allowing the three ledgers to diverge.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to fix it properly
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Always distribute idle assets proportionally
&lt;/h3&gt;

&lt;p&gt;The minimum repair is to preserve the total supply before burning shares and use it to calculate the redeemer’s portion of every idle balance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function _withdrawIdleAssets(
    uint256 shareAmount,
    uint256 totalSharesBefore,
    address recipient
) internal {
    uint256 daiBal = DAI.balanceOf(address(this));
    uint256 usdcBal = USDC.balanceOf(address(this));

    uint256 daiOut = daiBal * shareAmount / totalSharesBefore;
    uint256 usdcOut = usdcBal * shareAmount / totalSharesBefore;

    if (daiOut &amp;gt; 0) DAI.safeTransfer(recipient, daiOut);
    if (usdcOut &amp;gt; 0) USDC.safeTransfer(recipient, usdcOut);
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only a user burning all outstanding shares should be able to receive the vault’s entire balance.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Do not allow repositioning to silently produce zero liquidity
&lt;/h3&gt;

&lt;p&gt;If the old position contained assets but the new calculation produces zero liquidity, the transaction should revert:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;if (newLiquidity == 0 &amp;amp;&amp;amp; totalShares &amp;gt; 0) {
    revert ZeroLiquidityAfterReposition();
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A stronger design should also enforce minimum deployed liquidity and minimum capital utilization. Checking only for a non-zero result can still permit economically meaningless values near rounding boundaries.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Separate asset accounting from strategy state
&lt;/h3&gt;

&lt;p&gt;Withdrawal rights must be derived from assets and shares, not from whether an LP position happens to exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;userAssets = totalManagedAssets * userShares / totalShares
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;totalManagedAssets&lt;/code&gt; should account for at least:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;DAI and USDC held in the Uniswap V4 position;&lt;/li&gt;
&lt;li&gt;idle DAI and USDC held by the vault;&lt;/li&gt;
&lt;li&gt;collected assets waiting to be reinvested;&lt;/li&gt;
&lt;li&gt;any reliably measurable principal or fees pending collection.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The strategy layer decides where capital is deployed. The accounting layer decides who owns it. A single &lt;code&gt;liquidity&lt;/code&gt; variable cannot safely answer both questions.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Harden &lt;code&gt;reposition()&lt;/code&gt; against price manipulation
&lt;/h3&gt;

&lt;p&gt;Checking only the current tick is not sufficient for a publicly callable vault rebalance. Practical defenses include:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;validating the price against a TWAP or trusted oracle;&lt;/li&gt;
&lt;li&gt;enforcing a minimum post-reposition asset value;&lt;/li&gt;
&lt;li&gt;requiring &lt;code&gt;minLiquidityOut&lt;/code&gt; or &lt;code&gt;minAssetsDeployed&lt;/code&gt;;&lt;/li&gt;
&lt;li&gt;limiting single-block price movement;&lt;/li&gt;
&lt;li&gt;restricting execution to approved keepers or using delayed execution;&lt;/li&gt;
&lt;li&gt;testing zero-liquidity, one-sided balance, and rounding-boundary cases explicitly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Panda Security’s vault audit checklist
&lt;/h2&gt;

&lt;p&gt;For vaults connected to LP, lending, or compounding strategies, we recommend testing the following invariants:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. A user can never receive more assets than their proportional ownership.
2. A partial redemption can never withdraw the vault's entire balance.
3. Failed strategy migration cannot alter asset ownership while shares exist.
4. liquidity == 0 does not imply totalAssets == 0.
5. Asset value before and after repositioning stays within an explicit limit.
6. Minimal share amounts cannot bypass proportional accounting.
7. An externally controlled price cannot select an unsafe payout branch.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Happy-path tests covering “deposit, earn fees, withdraw” are not enough. The most valuable cases often combine unusual states:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a one-wei deposit followed by a one-wei redemption;&lt;/li&gt;
&lt;li&gt;a position moving completely out of range;&lt;/li&gt;
&lt;li&gt;removal of an old position followed by a zero new-liquidity result;&lt;/li&gt;
&lt;li&gt;token balances remaining while LP liquidity is zero;&lt;/li&gt;
&lt;li&gt;withdrawals by the final shareholder and by a non-final shareholder;&lt;/li&gt;
&lt;li&gt;price manipulation combined with permissionless keeper actions.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Final thoughts
&lt;/h2&gt;

&lt;p&gt;The attacker did not break Uniswap and did not need a sophisticated reentrancy chain. They moved the vault into a state where asset ownership had never been properly defined, then called a public withdrawal function.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;liquidity == 0&lt;/code&gt; is a strategy condition, not proof of ownership. As long as multiple shares exist, every withdrawal path must return to the same basic rule: the user’s payout is determined by their ownership percentage, regardless of whether the assets are currently inside an LP position or sitting idle in the vault.&lt;/p&gt;

&lt;p&gt;That distinction is central to Panda Security’s approach to vault reviews: &lt;strong&gt;verify asset conservation and share invariants before reviewing yield logic. A yield bug reduces returns; an ownership bug empties the vault.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Vaults, AMM strategies, auto-compounding systems, and Uniswap V4 integrations should undergo source review, state-machine testing, and mainnet-fork attack simulation before deployment. Panda Security will continue turning real-world exploit paths into concrete audit checks that teams can apply before attackers do.&lt;/p&gt;

</description>
      <category>defi</category>
      <category>security</category>
      <category>solidity</category>
      <category>uniswap</category>
    </item>
    <item>
      <title>Audit H/M report Summary from Ondo code4rena</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Fri, 13 Oct 2023 06:07:27 +0000</pubDate>
      <link>https://dev.to/pandasecurity/audit-hm-report-summary-from-ondo-code4rena-4dii</link>
      <guid>https://dev.to/pandasecurity/audit-hm-report-summary-from-ondo-code4rena-4dii</guid>
      <description>&lt;h2&gt;
  
  
  1.Chain support chain cannot be removed or cleared in bridge contracts.
&lt;/h2&gt;

&lt;p&gt;Contracts provide a way to add support for a chain; however, they do not provide a way to delete it.&lt;br&gt;
When a contract offers an addition method, it's important to consider whether it should also provide a deletion method&lt;/p&gt;

&lt;h2&gt;
  
  
  2.Contract use msg.sender as remote chain receiver when bridge token.
&lt;/h2&gt;

&lt;p&gt;AA wallet like safe has different wallet address on different chains.&lt;br&gt;
This vulnerability requires us to be familiar with some commonly used third-party tools in order to discover it&lt;/p&gt;

&lt;h2&gt;
  
  
  3.Two different transactions can result in the same txnHash value, thus breaking the approval process of transaction minting
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;txnHashToTransaction[txnHash] = Transaction(srcSender, amt); 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Consider whether the keys in similar mappings are always unique?&lt;/p&gt;

&lt;h2&gt;
  
  
  4.Admin can’t burn tokens from blocklisted addresses because of a check in _beforeTokenTransfer
&lt;/h2&gt;

&lt;p&gt;The burn operation prematurely calls the beforeTransfer function, but beforeTransfer needs to check if the address is blocked, which hinders the successful execution of the burn&lt;/p&gt;

&lt;h2&gt;
  
  
  origin
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://blog.coffiasd.cn/23-10-13.html" rel="noopener noreferrer"&gt;https://blog.coffiasd.cn/23-10-13.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  contact me for SC private review
&lt;/h2&gt;

&lt;p&gt;Twitter:&lt;a href="https://twitter.com/coffiasse" rel="noopener noreferrer"&gt;https://twitter.com/coffiasse&lt;/a&gt;&lt;br&gt;
Github:&lt;a href="https://github.com/coffiasd" rel="noopener noreferrer"&gt;https://github.com/coffiasd&lt;/a&gt;&lt;br&gt;
TG:@coffiasd&lt;/p&gt;

</description>
      <category>solidity</category>
    </item>
    <item>
      <title>All you need to know about openzeppelin access control</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Thu, 27 Jul 2023 04:57:34 +0000</pubDate>
      <link>https://dev.to/pandasecurity/all-you-need-to-know-about-openzeppelin-access-control-18h8</link>
      <guid>https://dev.to/pandasecurity/all-you-need-to-know-about-openzeppelin-access-control-18h8</guid>
      <description>&lt;h2&gt;
  
  
  what is access control
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2hsype5oaagd3xfqm7w5.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F2hsype5oaagd3xfqm7w5.png" alt="Alt access control" width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Basiclly,access control means "who is allowed to do this thing".It's quite important for smart contract to specify a particular address who can totally control the whole system.Therefor it's critical to fully understand how to use access control before using it in your project or just copy some example code from somewhere.&lt;br&gt;
In openzeppelin there are mainly two  ways to implementing access control.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Single onlyOwner role&lt;/li&gt;
&lt;li&gt;Role-Based Access Control (RBAC)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Let's dive into how to master this 2 ways.&lt;/p&gt;
&lt;h2&gt;
  
  
  master how to use it by practice
&lt;/h2&gt;
&lt;h3&gt;
  
  
  OnlyOwner
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/Ownable.sol";

contract MyContract is Ownable {
    function normalThing() public {
        // anyone can call this normalThing()
    }

    function specialThing() public onlyOwner {
        // only the owner can call specialThing()!
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;let's create two user for our test.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;owner address&lt;/li&gt;
&lt;li&gt;arbitrary address
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    address owner = address(0x100);
    address arbitrary = address(0x101);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;and the we use owner address to create the smart contract,it's quite simple you don't even need to pass any parameters.openzeppelin &lt;code&gt;Ownable&lt;/code&gt; would set msg.sender as it's owner as default.we use foundry for testing,if you're not familiar  with  foundry,go and learn it first before proceeding further.&lt;/p&gt;

&lt;p&gt;Firstly,we use owner address create our smart contract above in foundry test contract setUp function.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//let's set address owner to msg.sender.
vm.prank(owner);

//create the contract.
myContract = new MyContract();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then we use the arbitrary address to invoke the &lt;code&gt;specialThing&lt;/code&gt; function which is protected by onlyOwner modifier.As expect the invoke revert because of access control.Here goes the code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//use arbitrary address invoke specialThing() function.
vm.prank(arbitrary);
vm.expectRevert("Ownable: caller is not the owner");
myContract.specialThing();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Finally,we use our owner who master the contract invoke the &lt;code&gt;specialThing&lt;/code&gt; function.As you see the owne succeed.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//switch to owner.
vm.prank(owner);
myContract.specialThing();
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want the whole example codes go for:&lt;a href="https://gist.github.com/coffiasd/84552b5a33845fa567bfc3aa5204d460" rel="noopener noreferrer"&gt;https://gist.github.com/coffiasd/84552b5a33845fa567bfc3aa5204d460&lt;/a&gt;&lt;br&gt;
If you have any questions, you can also reach me through the contact information below.&lt;/p&gt;
&lt;h3&gt;
  
  
  Role-Based Access Control
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;pragma solidity ^0.8.0;

import "@openzeppelin/contracts/access/AccessControl.sol";
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract MyToken2 is ERC20, AccessControl {
    // Create a new role identifier for the minter role
    bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");

    constructor(address minter) ERC20("MyToken", "TKN") {
        _grantRole(DEFAULT_ADMIN_ROLE, minter);
        // Grant the minter role to a specified account
        _grantRole(MINTER_ROLE, minter);
    }

    function mint(address to, uint256 amount) public {
        // Check that the calling account has the minter role
        require(hasRole(MINTER_ROLE, msg.sender), "Caller is not a minter");
        _mint(to, amount);
    }
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;We can create whatever role we want in Role-Based Access Control (RBAC) contract.Let's say we want create a minter role who can mint some ERC20 token.We can use a special constant &lt;code&gt;bytes32&lt;/code&gt; as the name of the role like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Create a new role identifier for the minter role
bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE");
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Firstly,we use &lt;code&gt;owner&lt;/code&gt; address as our minter to create our access control contract.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; myToken = new MyToken2(owner);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And then we use the arbitrary user to mint some ERC20 token:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//use arbitrary address invoke mint() function.
vm.prank(arbitrary);
vm.expectRevert("Caller is not a minter");
myToken.mint(address(this), 1);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;the above invoke would be revert because of arbitrary don't hold the minter role.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;//switch to owner.
vm.prank(owner);
myToken.mint(address(this), 1);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If we switch msg.sender to owner the call would successed.&lt;/p&gt;

&lt;p&gt;What's more? we can use owner address to grant a &lt;code&gt;minter&lt;/code&gt; role whoever we want.Let's say we grant the &lt;code&gt;minter&lt;/code&gt; role to the arbitrary address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vm.prank(owner);
myToken.grantRole(keccak256("MINTER_ROLE"), arbitrary);

vm.prank(arbitrary);
myToken.mint(address(this), 1);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After invoke the grantRole function the arbitrary has the right to mint token.&lt;/p&gt;

&lt;p&gt;At the same time,arbitrary address don't has the right to grant role to another address:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vm.prank(arbitrary);
vm.expectRevert();
myToken.grantRole(keccak256("MINTER_ROLE"), address(0x1001));
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;As the owner we also have the right to revoke role we grant before:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;vm.prank(owner);
myToken.revokeRole(keccak256("MINTER_ROLE"), arbitrary);

vm.prank(arbitrary);
vm.expectRevert("Caller is not a minter");
myToken.mint(address(this), 1);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After we invoke role of  arbitrary,he can't mint ERC20 token anymore.&lt;/p&gt;

&lt;p&gt;If you want the whole example codes go for:&lt;a href="https://gist.github.com/coffiasd/340241d63980dc9d423e4ece2f9b20db" rel="noopener noreferrer"&gt;https://gist.github.com/coffiasd/340241d63980dc9d423e4ece2f9b20db&lt;/a&gt;&lt;br&gt;
If you have any questions, you can also reach me through the contact information below.&lt;/p&gt;

&lt;p&gt;Origin:&lt;a href="https://blog.coffiasd.cn/03-07-26.html" rel="noopener noreferrer"&gt;https://blog.coffiasd.cn/03-07-26.html&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  let's connect
&lt;/h2&gt;

&lt;p&gt;Twitter:&lt;a href="https://twitter.com/coffiasse" rel="noopener noreferrer"&gt;https://twitter.com/coffiasse&lt;/a&gt;&lt;br&gt;
Github:&lt;a href="https://github.com/coffiasd" rel="noopener noreferrer"&gt;https://github.com/coffiasd&lt;/a&gt;&lt;br&gt;
TG:@coffiasd&lt;/p&gt;

</description>
    </item>
    <item>
      <title>How to use solidity CREATE and CREATE2</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Thu, 27 Jul 2023 04:55:33 +0000</pubDate>
      <link>https://dev.to/pandasecurity/how-to-use-solidity-create-and-create2-25hb</link>
      <guid>https://dev.to/pandasecurity/how-to-use-solidity-create-and-create2-25hb</guid>
      <description>&lt;h2&gt;
  
  
  Overview
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi04z0gzhvaekjmlcr8vd.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fi04z0gzhvaekjmlcr8vd.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;There are 2 major ways to deploy smart contract &lt;code&gt;CREATE&lt;/code&gt; and &lt;code&gt;CREATE2&lt;/code&gt;. They look quite similar, but there are still some differences.CREATE2 and CREATE are one of solidity opcode which give us the ability to predict the address where a contract will be deployed before we deploy a smart contract.You might be curious about what opcode is.Opcodes are the fundamental building blocks of EVM bytecode, and each opcode represents a specific operation that the EVM can perform. For example, there are opcodes for arithmetic operations, logical operations, storage access, memory manipulation, conditional branching, and more.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is CREATE
&lt;/h2&gt;

&lt;p&gt;Smart contracts can be created both by other contracts and regular EOA.They both compute the new address the same way:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;new_address &lt;span class="o"&gt;=&lt;/span&gt; keccak256&lt;span class="o"&gt;(&lt;/span&gt;sender, nonce&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So each created address is associated with a nonce value.Nonce increased on every transaction.It is related to the number of transactions we make and is unpredictable. That's why we need CREATE2.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is CREATE2 ?
&lt;/h2&gt;

&lt;p&gt;The whole idea behind this opcode is to make the resulting address independent of future events. Regardless of what may happen on the blockchain, it will always be possible to deploy the contract at the precomputed address.There are four parameters in CREATE2 function:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;0xFF&lt;/li&gt;
&lt;li&gt;the address of sender&lt;/li&gt;
&lt;li&gt;A salt (random value from sender)&lt;/li&gt;
&lt;li&gt;bytecode (the code of the new contract)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;new_address &lt;span class="o"&gt;=&lt;/span&gt; keccak256&lt;span class="o"&gt;(&lt;/span&gt;0xFF, sender, salt, bytecode&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why we need CREATE2 ?
&lt;/h2&gt;

&lt;p&gt;Imagine a scenario where you need to deploy a contract to multiple networks, and precisely at that moment, you need to store the addresses of the yet-to-be-deployed contracts as storage parameters within the currently deployed contract. In such cases, you would require knowing the future addresses of the contracts to be deployed in advance.&lt;/p&gt;

&lt;h2&gt;
  
  
  How to use it
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Use hash function to calculate address.
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    uint256 _salt = 1;
    bytes32 hash = keccak256(
        abi.encodePacked(
            bytes1(0xff),
            address(this),
            _salt,
            keccak256(type(TokenTemplate).creationCode)
        )
    );
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Use deploy to get the deployed address.
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    function deploy(
        bytes memory bytecode,
        uint _salt
    ) public payable returns (address) {
        address addr;

        assembly {
            addr := create2(
                callvalue(), // wei sent with current call
                // Actual code starts after skipping the first 32 bytes
                add(bytecode, 0x20),
                mload(bytecode), // Load the size of code contained in the first 32 bytes
                _salt // Salt from function arguments
            )

            if iszero(extcodesize(addr)) {
                revert(0, 0)
            }
        }

        return addr;
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;you can find the whole code on github:&lt;br&gt;
&lt;a href="https://gist.github.com/coffiasd/c2ff1a2d718d286b9064873be2fe079a" rel="noopener noreferrer"&gt;https://gist.github.com/coffiasd/c2ff1a2d718d286b9064873be2fe079a&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Origin: &lt;a href="https://blog.coffiasd.cn/03-07-24.html" rel="noopener noreferrer"&gt;https://blog.coffiasd.cn/03-07-24.html&lt;/a&gt;&lt;br&gt;
Follow my twitter:&lt;a href="https://twitter.com/coffiasse" rel="noopener noreferrer"&gt;https://twitter.com/coffiasse&lt;/a&gt;&lt;br&gt;
TG:@coffiasd&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Merkle树的逻辑和证明</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Mon, 17 Jul 2023 05:29:43 +0000</pubDate>
      <link>https://dev.to/pandasecurity/merkleshu-de-luo-ji-he-zheng-ming-4an</link>
      <guid>https://dev.to/pandasecurity/merkleshu-de-luo-ji-he-zheng-ming-4an</guid>
      <description>&lt;h2&gt;
  
  
  什么是Merkle树
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9onfwb5psfxgpgvmki52.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F9onfwb5psfxgpgvmki52.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  定义
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;Merkle Tree&lt;/code&gt;，也叫默克尔树或哈希树，是区块链的底层加密技术，被比特币和以太坊区块链广泛采用。Merkle Tree是一种自下而上构建的加密树，每个叶子是对应数据的哈希，而每个非叶子为它的2个子节点的哈希。&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb3h5d2gsr82lqbj9qbs9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fb3h5d2gsr82lqbj9qbs9.png" alt=" " width="799" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  如何生成Merkle树的数据
&lt;/h3&gt;

&lt;p&gt;在solidity中我们通过keccak256算法计算hash值:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;keccak256(abi.encodePacked(toHashValue)

e.g.:
hash前
0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2

hash后
0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;在对叶子节点的值进行hash运算之后，再把相邻的节点再进行hash运算，直到只剩下一个根节点。&lt;br&gt;
假设存在两个相邻的节点A和B,那么在进行hash运算的时候到地址是hash(A+B)呢？还是hash(B+A)呢？其实这是由A和B的大小决定的，在openzeppelin对应的merkle代码中我们可以找到这么一段代码:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) {
        return a &amp;lt; b ? _efficientHash(a, b) : _efficientHash(b, a);
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;总结来说就是把相对小的数值放到前面去这么来排序计算hash值。这个地方在自己动手实际运算的时候可能会有些许困惑。&lt;br&gt;
在实际的项目中一般只需要把计算的最后结果的根hash值存储到合约中，如果大量的地址都需要存到合约中的话会消耗大量的gas费。经过merkle树计算之后，大大的减少了需要存储的数据。&lt;br&gt;
通过一段foundry的setUp演示下如何计算和存储root hash值:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
    bytes32 public root;
    bytes32[] public leafs;
    bytes32[] public l2;

    function setUp() public {
        address[] memory addrss = new address[](4);
        addrss[0] = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;
        addrss[1] = 0x2d886570A0dA04885bfD6eb48eD8b8ff01A0eb7e;
        addrss[2] = 0xed857ac80A9cc7ca07a1C213e79683A1883df07B;
        addrss[3] = 0x690B9A9E9aa1C9dB991C7721a92d351Db4FaC990;

        //通过地址列表计算叶子节点的hash值
        leafs.push(keccak256(abi.encodePacked(addrss[0])));
        leafs.push(keccak256(abi.encodePacked(addrss[1])));
        leafs.push(keccak256(abi.encodePacked(addrss[2])));
        leafs.push(keccak256(abi.encodePacked(addrss[3])));

        //计算第二层的hash值
        l2.push(keccak256(abi.encodePacked(leafs[0], leafs[1])));
        l2.push(keccak256(abi.encodePacked(leafs[2], leafs[3])));

        //计算根的hash值
        root = keccak256(abi.encodePacked(l2[1], l2[0]));
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;为了演示方便我们值写了4个地址，实际项目中可能地址数量非常大。&lt;/p&gt;

&lt;h2&gt;
  
  
  如何来验证Merkle树
&lt;/h2&gt;

&lt;p&gt;在合约中存储到root hash值之后我们如何去验证由客户端发过来的地址是否是有效地址或者说在白名单中的地址呢？&lt;br&gt;
首先我们需要将地址进行hash运算，作为第三个参数，然后将地址相邻的hash值作为proof传到验证函数中。&lt;br&gt;
proof列表对应下面图片中的红色标记区域&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fie841nie2fo1qt4e8ana.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fie841nie2fo1qt4e8ana.png" alt=" " width="799" height="625"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;测试的验证方法:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    function testVerify() public {
        address proofAddress = 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2;

        bytes32[] memory proof = new bytes32[](2);
        proof[0] = leafs[1];
        proof[1] = l2[1];

        assert(
            MerkleProof.verify(
                proof,
                root,
                keccak256(abi.encodePacked(proofAddress))
            )
        );
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;完整的foundry测试代码:&lt;a href="https://gist.github.com/coffiasd/96022abd7f9a8a2189bda14bf9e755dc" rel="noopener noreferrer"&gt;https://gist.github.com/coffiasd/96022abd7f9a8a2189bda14bf9e755dc&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  在实际项目中的应用场景
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;发放空投&lt;/li&gt;
&lt;li&gt;NFT的白名单&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  在合约审计中的常见漏洞
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;function parentHash(bytes32 a, bytes32 b) public pure returns (bytes32) {
        if (a &amp;lt; b) {
            return keccak256(abi.encode(a, b));
        } else {
            return keccak256(abi.encode(b, a));
        }
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;abi.encode(address, uint)将会输出64字节。由于abi.encode(bytes32, bytes32)也是64字节，因此在叶子节点和父节点之间可能会发生哈希碰撞。&lt;/p&gt;

&lt;p&gt;Follow我的推进行交流学习:&lt;a href="https://twitter.com/nifty12111" rel="noopener noreferrer"&gt;https://twitter.com/nifty12111&lt;/a&gt;&lt;br&gt;
TG:@coffiasd&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Solidity Assembly的基础用法</title>
      <dc:creator>PandaSecurity</dc:creator>
      <pubDate>Sat, 15 Jul 2023 02:23:06 +0000</pubDate>
      <link>https://dev.to/pandasecurity/solidity-assemblyde-ji-chu-yong-fa-499k</link>
      <guid>https://dev.to/pandasecurity/solidity-assemblyde-ji-chu-yong-fa-499k</guid>
      <description>&lt;h2&gt;
  
  
  什么是Assembly
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh97shde4euu7d9nrpbaj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fh97shde4euu7d9nrpbaj.png" alt=" " width="800" height="420"&gt;&lt;/a&gt;&lt;br&gt;
在编写Solidity代码时，我们可以使用&lt;code&gt;assembly{}&lt;/code&gt;关键字开始编写Yul代码，它是一种简化且扩展了的汇编语言。通过使用assembly，我们可以直接访问堆栈，并优化代码以提高内存效率，从而减少执行交易所需的燃气量。这最终降低了用户的交易成本。&lt;/p&gt;

&lt;p&gt;然而，在可读性方面存在一些妥协。许多前端开发人员可以阅读Solidity智能合约并理解正在执行的功能以及如何将其应用到他们的web3查询中。相比之下，汇编可能会让人感到有些困惑，如果你不熟悉低级编程，可能很难理解其逻辑和流程。&lt;/p&gt;
&lt;h2&gt;
  
  
  操作码列表
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;操作码&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;add(x, y)&lt;/td&gt;
&lt;td&gt;将栈中的前两个值相加，并用结果替换它们&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sub(x, y)&lt;/td&gt;
&lt;td&gt;减法操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mul(x, y)&lt;/td&gt;
&lt;td&gt;乘法操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;div(x, y)&lt;/td&gt;
&lt;td&gt;除法操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mod(x, y)&lt;/td&gt;
&lt;td&gt;取模操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lt(x, y)&lt;/td&gt;
&lt;td&gt;小于。如果 x 小于 y，则返回 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;gt(x, y)&lt;/td&gt;
&lt;td&gt;大于&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;eq(x, y)&lt;/td&gt;
&lt;td&gt;等于。如果 x 等于 y，则返回 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;not(x)&lt;/td&gt;
&lt;td&gt;按位取反 (1010 &amp;gt; 0101)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;and(x, y)&lt;/td&gt;
&lt;td&gt;按位与 (1000 AND 1100 &amp;gt; 1000)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;or(x, y)&lt;/td&gt;
&lt;td&gt;按位或 (1000 AND 1100 &amp;gt; 1100)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;xor(x, y)&lt;/td&gt;
&lt;td&gt;按位异或 (1000 XOR 1100 &amp;gt; 0100)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;byte(n, x)&lt;/td&gt;
&lt;td&gt;X 的第 N 个字节&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;keccak256(pos, n)&lt;/td&gt;
&lt;td&gt;本地哈希算法&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;pop(x)&lt;/td&gt;
&lt;td&gt;从栈中弹出 X&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mload(pos)&lt;/td&gt;
&lt;td&gt;加载存储在位置 pos 的内存数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;mstore(pos, value)&lt;/td&gt;
&lt;td&gt;在位置 pos 的memory中存储值 value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sload(pos)&lt;/td&gt;
&lt;td&gt;加载存储在位置 pos 的存储数据&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;sstore(pos, value)&lt;/td&gt;
&lt;td&gt;在位置 pos 的storage中存储值 value&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;balance(address)&lt;/td&gt;
&lt;td&gt;以 wei 为单位返回地址的以太币余额&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;call(gas, address, value, in, insize, out, outsize)&lt;/td&gt;
&lt;td&gt;调用外部合约，也可用于发送资金，成功时返回 1&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;delegatecall(gas, address, value, in, insize, out, outsize)&lt;/td&gt;
&lt;td&gt;与上述相同，但由用户而不是合约进行调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;revert(p, s)&lt;/td&gt;
&lt;td&gt;撤销事务和任何状态更改&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;return(p, s)&lt;/td&gt;
&lt;td&gt;结束执行并返回数据&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;这里需要注意的是&lt;code&gt;mstore&lt;/code&gt;和&lt;code&gt;sstore&lt;/code&gt;的区别，主要存储的位置的不一致，一种是存储在memory中，另一种是存储在storage中。&lt;/p&gt;
&lt;h2&gt;
  
  
  使用案例
&lt;/h2&gt;
&lt;h3&gt;
  
  
  读写
&lt;/h3&gt;

&lt;p&gt;下面我们通过一个例子展示下如何使用&lt;code&gt;Assembly&lt;/code&gt;把数据存储到storage中，然后通过函数读取的方式读出来。&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    function testAssembly() public {
        //定义一个存储的值方便测试.
        uint256 x = 123;
        bool res;

        //将x存储到storage中。
        assembly {
            sstore(0, x)
        }

        //在storage中读取x的值,其中0是pos
        assembly {
            let readX := sload(0)
            //将readX的值存储到memory,pos == 0x80的位置。
            mstore(0x80, readX)
        }

        //将0x80中的值取出来.
        assembly {
            let v := mload(0x80)
            if eq(v, x) {
                res := true
            }
        }

        //判断最终取出来的v == x?
        assert(res);
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;上诉的例子在实际项目中不太可能出现，只是为了展示读写的过程。&lt;/p&gt;

&lt;h3&gt;
  
  
  基础运算
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;    function testAssembly() public {
        //定义一个存储的值方便测试.
        uint256 x = 123;
        uint256 r = 345;

        //将x存储到storage中。
        assembly {
            x := add(x, 300)
            x := sub(x, 78)
        }

        //判断最终取出来的r == x?
        assert(r == x);
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Follow我的推进行交流学习:&lt;a href="https://twitter.com/nifty12111" rel="noopener noreferrer"&gt;https://twitter.com/nifty12111&lt;/a&gt;&lt;br&gt;
TG:@coffiasd&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
