<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Surfing Solodit]]></title><description><![CDATA[Audit-driven smart contract security research.]]></description><link>https://surfing-solodit.com</link><generator>RSS for Node</generator><lastBuildDate>Wed, 15 Apr 2026 10:28:31 GMT</lastBuildDate><atom:link href="https://surfing-solodit.com/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Uniswap V4 Hooks Security Deep Dive]]></title><description><![CDATA[Building on a previous article written for the Cyfrin blog (as featured in the BlockThreat newsletter) that covered the main architectural changes and technical innovations of Uniswap v4, this article categorizes and analyzes findings from public aud...]]></description><link>https://surfing-solodit.com/uniswap-v4-hooks-security-deep-dive</link><guid isPermaLink="true">https://surfing-solodit.com/uniswap-v4-hooks-security-deep-dive</guid><category><![CDATA[uniswap]]></category><category><![CDATA[uniswap v4]]></category><category><![CDATA[Smart Contracts]]></category><category><![CDATA[Blockchain]]></category><dc:creator><![CDATA[Giovanni Di Siena]]></dc:creator><pubDate>Fri, 31 Oct 2025 18:00:59 GMT</pubDate><content:encoded><![CDATA[<p>Building on a <a target="_blank" href="https://www.cyfrin.io/blog/uniswap-v4-vs-v3-architectural-changes-and-technical-innovations-with-code-examples">previous article written for the Cyfrin blog</a> (as featured in the <a target="_blank" href="https://newsletter.blockthreat.io/p/blockthreat-week-45-2024">BlockThreat newsletter</a>) that covered the main architectural changes and technical innovations of Uniswap v4, this article categorizes and analyzes findings from public audit reports specifically related to custom v4 hooks. For background and framing, resources such as the <a target="_blank" href="https://github.com/Uniswap/v4-core/blob/main/docs/security/Known_Effects_of_Hook_Permissions.pdf">Known Effects of Hook Permissions</a> by Uniswap Labs and <a target="_blank" href="https://github.com/ComposableSecurity/SCSVS/blob/master/2.0/0x200-Components/0x209-C9-Uniswap-V4-Hook.md">this checklist</a> by Composable Security provide useful reference points.</p>
<p>As highlighted in this <a target="_blank" href="https://blocksec.com/blog/thorns-in-the-rose-exploring-security-risks-in-uniswap-v4-s-novel-hook-mechanism#threat-models">BlockSec threat model</a>, hooks can either be benign but vulnerable or intentionally malicious. This article focuses on the former, in which there is some overlap between categories; however, it is equally important to account for the latter, as well as upgradeability risks, private key compromise, and other centralisation concerns, when interacting with unverified or otherwise unvetted hooks.</p>
<p>It is also worth noting that potential attack vectors can arise from existing, well-known smart contract vulnerabilities that manifest within the hook itself. While a few particularly interesting examples will be included, this article focuses primarily on exploits and other issues that are specific to Uniswap v4 insofar as they relate to custom hooks taking custody of funds and managing critical application state.</p>
<h2 id="heading-hooks-and-permissions">Hooks and permissions</h2>
<h3 id="heading-deploying-hooks-with-permissions-that-are-not-implemented">Deploying hooks with permissions that are not implemented</h3>
<p>Hook permissions are encoded in least significant bits of the hook address and compared against <a target="_blank" href="https://github.com/Uniswap/v4-core/blob/80311e34080fee64b6fc6c916e9a51a437d0e482/src/libraries/Hooks.sol#L28-L46">flags</a> to determine whether it should implement the corresponding function:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">hasPermission</span>(<span class="hljs-params">IHooks <span class="hljs-built_in">self</span>, <span class="hljs-keyword">uint160</span> flag</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">pure</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span></span>) </span>{
    <span class="hljs-keyword">return</span> <span class="hljs-keyword">uint160</span>(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">self</span>)) <span class="hljs-operator">&amp;</span> flag <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
}
</code></pre>
<p>If a hook lacks the necessary permissions for a given function then it effectively behaves as a no-op; however, if the hook address encodes a certain permission for which the corresponding function is not implemented then (without any fallback logic) this will result in a revert.</p>
<p>The v4 periphery <code>BaseHook</code> <a target="_blank" href="https://github.com/Uniswap/v4-periphery/blob/60cd93803ac2b7fa65fd6cd351fd5fd4cc8c9db5/src/utils/BaseHook.sol#L18-L33">abstract contract</a> is designed to avoid deploying such a hook by validating that the deployed hook address agrees with the expected permissions of the hook. Similar and more extended functionality is provided by the <a target="_blank" href="https://github.com/OpenZeppelin/uniswap-hooks/blob/master/src/base/BaseHook.sol">OpenZeppelin implementation</a>.</p>
<p><strong>Heuristic:</strong> does the hook inherit an abstract <code>BaseHook</code> implementation? If not, are there checks in place to avoid deploying a hook that encodes specific permissions but does not implement the corresponding functions?</p>
<h3 id="heading-deploying-hooks-without-permissions-required-for-implemented-functions">Deploying hooks without permissions required for implemented functions</h3>
<p>As mentioned above, if a hook lacks the necessary permissions for a given function then its execution will be skipped. Counter to the case in which the permission is granted without implementing the necessary functionality, failing to encode the necessary permissions for implemented functions will most likely result in unexpected and undesired behaviour as key logic is omitted.</p>
<p>In <a target="_blank" href="https://solodit.cyfrin.io/issues/all-swaps-will-revert-if-the-dynamic-protocol-fee-is-enabled-since-hook-configsol-does-not-encode-the-afterswapreturndelta-permission-cyfrin-none-sorella-l2-angstrom-markdown">this example</a>, a protocol fee is intended to be taken in the <code>afterSwap()</code> hook; however, absence of the <code>AFTER_SWAP_RETURNS_DELTA_FLAG</code> permission results in a denial-of-service (DoS) of all swaps once the protocol fee is enabled.</p>
<p>The contract creation code in addition to the salt and deployer address determine the hook address, so different constructor parameters and deployer addresses will result in different hook addresses. Again, use of one of the base hook implementations will help to verify that the permissions of the derived hook address are encoded as intended:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">/// @notice Utility function intended to be used in hook constructors to ensure</span>
<span class="hljs-comment">/// the deployed hooks address causes the intended hooks to be called</span>
<span class="hljs-comment">/// @param permissions The hooks that are intended to be called</span>
<span class="hljs-comment">/// @dev permissions param is memory as the function will be called from constructors</span>
 <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">validateHookPermissions</span>(<span class="hljs-params">IHooks <span class="hljs-built_in">self</span>, Permissions <span class="hljs-keyword">memory</span> permissions</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">pure</span></span> </span>{
    <span class="hljs-keyword">if</span> (
        permissions.beforeInitialize <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-built_in">self</span>.hasPermission(BEFORE_INITIALIZE_FLAG)
            <span class="hljs-operator">|</span><span class="hljs-operator">|</span> permissions.afterInitialize <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-built_in">self</span>.hasPermission(AFTER_INITIALIZE_FLAG)
            <span class="hljs-operator">|</span><span class="hljs-operator">|</span> permissions.beforeAddLiquidity <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-built_in">self</span>.hasPermission(BEFORE_ADD_LIQUIDITY_FLAG)
            <span class="hljs-operator">|</span><span class="hljs-operator">|</span> permissions.afterAddLiquidity <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-built_in">self</span>.hasPermission(AFTER_ADD_LIQUIDITY_FLAG)
            <span class="hljs-operator">|</span><span class="hljs-operator">|</span> permissions.beforeRemoveLiquidity <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-built_in">self</span>.hasPermission(BEFORE_REMOVE_LIQUIDITY_FLAG)
            <span class="hljs-operator">|</span><span class="hljs-operator">|</span> permissions.afterRemoveLiquidity <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-built_in">self</span>.hasPermission(AFTER_REMOVE_LIQUIDITY_FLAG)
            <span class="hljs-operator">|</span><span class="hljs-operator">|</span> permissions.beforeSwap <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-built_in">self</span>.hasPermission(BEFORE_SWAP_FLAG)
            <span class="hljs-operator">|</span><span class="hljs-operator">|</span> permissions.afterSwap <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-built_in">self</span>.hasPermission(AFTER_SWAP_FLAG)
            <span class="hljs-operator">|</span><span class="hljs-operator">|</span> permissions.beforeDonate <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-built_in">self</span>.hasPermission(BEFORE_DONATE_FLAG)
            <span class="hljs-operator">|</span><span class="hljs-operator">|</span> permissions.afterDonate <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-built_in">self</span>.hasPermission(AFTER_DONATE_FLAG)
            <span class="hljs-operator">|</span><span class="hljs-operator">|</span> permissions.beforeSwapReturnDelta <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-built_in">self</span>.hasPermission(BEFORE_SWAP_RETURNS_DELTA_FLAG)
            <span class="hljs-operator">|</span><span class="hljs-operator">|</span> permissions.afterSwapReturnDelta <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-built_in">self</span>.hasPermission(AFTER_SWAP_RETURNS_DELTA_FLAG)
            <span class="hljs-operator">|</span><span class="hljs-operator">|</span> permissions.afterAddLiquidityReturnDelta <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-built_in">self</span>.hasPermission(AFTER_ADD_LIQUIDITY_RETURNS_DELTA_FLAG)
            <span class="hljs-operator">|</span><span class="hljs-operator">|</span> permissions.afterRemoveLiquidityReturnDelta
                <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-built_in">self</span>.hasPermission(AFTER_REMOVE_LIQUIDITY_RETURNS_DELTA_FLAG)
    ) {
        HookAddressNotValid.<span class="hljs-built_in">selector</span>.revertWith(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">self</span>));
    }
}
</code></pre>
<p><strong>Heuristic:</strong> does the hook inherit an abstract <code>BaseHook</code> implementation? If not, are there checks in place to avoid deploying a hook that implements a specific function without encoding the corresponding permission?</p>
<h3 id="heading-unimplemented-functions-allowing-bypass-through-underlying-uniswap-v4-contracts">Unimplemented functions allowing bypass through underlying Uniswap v4 contracts</h3>
<p>When focusing on a specific hook implementation, it is easy to forget about the entrypoints that still exist on Uniswap v4 itself. Even with all hook permissions correctly encoded for the functions that are expected to be exposed, there can be instances in which unimplemented functions should have actually been implemented (and the permissions set accordingly). This issue can take many forms depending on the context but ultimately arises because the business logic of the application may dictate that certain actions should only be performed through the hook or a related contract.</p>
<p>For instance, it may be intended to <a target="_blank" href="https://github.com/pashov/audits/blob/master/team/md/Bunni-security-review-August.md#l-03-inconsistencies-in-pool-balances-due-to-external-deposits">restrict modifying liquidity to happen exclusively through the hook</a>, but be overlooked that it would still be possible to do so directly through the <code>PoolManager</code> contract. Unless the relevant liquidity modification hooks are implemented to revert in this case, it is possible to circumvent execution of intended hook logic.</p>
<p>In <a target="_blank" href="https://solodit.cyfrin.io/issues/jit-liquidity-penalty-can-be-bypassed-openzeppelin-none-openzeppelin-uniswap-hooks-v110-rc-1-audit-markdown">this example</a>, the intended behaviour of a hook designed to penalize just-in-time (JIT) liquidity provision in the <code>afterRemoveLiquidity()</code> function can be bypassed by first increasing the liquidity to collect all the fees accrued from swaps. This resets the fee state and hence bypasses the penalty mechanism. In this case, the solution is to extend the hook permissions to additionally track fee collection that occurs during liquidity additions within the <code>beforeAddLiquidity()</code> function.</p>
<p>Another simpler <a target="_blank" href="https://solodit.cyfrin.io/issues/donations-can-be-made-directly-to-the-uniswap-v4-pool-due-to-missing-overrides-cyfrin-none-paladin-valkyrie-markdown">instance</a> is when it may not desirable to allow donations. Without activating the <code>beforeDonate()</code> hook to always revert on donations, calls to <code>PoolManager::donate</code> would succeed in increasing the fee growth.</p>
<p><strong>Heuristic:</strong> can actions that are intended to originate from the hook or otherwise alter the behaviour of other implemented hook functions be executed directly on the underlying Uniswap v4 contracts? Are the necessary hooks implemented to take control of execution in these cases?</p>
<h3 id="heading-insufficient-pool-key-validation">Insufficient pool key validation</h3>
<p>Uniswap v4 does not restrict who can create new liquidity pools, nor which hook address to use in a new liquidity pool. If a hook is not restricted to a specific pool or set of pools, an attacker could deploy a malicious pool with fake tokens using and abusing the hook through attack vectors such as reentrancy or manipulation of the internal accounting.</p>
<p>As with all unsafe external calls and caller-supplied inputs, insufficient validation can potentially compromise the hook contract depending on the context, so hooks that are designed for specific pools should validate token pairs specified within the pool key during initialization. Hook functions should in general also grant permissioned access to only the specific subset of Uniswap v4 pools that are intended to use and interact with the given hook contract.</p>
<p><a target="_blank" href="https://certora.cdn.prismic.io/certora/Z6zPyJbqstJ9-ib-_DopplerSecurityAssessmentReport_Final.pdf">C-01</a> details a variation of this vulnerability in which the victim contract, intended to coordinate all ecosystem hooks and associated contracts, can be drained by specification of a malicious pool key due to insufficient input validation of the hook and currency addresses.</p>
<p>More examples: [<a target="_blank" href="https://certora.cdn.prismic.io/certora/Z6zPyJbqstJ9-ib-_DopplerSecurityAssessmentReport_Final.pdf">C-02</a>, <a target="_blank" href="https://certora.cdn.prismic.io/certora/Z6zPyJbqstJ9-ib-_DopplerSecurityAssessmentReport_Final.pdf">H-01</a>, <a target="_blank" href="https://github.com/GuardianAudits/Audits/blob/main/GammaStrategies/2025-04-14_Gamma_UniswapV4_LimitOrders.pdf">M-02</a>, <a target="_blank" href="https://github.com/pashov/audits/blob/master/team/md/Bunni-security-review-August.md#l-12-lack-of-pool-existence-check-in-setamammenabledoverride">L-12</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/insufficient-validation-on-the-pool-key-in-liquidity-modifying-functions-cyfrin-none-paladin-valkyrie-markdown">5</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/bunni-tokens-can-be-deployed-with-arbitrary-hooks-cyfrin-none-bunni-markdown">6</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/51">7</a>].</p>
<p><strong>Heuristic:</strong> are the pool key and associated currency addresses validated to ensure that permissioned access is granted only to the specific subset of Uniswap v4 pools expected to use and interact with the hook?</p>
<h2 id="heading-hook-callbacks">Hook callbacks</h2>
<h3 id="heading-insufficient-callback-access-controls">Insufficient callback access controls</h3>
<p>Similar to other types of callback, such as those invoked by flash loans providers, the v4 <code>unlockCallback()</code> and other hook function callbacks should in general be callable only by the singleton <code>PoolManager</code> contract. These details may differ slightly depending on the specifics of a given protocol design, in which case careful management of access control is of even more pertinent concern.</p>
<p>As above, protections can be implemented by inheriting one of the <code>BaseHook</code> implementations along with the <code>SafeCallback</code> <a target="_blank" href="https://github.com/Uniswap/v4-periphery/blob/fdb5d3ea09d07eef380edf1a329140c98ff39dbb/src/base/SafeCallback.sol#L14-L20">base contract</a> and overriding <code>_unlockCallback()</code>.</p>
<pre><code class="lang-solidity"><span class="hljs-comment">/// @dev We force the onlyPoolManager modifier by exposing a virtual function after the onlyPoolManager check.</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">unlockCallback</span>(<span class="hljs-params"><span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> data</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">onlyPoolManager</span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span></span>) </span>{
    <span class="hljs-keyword">return</span> _unlockCallback(data);
}

<span class="hljs-comment">/// @dev to be implemented by the child contract, to safely guarantee the logic is only executed by the PoolManager</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_unlockCallback</span>(<span class="hljs-params"><span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> data</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span></span>)</span>;
</code></pre>
<p>Perhaps the most high profile example of this attack vector was its use as a lever in the <a target="_blank" href="https://rekt.news/cork-protocol-rekt">$12 million Cork Protocol exploit</a> in which the <code>beforeSwap()</code> function was called by an attacker with malicious hook data.</p>
<p>Other examples include:</p>
<ul>
<li><p><a target="_blank" href="https://certora.cdn.prismic.io/certora/Z6zPyJbqstJ9-ib-_DopplerSecurityAssessmentReport_Final.pdf">H-02</a> in which is insufficient access control allowed any address to call <code>beforeInitialize()</code> directly to overwrite the stored pool key.</p>
</li>
<li><p><a target="_blank" href="https://github.com/GuardianAudits/Audits/blob/main/GammaStrategies/2025-04-14_Gamma_UniswapV4_LimitOrders.pdf">C-06</a> in which is insufficient access control allowed any address to call <code>beforeSwap()</code> and <code>afterSwap()</code>, undermining the core limit order mechanism.</p>
</li>
<li><p>The example <a target="_blank" href="https://github.com/saucepoint/v4-stoploss/blob/881a13ac3451b0cdab0e19e122e889f1607520b7/src/StopLoss.sol#L67-L72">v4-stoploss hook</a> which allows any address to call <code>afterSwap()</code> with arbitrary parameters, again undermining the limit order mechanism.</p>
</li>
</ul>
<p><strong>Heuristic:</strong> does the hook inherit an abstract <code>BaseHook</code> implementation? Does the hook or related contract(s) inherit the abstract <code>SafeCallback</code>? If not, are there checks in place to prevent unprivileged callers from invoking <code>unlockCallback()</code> and other hook functions?</p>
<h3 id="heading-incorrect-callback-return-data-encoding">Incorrect callback return data encoding</h3>
<p>The length of data returned by a hook callback should at least be sufficient to contain the 4 byte selector, <a target="_blank" href="https://github.com/Uniswap/v4-core/blob/80311e34080fee64b6fc6c916e9a51a437d0e482/src/libraries/Hooks.sol#L150-L153">encoded in 32 bytes</a>:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// Length must be at least 32 to contain the selector. Check expected selector and returned selector match.</span>
<span class="hljs-keyword">if</span> (result.<span class="hljs-built_in">length</span> <span class="hljs-operator">&lt;</span> <span class="hljs-number">32</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span> result.parseSelector() <span class="hljs-operator">!</span><span class="hljs-operator">=</span> data.parseSelector()) {
    InvalidHookResponse.<span class="hljs-built_in">selector</span>.revertWith();
}
</code></pre>
<p>For the hook functions invoked using <code>Hooks::callHookWithReturnDelta</code> and expecting to parse a return delta, the length of data should be <a target="_blank" href="https://github.com/Uniswap/v4-core/blob/80311e34080fee64b6fc6c916e9a51a437d0e482/src/libraries/Hooks.sol#L164-L166">exactly 64 bytes</a> to contain both the encoded selector plus the 32 byte delta:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// A length of 64 bytes is required to return a bytes4, and a 32 byte delta</span>
<span class="hljs-keyword">if</span> (result.<span class="hljs-built_in">length</span> <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">64</span>) InvalidHookResponse.<span class="hljs-built_in">selector</span>.revertWith();
<span class="hljs-keyword">return</span> result.parseReturnDelta();
</code></pre>
<p><code>Hooks::beforeSwap</code> additionally expects a 3 byte fee override and <a target="_blank" href="https://github.com/Uniswap/v4-core/blob/80311e34080fee64b6fc6c916e9a51a437d0e482/src/libraries/Hooks.sol#L257-L258">enforces a length of 96 bytes</a>:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// A length of 96 bytes is required to return a bytes4, a 32 byte delta, and an LP fee</span>
<span class="hljs-keyword">if</span> (result.<span class="hljs-built_in">length</span> <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">96</span>) InvalidHookResponse.<span class="hljs-built_in">selector</span>.revertWith();
</code></pre>
<p>If the length of return data is not as expected, either because the incorrect types are encoded or otherwise, then execution will revert.</p>
<p><strong>Heuristic:</strong> does the hook inherit an abstract <code>BaseHook</code> implementation? If not, are the hook function signatures and return data lengths as expected by the <code>Hooks</code> library?</p>
<h3 id="heading-before-vs-after-hooks">Before vs after hooks</h3>
<p>It may not seem important whether some functionality is implemented in a hook that executes before an operation versus one that executes afterwards, and indeed in certain circumstances this may be perfectly fine; however, hook developers and auditors should be clear on the assumptions that are being made about the placement of such logic as this can give rise to a category of very subtle bugs.</p>
<p>Consider the example of some custom incentives logic. Say, perhaps, that it intends to clean up critical state used in such computations upon complete removal of liquidity. If this is performed in the <code>beforeRemoveLiquidity()</code> hook rather than <code>afterRemoveLiquidity()</code> then at the point of execution the liquidity will remain unchanged and the state will never be updated, the consequence being that incentives will be computed for liquidity positions that no longer exist. While this seems obvious in hindsight, it can be challenging to spot as complexity grows.</p>
<p>This could also play out in the reverse scenario, as in <a target="_blank" href="https://solodit.cyfrin.io/issues/rewardgrowthoutsidex128-is-not-correctly-initialized-in-poolrewardsupdateafterliquidityadd-cyfrin-none-sorella-l2-angstrom-markdown">this example</a> in which tick initialization logic present in the <code>afterAddLiquidity()</code> hook should have been placed in <code>beforeAddLiquidity()</code>. Executing after the core liquidity modification logic has already run means the tick will already have been initialized once execution returns to the hook and so the conditional logic will never be triggered.</p>
<p>More examples: [<a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/49">1</a>].</p>
<p><strong>Heuristic:</strong> is hook logic dependent on whether it is executed before or after the core Uniswap v4 logic? Will liquidity, tick initialization state, etc differ depending on the choice of before/after hook? Will a mistake here cause key logic to execute differently, incorrectly, or be entirely omitted?</p>
<h2 id="heading-unhandled-reverts">Unhandled reverts</h2>
<p>When reviewing hooks, it is important to be very mindful of potential sources of reverts both in the hook business logic and also based on liquidity modification/swaps.</p>
<h3 id="heading-sync-before-unlock">Sync before unlock</h3>
<p><code>PoolManager::sync</code> has the <code>onlyWhenUnlocked</code> modifier to ensure that the currency and reserves transient storage can only be synchronised after <code>PoolManager::unlock</code> has already been called. If a hook function <a target="_blank" href="https://github.com/pashov/audits/blob/master/team/md/Bunni-security-review-August.md#h-01-function-poolmanagersync-called-before-the-unlock-process">attempts to synchronise the currency reserves before unlocking the <code>PoolManager</code></a>, execution will revert. Instead, this logic should be moved to the unlock callback and executed prior to any custom accounting.</p>
<p><strong>Heuristic:</strong> are calls to <code>sync()</code> always performed following a prior <code>unlock()</code>?</p>
<div data-node-type="callout">
<div data-node-type="callout-emoji">💡</div>
<div data-node-type="callout-text">This is no longer the case in the most recent version of the <code>PoolManager</code> contract – <code>sync()</code> can now be called without first unlocking the singleton.</div>
</div>

<h3 id="heading-unsettled-deltas-from-uncleared-or-unsynchronised-dust">Unsettled deltas from uncleared or unsynchronised dust</h3>
<p>The key invariant of flash accounting and the unlock mechanism is that all deltas must be settled before returning execution from the unlock callback to the <code>PoolManager</code>. Actions such as swapping tokens or modifying liquidity generate deltas which represent the net changes in token balances owed by or to the pool and are accumulated in transient storage. If there are any unsettled deltas at the end of execution, the integrating contract may need to withdraw assets owed to the user using the <code>take()</code> function or deposit assets owed to the pool followed by a call to the <code>settle()</code> function.</p>
<p>In some instances, there may be small “dust” balances that prevent account deltas from being fully settled and so the <code>clear()</code> function should be used to explicitly account this to the pool. This is implemented as a protection against loss of caller funds; however, dust balances can thus be used as a DoS attack vector if this scenario is not properly handled, as in <a target="_blank" href="https://github.com/GuardianAudits/Audits/blob/main/GammaStrategies/2025-04-14_Gamma_UniswapV4_LimitOrders.pdf">H-01</a>.</p>
<p>In similar fashion, <a target="_blank" href="https://github.com/GuardianAudits/Audits/blob/main/GammaStrategies/2025-04-14_Gamma_UniswapV4_LimitOrders.pdf">L-13</a> details a DoS scenario in which tokens are donated but not synchronised in transient storage with a call to <code>sync()</code>, resulting in a revert when attempting to settle the deltas of an operation.</p>
<p><strong>Heuristic:</strong> are there any scenarios in which dust could accumulate and, if so, is it explicitly cleared? Are the currency and reserves always synced in transient storage before performing operations that modify account deltas?</p>
<h3 id="heading-reverts-when-modifying-liquidity">Reverts when modifying liquidity</h3>
<p>Unhandled reverts in either the <code>beforeRemoveLiquidity()</code> or <code>afterRemoveLiquidity()</code> hook can result in liquidity provider (LP) funds being permanently locked unless there is some way of escaping from this DoS state. This scenario can also prevent fees from ever being collected, since unlike Uniswap v3 the delta due to fee growth is required to be settled on every liquidity modification. Reverts during the addition of liquidity are slightly less problematic since this cannot result in funds becoming locked but will still cause severe disruption to the protocol.</p>
<p>Other examples include <a target="_blank" href="https://audits.sherlock.xyz/contests/468/report#:~:text=Issue%20H%2D8%3A%20Liquidity%20provided%20when%20initializing%20a%20collection%20in%20Locker.sol%20will%20be%20stuck%20in%20Uniswap%2C%20with%20no%20way%20for%20the%20user%20to%20recover%20it">H-8</a> in which the initial LP cannot withdraw due to protocol-specific initialization, which as additionally noted by <a target="_blank" href="https://audits.sherlock.xyz/contests/468/report#:~:text=Issue%20M%2D6%3A%20The%20unused%20tokens%20from%20the%20user%E2%80%99s%20initialization%20of%20UniswapV4%E2%80%98s%20pool%20will%20be%20locked%20in%20the%20UniswapImplementation%20contract.">M-6</a> does not refund unused tokens for this initial liquidity provision.</p>
<p><strong>Heuristic:</strong> are there any calls within the liquidity modification hooks that could revert? Are these potentially problematic calls explicitly handled to prevent unexpected reverts?</p>
<h3 id="heading-reverts-in-peripheral-hook-logic">Reverts in peripheral hook logic</h3>
<p>Peripheral logic, such as custom incentives and rebalancing, should be treated with care to avoid DoS and potential loss of funds due to unhandled reverts. The severity of such reverts depends on which functionality is disabled and whether it can be recovered. While an inability to add liquidity and swap against an affected pool represents loss of core functionality, the most impactful cases are those in which funds are permanently locked, for example due to an inability to remove existing liquidity from the pool or withdraw other tokens in the custody of the contract(s).</p>
<p>While not critical, some sources of reverts may <a target="_blank" href="https://solodit.cyfrin.io/issues/swaps-are-not-possible-on-pools-registered-with-bunni-due-to-incorrect-access-control-in-valkyriehookletafterswap-cyfrin-none-paladin-valkyrie-markdown">never allow the intended functionality to run</a>, where incorrect access control prevents an external integration from executing correctly. If the hook is <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/96">not upgradeable</a> then it would need to be redeployed with the fix which could cause severe disruption to the protocol and its users.</p>
<p>Sometimes, the logic may appear sound for one invocation of an operation but then fail on subsequent calls, as in <a target="_blank" href="https://solodit.cyfrin.io/issues/all-swaps-other-than-the-top-of-block-swap-will-revert-cyfrin-none-sorella-l2-angstrom-markdown">this example</a> of a top-of-block swap tax that is erroneously applied to all other swaps and causes a DoS. There may also be <a target="_blank" href="https://solodit.cyfrin.io/issues/swaps-will-revert-when-a-b-xhat-x-0-cyfrin-none-sorella-l2-angstrom-markdown">edge cases in the math</a> used to compute such taxes/prices/etc that can cause some subset of calls to fail.</p>
<p><a target="_blank" href="https://github.com/pashov/audits/blob/master/team/md/Bunni-security-review-August.md#h-05-using-the-same-block-number-as-nonce-for-permit2-order">H-05</a> is an example of DoS due to a failure to account for multiple pool IDs. Certain operations, such as rebalancing, may be possible for multiple pools associated with a given hook within a given block; however, the logic may be such that this is overlooked, restricting the functionality to only a single pool if a distinct nonce is not used.</p>
<p>In the examples [<a target="_blank" href="https://solodit.cyfrin.io/issues/permissionless-reward-distribution-can-be-abused-to-cause-full-denial-of-service-and-loss-of-funds-for-pools-cyfrin-none-paladin-valkyrie-markdown">1</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/reward-tokens-can-become-inaccessible-due-to-revert-when-rewardpertokenstored-is-downcast-and-overflows-cyfrin-none-paladin-valkyrie-markdown">2</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/poolrewards-arrays-can-grow-without-bounds-resulting-in-dos-of-core-functionality-cyfrin-none-paladin-valkyrie-markdown">3</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/denial-of-service-for-swaps-on-multirangehook-pools-cyfrin-none-paladin-valkyrie-markdown">4</a>], permissionless rewards distribution and range creation can be abused to trigger reverts due to overflow and excessive gas usage. This DoS vector is also demonstrated in <a target="_blank" href="https://solodit.cyfrin.io/issues/broken-block-time-assumptions-affect-am-amm-epoch-duration-and-can-dos-rebalancing-on-arbitrum-cyfrin-none-bunni-markdown">rebalancing</a> and <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/1">incentives</a> logic due to differences between chains.</p>
<p>More examples: [<a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/67">1</a>].</p>
<p><strong>Heuristic:</strong> are there any calls in custom logic within hook function implementations that could revert? Are there any peripheral accounting mechanisms that could revert, perhaps due to excessive gas usage or under/overflow? Are these potentially problematic cases explicitly handled to prevent unexpected reverts?</p>
<h2 id="heading-dynamic-fees">Dynamic fees</h2>
<h3 id="heading-incorrect-dynamic-fee-accounting">Incorrect dynamic fee accounting</h3>
<p>Uniswap v4 pools can support dynamic swap fees beyond the typical static options if the pool key specifies the fee using <code>LPFeeLibrary.DYNAMIC_FEE_FLAG</code> to signal support. This is queried in <code>Hooks::beforeSwap</code> like so:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// dynamic fee pools that want to override the cache fee, return a valid fee with the override flag. If override flag</span>
<span class="hljs-comment">// is set but an invalid fee is returned, the transaction will revert. Otherwise the current LP fee will be used</span>
<span class="hljs-keyword">if</span> (key.fee.isDynamicFee()) lpFeeOverride <span class="hljs-operator">=</span> result.parseFee();
</code></pre>
<p>The dynamic fee can be updated by either:</p>
<ul>
<li><p>Having the hook contract call <code>PoolManager::updateDynamicLPFee</code>.</p>
</li>
<li><p>Returning <code>fee | LPFeeLibrary.OVERRIDE_FEE_FLAG</code> from the <code>beforeSwap()</code> hook.</p>
</li>
</ul>
<p>Dynamic fee pools initialize with a 0% default fee, so this <a target="_blank" href="https://solodit.cyfrin.io/issues/dynamic-lp-fees-will-remain-zero-by-default-unless-explicitly-updated-cyfrin-none-sorella-l2-angstrom-markdown">should be overridden</a> with a call to the <code>updateDynamicLPFee()</code> function in the <code>afterInitialize()</code> hook.</p>
<p>The per-swap override allows the dynamic fee to change on every swap, avoiding repeated calls to the <code>PoolManager</code>, and is handled by <code>Pool::swap</code> like so:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// if the beforeSwap hook returned a valid fee override, use that as the LP fee, otherwise load from storage</span>
<span class="hljs-comment">// lpFee, swapFee, and protocolFee are all in pips</span>
{
    <span class="hljs-keyword">uint24</span> lpFee <span class="hljs-operator">=</span> params.lpFeeOverride.isOverride()
    ? params.lpFeeOverride.removeOverrideFlagAndValidate()
    : slot0Start.lpFee();

    swapFee <span class="hljs-operator">=</span> protocolFee <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> ? lpFee : <span class="hljs-keyword">uint16</span>(protocolFee).calculateSwapFee(lpFee);
}
</code></pre>
<p>If either of <code>LPFeeLibrary.DYNAMIC_FEE_FLAG</code> or <code>LPFeeLibrary.OVERRIDE_FEE_FLAG</code> are omitted then the intended fee override will not be applied and default fee will be used.</p>
<p>Even with the dynamic fee correctly specified, the actual accounting logic may be implemented incorrectly. This could result in miscalculation of the fees assigned to the intended recipient(s) or even unrecoverable balances becoming locked in the hook.</p>
<p>More examples: [<a target="_blank" href="https://github.com/pashov/audits/blob/master/team/md/Bunni-security-review-August.md#c-02-fund-loss-because-of-wrong-amamm-fee-amount">C-02</a>, <a target="_blank" href="https://github.com/GuardianAudits/Audits/blob/main/GammaStrategies/2025-04-14_Gamma_UniswapV4_LimitOrders.pdf">M-03</a>, <a target="_blank" href="https://audits.sherlock.xyz/contests/468/report#:~:text=Issue%20M%2D4%3A%20Admin%20can%20not%20set%20the%20pool%20fee%20since%20it%20is%20only%20set%20in%20memory">M-4</a>, <a target="_blank" href="https://audits.sherlock.xyz/contests/468/report#:~:text=Issue%20M%2D11%3A%20There%20is%20a%20logical%20error%20in%20the%20_distributeFees\(\)%20function%2C%20resulting%20in%20an%20unfair%20distribution%20of%20fees.">M-11</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/6">5</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/64">6</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/111">7</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/116">8</a>].</p>
<p><strong>Heuristic:</strong> does the hook intend to support dynamic swap fees and, if so, does the pool key signal support? Is functionality exposed with sufficient access control to update the dynamic fee directly on the <code>PoolManager</code>? Is the default dynamic fee overridden in the <code>afterInitialize()</code> hook? Are dynamic fees returned correctly from the <code>beforeSwap()</code> hook with the LP fee override flag applied?</p>
<h3 id="heading-abuse-of-dynamic-fees">Abuse of dynamic fees</h3>
<p>Depending on how the protocol is configured, there are instances in which privileged callers can obtain the right to set and adjust dynamic swap fees. For example, Bunni v2 implements the <a target="_blank" href="https://arxiv.org/abs/2403.03367">auction-managed AMM</a> mechanism to run censorship-resistant onchain auctions for the right to temporarily set the swap fee rate and receive accrued fees from swaps.</p>
<p><a target="_blank" href="https://drive.proton.me/urls/DYAMRKAWZR#LCSmalUZrzCT">TOB-BUNNI-11</a> details a scenario in which a malicious manager can manipulate the TWAP price while avoiding arbitrage. If the oracle is used determine asset prices in an external lending protocol then it is possible for the manager to exploit this by borrowing against overvalued collateral.</p>
<p><a target="_blank" href="https://github.com/pashov/audits/blob/master/team/md/Bunni-security-review-August.md#h-02-amamm-manager-can-capture-fees-from-the-protocol-and-referrers">H-02</a> demonstrates how a malicious manager could capture fees at the expense of the protocol.</p>
<p>More examples: [<a target="_blank" href="https://4272937506-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FZ7N2KDzpDSvxEq8jqwXq%2Fuploads%2Fj10Buf4Tyonz3JVLRYQL%2F2024-12_Security_Review_FlayerLabs_Flaunch.pdf?alt=media&amp;token=182eb05f-7e06-4793-bae5-5d7170bd5870">H-02</a>].</p>
<p><strong>Heuristic:</strong> can dynamic swap fees be manually set or adjusted by an actor other than the protocol administrator? Are reasonable bounds applied to prevent manipulation and loss to other fee recipients?</p>
<h2 id="heading-custom-accounting">Custom accounting</h2>
<p>Custom accounting is a very powerful feature of Uniswap v4, but by the same token can easily put entire protocol liquidity at risk. Unlike vanilla hooks, those leveraging custom accounting take control of the underlying liquidity and so any bug in the business logic of these hooks, or any other related contracts that store or otherwise handle ERC-6909 claim tokens, is likely to be catastrophic. It is imperative that this accounting is water tight. Even if it is seemingly well-implemented, developers and auditors alike should be very paranoid of all sources and sinks of value, as will be demonstrated below.</p>
<h3 id="heading-insufficient-input-validation">Insufficient input validation</h3>
<p>Insufficient input validation can give rise to multiple categories of bugs and is often a precursor to some of the highest severity exploits. In the context of custom accounting, more complex hook protocol architecture can have its accounting be abused or otherwise broken by a caller invoking certain functions with arbitrary inputs.</p>
<p>Consider <a target="_blank" href="https://drive.proton.me/urls/DYAMRKAWZR#LCSmalUZrzCT">TOB-BUNNI-6</a> in which anyone can call the rebalance order pre/post hooks to trigger the rebalance logic outside of legitimate order fulfilment which results in funds being pulled from the central <code>BunniHub</code> contract. <a target="_blank" href="https://drive.proton.me/urls/DYAMRKAWZR#LCSmalUZrzCT">TOB-BUNNI-7</a> details a similar issue with the rebalance order fulfilment mechanism, in this instance insufficient validation of the order fulfilment inputs themselves, allowing for asymmetric execution of the rebalance order pre/post hooks which violates the intended symmetric execution.</p>
<p>More examples: [<a target="_blank" href="https://drive.proton.me/urls/DYAMRKAWZR#LCSmalUZrzCT">TOB-BUNNI-8</a>].</p>
<p><strong>Heuristic:</strong> are the inputs of all functions touching logic associated with custom accounting sufficiently validated? Are there specific permissioned addresses that should be required? Are there any combinations of inputs that allows the intended, possibly symmetric, execution to be bypassed or otherwise violated?</p>
<h3 id="heading-incorrect-segregation-of-funds">Incorrect segregation of funds</h3>
<p>As with typical smart contract accounting, there may be token balances designated to different purposes and beneficial owners. Incorrect segregation of funds is a common issue in which one of potentially multiple entities can perform some action with access to an asset/amount to which they should not be entitled.</p>
<p>The severity of such issues depends on the specific business logic, perhaps limited to elevated admin control in the less impactful case but potentially allowing an attacker to extract value in higher-severity occurrences. Custom Uniswap v4 accounting is no different. Recursive LP tokens, fees, donations, and other incentive balances are often sources of this type of issue that are easy to overlook.</p>
<p><a target="_blank" href="https://github.com/pashov/audits/blob/master/team/md/Bunni-security-review-August.md#c-05-attacker-can-steal-bids-and-rent-tokens-from-bunnihook">C-05</a> demonstrates how a mismatch between raw ERC-20 and ERC-6909 accounting could be abused to drain the underlying currency of a pool. In this example, such an exploit is predicated on utilizing the LP token of one pool, stored in the target contract as rent payments, as a currency of the recursive malicious pool.</p>
<p>A similar example of abusing recursive LP tokens to drain rewards balances can be found <a target="_blank" href="https://solodit.cyfrin.io/issues/rewards-can-be-stolen-when-incentivizederc20-tokens-are-recursively-provided-as-liquidity-cyfrin-none-paladin-valkyrie-markdown">here</a>. In this case, deltas are lost to the <code>PoolManager</code> upon liquidity provision. The underlying tokenised incentives that are encapsulated by the wrapped liquidity tokens can be stolen by leveraging flash accounting to sync, claim on behalf of the <code>PoolManager</code>, settle, and finally take the extracted tokens.</p>
<p><a target="_blank" href="https://github.com/pashov/audits/blob/master/team/md/Bunni-security-review-August.md#h-04-attacker-can-dos-the-rebalance-mechanism">H-04</a> shows how dust balances accumulated by either fee/donation mechanism can result in the hook reverting due to the issue of unsettled deltas from uncleared or unsynchronised dust as explained above.</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/accrued-limit-order-fees-can-be-stolen-openzeppelin-none-openzeppelin-uniswap-hooks-v110-rc-1-audit-markdown">H-02</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/am-amm-fees-could-be-incorrectly-used-by-rebalance-mechanism-as-order-input-cyfrin-none-bunni-markdown">2</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/collision-between-rebalance-order-consideration-tokens-and-am-amm-fees-for-bunni-pools-using-bunni-tokens-cyfrin-none-bunni-markdown">3</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/52">4</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/54">5</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/23">6</a>].</p>
<p><strong>Heuristic:</strong> are there any assets or addresses with multiple accounting designations? Is there a mixture of ERC-20 and ERC-6909 accounting? If so, does is it work correctly in tandem? Are fees, donations, or any other reserved assets explicitly considered? Should recursive LP tokens be supported and can any underlying yield be stolen when provided as liquidity?</p>
<h3 id="heading-incorrect-swap-logic">Incorrect swap logic</h3>
<p>A key aspect of the design space unlocked by custom accounting is the ability to override the swap logic of the underlying concentrated liquidity model. While this pioneering feature allows for innovative AMM designs to be built on top of the primitive Uniswap v4 hook architecture, this greatly increases the attack surface and potential for subtle arithmetic bugs that can completely break the core functionality.</p>
<p>Consider <a target="_blank" href="https://drive.proton.me/urls/DYAMRKAWZR#LCSmalUZrzCT">TOB-BUNNI-15/16/17/18</a> and <a target="_blank" href="https://github.com/pashov/audits/blob/master/team/md/Bunni-security-review-August.md#m-21-incorrect-rounding-in-computeswap">M-21</a> in which it was possible to:</p>
<ul>
<li><p>Execute free swaps, providing zero input tokens but receiving a non-zero amount of output tokens.</p>
</li>
<li><p>Gain a net positive amount tokens from round trip swaps.</p>
</li>
<li><p>Receive a different amount of input/output tokens depending on the exact input/output configuration.</p>
</li>
<li><p>Trigger unhandled panic reverts due to arithmetic errors.</p>
</li>
<li><p>Underestimate the liquidity available to a swap.</p>
</li>
</ul>
<p>A separate example <a target="_blank" href="https://audits.sherlock.xyz/contests/468/report#:~:text=Issue%20M%2D13%3A%20Price%20limit%20is%20used%20as%20the%20price%20range%20in%20internal%20swaps%2C%20causing%20swap%20TXs%20to%20revert">M-13</a> additionally details a DoS vector for swaps erroneously using the specified swap price limit instead of computing the price target for the next swap step.</p>
<p><strong>Heuristic:</strong> does the hook override the underlying concentrated liquidity model? Do swaps behave as expected? Can any of the AMM invariants be broken?</p>
<h3 id="heading-incorrect-delta-accounting">Incorrect delta accounting</h3>
<p>Another aspect of custom accounting that goes in hand with modifying the swap logic is the ability for hooks to override the deltas that represent the net token movement between the pool, hook, and caller. If these deltas are computed or applied incorrectly, this can result in silent but severe accounting discrepancies that leak value at the expense of either the protocol or its users depending on the nature of the oversight.</p>
<p>An easy mistake to make here is misinterpreting the sign convention of <code>swapDelta</code>, <code>hookDelta</code>, or <code>callerDelta</code>. This may, for instance, result in the hook underpaying its users if the caller delta of the specified token is underestimated in <code>beforeSwap()</code> or overcharging users if the hook delta is overestimated in <code>afterSwap()</code>. In the worst case, value could be extracted from the pool if tokens are transferred in the wrong direction at the expense of the hook.</p>
<p>Note that the core <code>Hooks::beforeSwap</code> logic prevents the semantics of a swap from changing with application of the hook delta:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// Update the swap amount according to the hook's return, and check that the swap type doesn't change (exact input/output)</span>
<span class="hljs-keyword">if</span> (hookDeltaSpecified <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
    <span class="hljs-keyword">bool</span> exactInput <span class="hljs-operator">=</span> amountToSwap <span class="hljs-operator">&lt;</span> <span class="hljs-number">0</span>;
    amountToSwap <span class="hljs-operator">+</span><span class="hljs-operator">=</span> hookDeltaSpecified;
    <span class="hljs-keyword">if</span> (exactInput ? amountToSwap <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span> : amountToSwap <span class="hljs-operator">&lt;</span> <span class="hljs-number">0</span>) {
        HookDeltaExceedsSwapAmount.<span class="hljs-built_in">selector</span>.revertWith();
    }
}
</code></pre>
<p>In other words, exact input swaps are explicitly forbidden from becoming considered exact output swaps and vice versa. However, if <code>afterSwap()</code> returns a <code>hookDelta</code> with the incorrect sign, thinking it is owed then the magnitude and direction of payment can become inverted.</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/incorrect-usage-of-returned-balance-delta-openzeppelin-none-openzeppelin-uniswap-hooks-v110-rc-1-audit-markdown">M-02</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/30">2</a>].</p>
<p><strong>Heuristic:</strong> is the direction of net value flow consistent with the original intent? Can users be under/overcharged or extract value directly from the hook? Are deltas ever added/incremented when they should be subtracted/decremented, or vice versa?</p>
<h3 id="heading-reentrancy">Reentrancy</h3>
<p>As mentioned in the introduction, hooks can suffer from well-known smart contract vulnerabilities, including reentrancy which may arise from chaining together a number of lower-severity issues within more complex hook protocol architecture. This is exactly what happened with the <a target="_blank" href="https://blog.bunni.xyz/posts/bug-disclosure-reentrancy-lock-bypass/">live critical vulnerability in Bunni v2</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/pools-configured-with-a-malicious-hook-can-bypass-the-bunnihub-re-entrancy-guard-to-drain-all-raw-balances-and-vault-reserves-of-legitimate-pools-cyfrin-none-bunni-markdown">reported by Cyfrin</a>, in which the total protocol TVL was at risk.</p>
<p>In short, through <code>deployBunniToken()</code> it was possible to deploy malicious hooks with no validation which could subsequently be used to unlock the global reentrancy guard. Since the hook of a Bunni pool was not constrained to be the canonical <code>BunniHook</code> implementation, and given that <code>unlockForRebalance()</code> simply required the hook of a given pool to be the caller, anyone could create a malicious hook that invoked this function directly to unlock the reentrancy guard protecting the core <code>BunniHub</code> contract. This combined with the caching of pool state before unsafe external calls to rehypothecation vaults, which could again be freely specified as arbitrary malicious addresses by an attacker, meant that reentrancy from within both <code>hookHandleSwap()</code> and <code>withdraw()</code> was in fact possible to recursively extract both raw balances and vault reserves accounted across all pools.</p>
<p><a target="_blank" href="https://github.com/pashov/audits/blob/master/team/md/Bunni-security-review-August.md#c-03-fulfiller-can-arbitrate-the-rebalancing-process-interacting-with-bunnihub">C-03</a> details a similar, less impactful variation of this issue, in which execution of deposits over intermediate state between pre/post hooks could be abused. This also happened to be the finding that introduced the problematic function allowing the chain of lower severity vulnerabilities to be assembled into a full-drain critical exploit. In a manner similar to the <a target="_blank" href="https://www.euler.finance/blog/war-peace-behind-the-scenes-of-eulers-240m-exploit-recovery">$197 million Euler exploit</a>, this highlights the challenge of full-coverage mitigation review and importance of recognising the potential downstream effects of subtle, seemingly trivial, logic changes.</p>
<p>Even if direct reentrancy on the target contracts is not possible, read-only re-entrancy may still affect integrating contracts relying on quoter functions due to missing re-entrancy guards as noted in <a target="_blank" href="https://github.com/pashov/audits/blob/master/team/md/Bunni-security-review-August.md#l-13-read-only-reentrancy-on-withdraw-and-deposit">L-13</a>.</p>
<p><strong>Heuristic:</strong> does the canonical hook or any of its associated contracts perform unsafe external calls? Can this be influenced caller-defined inputs which may specify malicious addresses? Is a reentrancy guard applied to all critical functions? Can it be bypassed, perhaps by a malicious hook implementation?</p>
<h3 id="heading-rounding-and-precision-loss">Rounding and precision loss</h3>
<p>Another well-known albeit tricky smart contract vulnerability is precision loss due to rounding. While it may be relatively straightforward to identify the instances in which this behaviour will occur, it is significantly more challenging to land upon the specific scenario that may be leveraged in an attack.</p>
<p>In the case of the <a target="_blank" href="https://rekt.news/bunni-rekt">$8.4 million Bunni v2 exploit</a>, the above disclosure was very unfortunately not sufficient to prevent a ruinous loss of funds. At its core, this exploit was caused by floor rounding which allowed for the construction of an atomic liquidity increase by the attacker. To summarise, the exploit steps included:</p>
<ol>
<li><p>Swapping almost all of the pool’s reserves to minimise the active balance to the point where rounding errors become significant.</p>
</li>
<li><p>Repeatedly withdrawing a small number of shares to abuse the rounding behaviour of the idle balance, effectively creating a share price inflation while disproportionately decreasing the liquidity calculation based on the active and idle balances.</p>
</li>
<li><p>Swapping back to lock in the atomic liquidity increase and then drain the pool at the inflated price.</p>
</li>
</ol>
<p>While the above points may seem simple enough, this was a complex exploit against a complex protocol. The sad irony is the precision with which the attacker crafted the inputs to pull this off. A full post-mortem writeup can be found <a target="_blank" href="https://blog.bunni.xyz/posts/exploit-post-mortem/">here</a>, along with a more detailed analysis <a target="_blank" href="https://gist.github.com/giovannidisiena/716324d50b6649be3a0e91395890917e">here</a>. It is strongly recommended to read both of these understand the full nuance of the issue.</p>
<p>More examples: [<a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/61">1</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/71">2</a>].</p>
<p><strong>Heuristic:</strong> is any of the hook logic susceptible to rounding errors? If so, is it obvious how it might be abused? If not, consider which other calculations may depend on it (e.g. share price, liquidity, etc) and how execution may proceed at the extremes.</p>
<h2 id="heading-tick-trouble">Tick Trouble</h2>
<h3 id="heading-misaligned-ticks">Misaligned ticks</h3>
<p>Depending on the tick spacing of a given pool, there is a small region of tick space between the minimum/maximum usable ticks and the actual minimum/maximum ticks defined by the Uniswap v3 math that should be carefully avoided. For example, assuming a tick spacing of 60, the usable tick range is <code>[-887220, 887220]</code>. Thus, it should not be possible for the sqrt price tick to enter either range <code>[-887272, -887220)</code> or <code>(887220, 887272]</code>.</p>
<p>Uniswap will revert when the tick of a liquidity position is not an exact multiple of the pool’s tick spacing. This can result in DoS [<a target="_blank" href="https://solodit.cyfrin.io/issues/dos-of-bunni-pools-configured-with-dynamic-ldfs-due-to-insufficient-validation-of-post-shift-tick-bounds-cyfrin-none-bunni-markdown">1</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/oracleunigeodistribution-oracle-tick-validation-is-flawed-cyfrin-none-bunni-markdown">2</a>] and/or <a target="_blank" href="https://solodit.cyfrin.io/issues/missing-tick-validation-when-creating-a-new-range-in-multirangehook-cyfrin-none-paladin-valkyrie-markdown">unusable positions</a> if not correctly validated by the hook.</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">flipTick</span>(<span class="hljs-params"><span class="hljs-keyword">mapping</span>(<span class="hljs-params"><span class="hljs-keyword">int16</span> =&gt; <span class="hljs-keyword">uint256</span></span>) <span class="hljs-keyword">storage</span> <span class="hljs-built_in">self</span>, <span class="hljs-keyword">int24</span> tick, <span class="hljs-keyword">int24</span> tickSpacing</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> </span>{
    <span class="hljs-comment">// Equivalent to the following Solidity:</span>
    <span class="hljs-comment">//     if (tick % tickSpacing != 0) revert TickMisaligned(tick, tickSpacing);</span>
    <span class="hljs-comment">//     (int16 wordPos, uint8 bitPos) = position(tick / tickSpacing);</span>
    <span class="hljs-comment">//     uint256 mask = 1 &lt;&lt; bitPos;</span>
    <span class="hljs-comment">//     self[wordPos] ^= mask;</span>
    <span class="hljs-keyword">assembly</span> ("memory-safe") {
        tick <span class="hljs-operator">:=</span> <span class="hljs-built_in">signextend</span>(<span class="hljs-number">2</span>, tick)
        tickSpacing <span class="hljs-operator">:=</span> <span class="hljs-built_in">signextend</span>(<span class="hljs-number">2</span>, tickSpacing)
        <span class="hljs-comment">// ensure that the tick is spaced</span>
        <span class="hljs-keyword">if</span> <span class="hljs-built_in">smod</span>(tick, tickSpacing) {
            <span class="hljs-keyword">let</span> fmp <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(<span class="hljs-number">0x40</span>)
            <span class="hljs-built_in">mstore</span>(fmp, <span class="hljs-number">0xd4d8f3e6</span>) <span class="hljs-comment">// selector for TickMisaligned(int24,int24)</span>
            <span class="hljs-built_in">mstore</span>(<span class="hljs-built_in">add</span>(fmp, <span class="hljs-number">0x20</span>), tick)
            <span class="hljs-built_in">mstore</span>(<span class="hljs-built_in">add</span>(fmp, <span class="hljs-number">0x40</span>), tickSpacing)
            <span class="hljs-keyword">revert</span>(<span class="hljs-built_in">add</span>(fmp, <span class="hljs-number">0x1c</span>), <span class="hljs-number">0x44</span>)
        }
    ...
}
</code></pre>
<p>More examples: [<a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/8">1</a>].</p>
<p><strong>Heuristic:</strong> are caller-specified ticks validated to be aligned to the tick spacing? Are the range edge cases validated against the minimum and maximum usable ticks?</p>
<h3 id="heading-operations-over-zero-liquidity">Operations over zero liquidity</h3>
<p>If out-of-range initialization of the square root price is not prevented and <a target="_blank" href="https://x.com/0xKaden/status/1856784539978444827">swaps against zero liquidity</a> are not explicitly handled then this can allow the price to be freely manipulated to outside the intended tick range. Even If there is a small non-zero amount of liquidity in a pool, various issues can arise from the tick being manipulated to an extreme value.</p>
<p>In some circumstances, this can result in losses to honest users through arbitrage, as in <a target="_blank" href="https://github.com/GuardianAudits/Audits/blob/main/GammaStrategies/2025-04-14_Gamma_UniswapV4_LimitOrders.pdf">C-03</a>. In others, <a target="_blank" href="https://solodit.cyfrin.io/issues/manipulation-of-price-outside-of-the-fullrangehook-liquidity-range-can-result-in-dos-and-financial-loss-to-liquidity-providers-cyfrin-none-paladin-valkyrie-markdown">swaps</a> and the <a target="_blank" href="https://solodit.cyfrin.io/issues/h-03-adversary-can-prevent-the-launch-of-any-ilo-pool-with-enough-raised-capital-at-any-moment-by-providing-single-sided-liquidity-code4rena-vultisig-vultisig-git">addition of single-sided liquidity</a> to these end ranges can result in complete DoS of core functionality.</p>
<p>For protocols that implement additional incentives, care should be taken to <a target="_blank" href="https://solodit.cyfrin.io/issues/deposited-rewards-get-stuck-in-incentive-logic-contracts-when-there-is-no-pool-liquidity-cyfrin-none-paladin-valkyrie-markdown">avoid performing computations and depositing tokens when there is no pool liquidity</a>.</p>
<p>More examples: [<a target="_blank" href="https://4272937506-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FZ7N2KDzpDSvxEq8jqwXq%2Fuploads%2Fj10Buf4Tyonz3JVLRYQL%2F2024-12_Security_Review_FlayerLabs_Flaunch.pdf?alt=media&amp;token=182eb05f-7e06-4793-bae5-5d7170bd5870">H-01</a>, <a target="_blank" href="https://github.com/GuardianAudits/Audits/blob/main/GammaStrategies/2025-04-14_Gamma_UniswapV4_LimitOrders.pdf">L-26</a>].</p>
<p><strong>Heuristic:</strong> are ticks/ranges free to be pushed to or otherwise specified as extreme values? Are they validated against the the minimum/maximum usable ticks for the given tick spacing? Are there any additional mechanisms that will behave incorrectly when there is no liquidity?</p>
<h3 id="heading-incorrect-tick-crossings">Incorrect tick crossings</h3>
<p>The Uniswap v3 concentrated liquidity tick crossing logic defines where liquidity is considered in range and active (i.e., price is between upper/lower ticks). Active liquidity is utilized by swaps and earns fees, so fee growth state updates are also triggered for the liquidity positions becoming active/inactive whenever the price crosses a tick boundary.</p>
<p>Miscalculation of the active liquidity can have disastrous consequences, as evidenced by the <a target="_blank" href="https://100proof.org/kyberswap-post-mortem.html">KyberSwap Elastic critical vulnerability disclosure</a>, <a target="_blank" href="https://rekt.news/kyberswap-rekt">$56 million KyberSwap Elastic exploit</a> (very well-explained <a target="_blank" href="https://slowmist.medium.com/a-deep-dive-into-the-kyberswap-hack-3e13f3305d3a">here</a>), and <a target="_blank" href="https://rekt.news/raydium-rekt">$4.4 million Raydium exploit</a>. This could be due to rounding or incorrect state updates when crossing ticks during swaps, or modifying liquidity, for example resulting in double counting or other discrepancies that can be leveraged by an attacker.</p>
<p>In <a target="_blank" href="https://solodit.cyfrin.io/issues/all-rewards-can-be-stolen-due-to-incorrect-active-liquidity-calculations-when-the-current-tick-is-an-exact-multiple-of-the-tick-spacing-at-the-upper-end-of-a-liquidity-range-cyfrin-none-sorella-l2-angstrom-markdown">this example</a>, active liquidity calculations appear to function as expected; however, for zero-for-one swaps where the current tick is an exact multiple of the tick spacing at the upper end of a liquidity range, the effect of the boundary tick is skipped and the liquidity is accounted incorrectly. As a result, the net liquidity is significantly smaller than expected which ultimately inflates the computed fee growth and allows for all rewards to be stolen by such an intentionally, or perhaps even inadvertently, malicious position.</p>
<p>More examples: [<a target="_blank" href="https://www.openzeppelin.com/news/openzeppelin-uniswap-hooks-v1.1.0-rc-1-audit#:~:text=High%20Severity-,Limit%20Orders%20Can%20Be%20Incorrectly%20Filled,-The%20LimitOrderHook%20contract">H-01</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/tickiterator_advancetonextup-sets-uninitialized-end-tick-as-the-current-tick-which-causes-tickiteratorhasnext-to-return-true-when-this-is-not-actually-the-case-cyfrin-none-sorella-l2-angstrom-markdown">2</a>].</p>
<p><strong>Heuristic:</strong> are tick crossings handled correctly? Are there any edge cases, such as starting/ending exactly on a tick/word boundary, in which the logic breaks? Are there sufficient tests around these boundary conditions?</p>
<h2 id="heading-miscellaneous">Miscellaneous</h2>
<h3 id="heading-native-token-handling">Native token handling</h3>
<p>Unlike Uniswap v3, Uniswap v4 has support for native ETH abstracted via the custom <code>Currency</code> type. While this succeeds in providing a better user and developer experience, it is not without its dangers. Insufficient or otherwise incorrect handling of native tokens can give rise to some typical issues that are associated with and should be expected when dealing with native token support.</p>
<p>The first and likely most impactful point is that native token transfers are a source of <a target="_blank" href="https://solodit.cyfrin.io/issues/unsafe-external-calls-made-during-proportional-lp-fee-transfers-can-be-used-to-reenter-wrapper-contracts-cyfrin-none-vii-markdown">potential reentrancy</a>. This may be more easily overlooked when dealing with the <code>Currency</code> type and ERC-6909 representations, but it is nevertheless incredibly important to identify all sources of unsafe external calls that could be reentered.</p>
<p>A second, more subtle error is the absence of a <code>receive()</code> function. This can result in DoS depending on how the hook and its associated contracts are intended to transfer and receive value in the form of the native token. <a target="_blank" href="https://certora.cdn.prismic.io/certora/Z6zPyJbqstJ9-ib-_DopplerSecurityAssessmentReport_Final.pdf">H-04</a> demonstrates how this could occur when unable to receive dust balances redirected from a router contract.</p>
<p>In the worst case, if the capability is implemented but the <code>msg.value</code> is insufficiently validated then this could result in the native token balance becoming locked, as in <a target="_blank" href="https://github.com/GuardianAudits/Audits/blob/main/GammaStrategies/2025-04-14_Gamma_UniswapV4_LimitOrders.pdf">L-22</a>. This could also result in breaking the custom swap delta accounting, as in <a target="_blank" href="https://audits.sherlock.xyz/contests/468/report#:~:text=Issue%20M%2D19%3A%20UniswapImplementation%3A%3AbeforeSwap\(\)%20might%20revert%20when%20swapping%20native%20tokens%20to%20collection%20tokens">M-19</a>.</p>
<p>Also note that it is not necessary to perform validation on <code>currency1</code> as the native token can only ever be <code>token0</code>, since currencies are ordered by address and <code>address(0)</code> is used for the native currency [<a target="_blank" href="https://solodit.cyfrin.io/issues/unnecessary-currency1-native-token-checks-cyfrin-none-bunni-markdown">1</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/unnecessary-native-token-checks-cyfrin-none-paladin-valkyrie-markdown">2</a>].</p>
<p><strong>Heuristic:</strong> does the hook have support for native ETH? If so, can it be used to reenter any critical functionality? Do contracts that expect to receive native tokens implement the <code>receive()</code> function? If not, have edge cases such as router integration been considered? Is <code>msg.value</code> sufficiently validated?</p>
<h3 id="heading-jit-liquidity">JIT liquidity</h3>
<p>Just-in-time liquidity enables innovative capital-efficient protocol designs (e.g. Euler) but can also be used maliciously to manipulate pricing or reward mechanisms. This could include temporarily inflating liquidity positions to capture fees, incentives, or alter other core state before withdrawing immediately after, as in <a target="_blank" href="https://cantina.xyz/portfolio/2c7d45e3-0358-4254-8698-b4500fe7c6a9">M-1/2</a> which rely on front-running custom liquidity and rewards logic.</p>
<p>The scenario described by <a target="_blank" href="https://drive.proton.me/urls/DYAMRKAWZR#LCSmalUZrzCT">TOB-BUNNI-9</a> shows how an error in the hook withdrawal queue logic enabled JIT liquidity provision which could be used to manipulate the pool rebalance order computation, potentially leaving it in a worse state than before.</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/just-in-time-jit-liquidity-can-be-used-to-inflate-rebalance-order-amounts-cyfrin-none-bunni-markdown">1</a>].</p>
<p><strong>Heuristic:</strong> can JIT liquidity be provided? Does it represent net positive flow, or can it be malicious and should it be disincentivised/prevented?</p>
<h3 id="heading-incorrect-router-parameters">Incorrect router parameters</h3>
<p>While the Uniswap <code>V4Router</code> is, like all other router implementations, a peripheral contract, care must be taken to ensure that:</p>
<ol>
<li><p>All recipient and token addresses are correct.</p>
</li>
<li><p>Amounts are not accidentally zero or reversed.</p>
</li>
</ol>
<p><a target="_blank" href="https://certora.cdn.prismic.io/certora/Z6zPyJbqstJ9-ib-_DopplerSecurityAssessmentReport_Final.pdf">C-03</a> is an example of the former while <a target="_blank" href="https://certora.cdn.prismic.io/certora/Z6zPyJbqstJ9-ib-_DopplerSecurityAssessmentReport_Final.pdf">C-04/H-07</a> are examples of the latter, though these errors can of course also occur in the hook logic itself.</p>
<p><strong>Heuristic:</strong> is the correct recipient address specified? It is not necessarily always <code>msg.sender</code>. Is <code>amount0</code>/<code>token0</code> accidentally used in place of <code>amount1</code>/<code>token1</code> and vice versa?</p>
<h3 id="heading-custom-incentives">Custom incentives</h3>
<p>The following is a list of various other interesting findings not directly related to Uniswap v4 hooks but rather the implementation of custom LP fee handling and other incentives logic [<a target="_blank" href="https://solodit.cyfrin.io/issues/reward-distribution-can-be-blocked-by-an-initial-distribution-of-long-duration-cyfrin-none-paladin-valkyrie-markdown">1</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/missing-user-liquidity-sync-in-timeweightedincentivelogic_updatealluserstate-can-result-in-rewards-becoming-locked-cyfrin-none-paladin-valkyrie-markdown">2</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/unsafe-downcast-in-valkyriesubscribertoint256-could-silently-overflow-cyfrin-none-paladin-valkyrie-markdown">3</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/positions-can-be-locked-by-malicious-re-initialization-in-the-event-of-multiple-valkyriesubscriber-contracts-being-deployed-cyfrin-none-paladin-valkyrie-markdown">4</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/missing-duplicate-incentive-logic-can-result-in-incentive-system-accounting-being-broken-cyfrin-none-paladin-valkyrie-markdown">5</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/precision-loss-can-result-in-funds-becoming-stuck-in-incentive-logic-contracts-cyfrin-none-paladin-valkyrie-markdown">6</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/timeweightedincentivelogic-distributions-are-not-possible-for-tokens-that-revert-on-zero-transfers-cyfrin-none-paladin-valkyrie-markdown">7</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/rewards-distributed-with-timeweightedincentivelogic-can-continue-to-be-claimed-after-the-distribution-ends-cyfrin-none-paladin-valkyrie-markdown">8</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/effective-price-calculations-can-be-affected-by-edge-cases-in-math512libsqrt512-and-math512libdiv512by256-cyfrin-none-sorella-l2-angstrom-markdown">9</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/self-triggered-licredity_afterswap-back-run-enables-lp-fee-farming-cyfrin-none-licredity-markdown">10</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/fees-can-be-stolen-from-partially-unwrapped-uniswapv4wrapper-positions-cyfrin-none-vii-markdown">11</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/fees-can-become-stuck-in-uniswapv4wrapper-cyfrin-none-vii-markdown">12</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/11">13</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/24">14</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/26">15</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/29">16</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/48">17</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/53">18</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/50">19</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/55">20</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/63">21</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/81">22</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/85">23</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/97">24</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/104">25</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/107">26</a>, <a target="_blank" href="https://github.com/Cyfrin/audit-2025-07-hooked-exchange/issues/108">27</a>].</p>
<p>The main takeaways here are to be cautious of Uniswap v3 math that has been reimplemented for custom purposes, particularly when considering the swap math, tick crossings, and fee growth computations. For more complex hooks, full end-to-end tests should also validate the flow of tokens between multiple integrating contracts to ensure that there is no missing logic/transfers that could result in stuck funds.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Hooks are a very powerful innovation. This design considerably lowers the barrier to AMM experimentation but equally raises the bar for sound and secure implementation, especially when customising the underlying concentrated liquidity mechanism. The overall attack surface is large and can be fairly nuanced, as hopefully demonstrated in this article. If you found it to be helpful, amplification and all feedback would be greatly appreciated!</p>
]]></content:encoded></item><item><title><![CDATA[Chainlink Functions & Automation – A Deep Dive]]></title><description><![CDATA[While the various security considerations for integrating Chainlink Data Feeds are well documented, such as in this excellent deep dive by Dacian, and often heavily reported during both private and competitive audits, this is a notable gap when consi...]]></description><link>https://surfing-solodit.com/chainlink-functions-automation-a-deep-dive</link><guid isPermaLink="true">https://surfing-solodit.com/chainlink-functions-automation-a-deep-dive</guid><category><![CDATA[Chainlink]]></category><category><![CDATA[Smart Contracts]]></category><category><![CDATA[Blockchain]]></category><dc:creator><![CDATA[Giovanni Di Siena]]></dc:creator><pubDate>Thu, 13 Mar 2025 16:07:54 GMT</pubDate><content:encoded><![CDATA[<p>While the various security considerations for integrating Chainlink Data Feeds are well documented, such as in this excellent <a target="_blank" href="https://medium.com/cyfrin/chainlink-oracle-defi-attacks-93b6cb6541bf">deep dive</a> by <a target="_blank" href="https://dacian.me">Dacian</a>, and often <a target="_blank" href="https://solodit.cyfrin.io/?b=false&amp;f=&amp;fc=gte&amp;ff=&amp;fn=1&amp;i=HIGH%2CMEDIUM&amp;p=1&amp;pc=&amp;pn=&amp;r=all&amp;s=chainlink+price&amp;t=&amp;u=">heavily reported</a> during both private and competitive audits, this is a notable gap when considering other widely used Chainlink services.</p>
<p>This article will focus on the security considerations for integrating Chainlink Functions &amp; Automation, demonstrated with findings from a recent Cyfrin private audit for <a target="_blank" href="https://chain.link/economics/build-program">Chainlink Build Program</a> project <a target="_blank" href="https://www.thestandard.io/">The Standard</a>. A full list of high and medium severity findings can be found <a target="_blank" href="https://solodit.cyfrin.io/?b=false&amp;f=&amp;fc=gte&amp;ff=&amp;fn=1&amp;i=HIGH%2CMEDIUM%2CLOW&amp;p=1&amp;pc=&amp;pn=The+Standard+Auto+Redemption&amp;r=all&amp;s=&amp;t=&amp;u=Giovanni+Di+Siena">here</a>, along with the full report <a target="_blank" href="https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2024-12-18-cyfrin-the-standard-auto-redemption-v2.0.pdf">here</a>.</p>
<p>Note that while the main categories of findings and associated heuristics discussed in this article are around the implementation-specific composition of the two Chainlink services, chaining Automation upkeep calls with the fulfillment of the triggered Functions requests, it remains very pertinent to other codebases both when the services are leveraged in isolation and by virtue of the fact this multi-faceted integration is not an uncommon design pattern to observe.</p>
<h2 id="heading-a-note-on-chainlink-services-and-reverts">A Note on Chainlink Services and Reverts</h2>
<p>To first elucidate some nuances about the Chainlink Functions and Automation services that both leverage the same shared subscription and billing model, note that:</p>
<ul>
<li><p>The Chainlink Automation DON performs the upkeep check every block.</p>
</li>
<li><p>Upkeep transactions are broadcast immediately, save for some latency associated with transaction inclusion and confirmation.</p>
</li>
<li><p>The subscription is billed for every Automation upkeep transaction.</p>
</li>
<li><p>The Chainlink Functions DON reports the status of both off-chain computation and on-chain callback.</p>
</li>
<li><p>The Chainlink Functions DON will only ever attempt to fulfill a given request once.</p>
</li>
<li><p>Only one of the Functions callback <code>response</code>/<code>err</code> parameters will be set to non-zero bytes.</p>
</li>
<li><p>Failed Functions executions will not be retried, so the subscription is billed only once even if the callback reverts.</p>
</li>
<li><p>There exists a five-minute timeout after which a Functions request becomes stale, hence it is not guaranteed that the callback will receive a response.</p>
</li>
</ul>
<p>As we will see below, it is thus imperative to ensure that mission-critical logic contained within the Functions callback, potentially triggered by Automation upkeep, does not revert unless absolutely unavoidable. If it does, this case should be handled gracefully with an optional but highly recommended admin-controlled escape hatch to avoid halting further executions. The flow looks something like this:</p>
<pre><code class="lang-markdown"><span class="hljs-bullet">1.</span> <span class="hljs-code">`checkUpkeep()`</span> called off-chain by Chainlink Automation DON.
<span class="hljs-bullet">2.</span> Upkeep required.
<span class="hljs-bullet">3.</span> <span class="hljs-code">`performUpkeep()`</span> called on-chain by Chainlink Automation DON.
<span class="hljs-bullet">4.</span> Chainlink Functions request triggered within upkeep logic.
<span class="hljs-bullet">5.</span> Chainlink Functions DON attempts to fulfill request.
<span class="hljs-bullet">6.</span> Chainlink Functions <span class="hljs-code">`fulfillRequest()`</span> callback reverts.
<span class="hljs-bullet">7.</span> Mission-critical logic is not executed, leaving state corrupted.
<span class="hljs-bullet">8.</span> Admin resets corrupted state, allowing execution to resume as normal.
</code></pre>
<h2 id="heading-dos-of-core-functionality">DoS of Core Functionality</h2>
<p>With the stage freshly set, let’s consider all the ways to avoid having the <code>fulfillRequest()</code> callback revert:</p>
<ul>
<li><p>Do <strong>not</strong> revert if an error in off-chain computation is reported by the Functions DON (i.e. don’t bubble up <code>err</code> bytes).</p>
</li>
<li><p>Avoid attempting to decode an empty <code>response</code> when there are non-zero <code>err</code> bytes present.</p>
</li>
<li><p>Validate the <code>response</code> against its expected length to ensure that the decoding does not revert.</p>
</li>
<li><p>Avoid reverting if <a target="_blank" href="https://solodit.cyfrin.io/issues/additional-validation-should-be-performed-on-the-chainlink-functions-response-cyfrin-none-the-standard-auto-redemption-markdown">validation of the contents of the API response</a> fails.</p>
</li>
<li><p>Handle reverts from all external calls using <code>try/catch</code> blocks.</p>
</li>
<li><p>Short-circuit if specific inputs would cause other calls to revert.</p>
</li>
<li><p>Be very wary of return data bombs and other gas-exhaustion attacks.</p>
</li>
<li><p>Optionally add an access-controlled admin function to reset critical state.</p>
</li>
</ul>
<p>In <a target="_blank" href="https://solodit.cyfrin.io/issues/autoredemptionfulfillrequest-should-never-be-allowed-to-revert-cyfrin-none-the-standard-auto-redemption-markdown">this example</a>, upkeep can only be triggered when there are no in-flight requests waiting to be fulfilled. An unhandled revert in <code>AutoRedemption::fulfillRequest</code> would cause execution to end without resetting the required <code>lastRequestId</code> state. Due to the absence of any other method for resetting the state, this therefore results in a denial-of-service on the entire upkeep → callback mechanism.</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">performUpkeep</span>(<span class="hljs-params"><span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> performData</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    <span class="hljs-keyword">if</span> (lastRequestId <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-keyword">bytes32</span>(<span class="hljs-number">0</span>)) {
        triggerRequest();
    }
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">fulfillRequest</span>(<span class="hljs-params"><span class="hljs-keyword">bytes32</span> requestId, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> response, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> err</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> </span>{
    <span class="hljs-comment">// TODO proper error handling</span>
    <span class="hljs-comment">// if (err) revert; // @audit - no, don't do this!</span>
    <span class="hljs-keyword">if</span> (requestId <span class="hljs-operator">!</span><span class="hljs-operator">=</span> lastRequestId) <span class="hljs-keyword">revert</span>(<span class="hljs-string">"wrong request"</span>); <span class="hljs-comment">// @audit - avoid this also!</span>
    ...
    lastRequestId <span class="hljs-operator">=</span> <span class="hljs-keyword">bytes32</span>(<span class="hljs-number">0</span>); <span class="hljs-comment">// @audit - this will not be reset if execution reverts!</span>
}
</code></pre>
<p>Also note that as mentioned above, there is always some small chance that despite a very high degree of reliability the Chainlink Functions DON may not respond if a request becomes stale. Drawing on an excellent example present in the <a target="_blank" href="https://www.tunnl.io/">Tunnl</a> <a target="_blank" href="https://basescan.org/address/0x822f752b7CEC2A034bd9fb2bbeC6D71C0A1E5121#code">smart contracts</a>, this can be handled gracefully by implementing a manual or automated retry mechanism that allows Functions requests to be sent again in the event of failure.</p>
<pre><code class="lang-solidity"><span class="hljs-comment">/*
* @notice This function is called in case of Twitter API failure, RPC issue, or any general
* failure in order to manually retry functions request via admins based on status of offer
* @param offerIds The offer Ids to be sent for batch manual retry request
*/</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">retryRequests</span>(<span class="hljs-params"><span class="hljs-keyword">bytes32</span>[] <span class="hljs-keyword">calldata</span> offerIds</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">onlyAdmins</span> </span>{
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint256</span> i <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; i <span class="hljs-operator">&lt;</span> offerIds.<span class="hljs-built_in">length</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
        <span class="hljs-keyword">bytes32</span> offerId <span class="hljs-operator">=</span> offerIds[i];

        <span class="hljs-comment">// Check if the offer is eligible for a retry based on its status</span>
        <span class="hljs-keyword">if</span> (
            s_offers[offerId].status <span class="hljs-operator">=</span><span class="hljs-operator">=</span> Status.VerificationFailed <span class="hljs-operator">|</span><span class="hljs-operator">|</span>
            s_offers[offerId].status <span class="hljs-operator">=</span><span class="hljs-operator">=</span> Status.VerificationInFlight <span class="hljs-operator">|</span><span class="hljs-operator">|</span>
            s_offers[offerId].status <span class="hljs-operator">=</span><span class="hljs-operator">=</span> Status.AwaitingVerification
        ) {
            sendFunctionsRequest(offerId, RequestType.Verification);
        }
        <span class="hljs-keyword">if</span> (
            s_offers[offerId].status <span class="hljs-operator">=</span><span class="hljs-operator">=</span> Status.PayoutFailed <span class="hljs-operator">|</span><span class="hljs-operator">|</span>
            s_offers[offerId].status <span class="hljs-operator">=</span><span class="hljs-operator">=</span> Status.PayoutInFlight <span class="hljs-operator">|</span><span class="hljs-operator">|</span>
            (s_offers[offerId].status <span class="hljs-operator">=</span><span class="hljs-operator">=</span> Status.Active <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> s_offers[offerId].payoutDate <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>)
        ) {
            sendFunctionsRequest(offerId, RequestType.Payout);
        }
    }
}
</code></pre>
<p><strong>Heuristic:</strong> is it possible for Chainlink Functions &amp; Automation calls to revert? Can an attacker manipulate state to force this case? Is there an escape hatch to reset state in the event of failed requests or other incomplete execution?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/an-attacker-can-flood-the-protocol-with-false-offers-to-cause-dos-cyfrin-none-cyfrin-tunnl-v20-markdown"><strong>1</strong></a><strong>,</strong> <a target="_blank" href="https://solodit.cyfrin.io/issues/m-05-performupkeep-could-revert-pashov-audit-group-none-aegisvault-markdown"><strong>2</strong></a><strong>,</strong> <a target="_blank" href="https://solodit.cyfrin.io/issues/performupkeepsinglepool-can-result-in-a-griefing-attack-when-the-pool-has-not-been-updated-for-spearbit-tracer-pdf"><strong>3</strong></a><strong>,</strong> <a target="_blank" href="https://solodit.cyfrin.io/issues/math-error-in-creator-payment-calculation-cyfrin-none-cyfrin-tunnl-v20-markdown"><strong>4</strong></a><strong>,</strong> <a target="_blank" href="https://solodit.cyfrin.io/issues/chainlink-automation-upkeep-can-not-function-because-of-improper-integration-codehawks-liquid-staking-git"><strong>5</strong></a><strong>,</strong> <a target="_blank" href="https://solodit.cyfrin.io/issues/m-9-stoplimit-order-cannot-be-filled-under-certain-condition-sherlock-okus-new-order-types-contract-contest-git"><strong>6</strong></a>].</p>
<h2 id="heading-insufficient-access-controls">Insufficient Access Controls</h2>
<p>While it may be tempting to overlook access controls on the <code>checkUpkeep()</code> and <code>performUpkeep()</code> functions of <a target="_blank" href="https://github.com/smartcontractkit/chainlink/blob/develop/contracts/src/v0.8/automation/interfaces/AutomationCompatibleInterface.sol"><code>AutomationCompatibleInterface</code></a> given the intention to automate key aspects of a protocol, this is very likely to provide would-be attackers with one or more levers for manipulation. It is also important to note that, without use of the <a target="_blank" href="https://github.com/smartcontractkit/chainlink/blob/d1006bec3ff2051a68b892712e7536393c696d72/contracts/src/v0.8/automation/AutomationBase.sol#L22-L25"><code>cannotExexute()</code></a> modifier, both functions allow for state changes when called; therefore, even though <code>checkUpkeep()</code> is used for simulation and subsequent triggering of <code>performUpkeep()</code> by the Chainlink Automation DON, it could also contain state-changing logic. This means it may not be sufficient to add access controls to just one function or the other – always analyse the context and verify the behaviour of both.</p>
<p>In <a target="_blank" href="https://solodit.cyfrin.io/issues/auto-redemption-logic-can-be-abused-by-an-attacker-due-to-insufficient-access-control-cyfrin-none-the-standard-auto-redemption-markdown">this example</a>, recalling the context from above, upkeep can only be triggered when the <code>lastRequestId</code> state variable is equal to <code>bytes32(0)</code>; however, since this state is reset at the end of execution initiated within <code>triggerRequest()</code>, specifically in <code>AutoRedemption::fulfillRequest</code>, this means that upkeep can be repeatedly performed after the previous one has succeeded. Note that this is possible regardless of the trigger condition both due to the absence of access controls and a failure to re-check the trigger condition to prevent execution based on stale or manipulated data when upkeep is actually performed.</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">checkUpkeep</span>(<span class="hljs-params"><span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> checkData</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span> upkeepNeeded, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> performData</span>) </span>{
    (<span class="hljs-keyword">uint160</span> sqrtPriceX96,,,,,,) <span class="hljs-operator">=</span> pool.slot0();
    upkeepNeeded <span class="hljs-operator">=</span> sqrtPriceX96 <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> triggerPrice;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">performUpkeep</span>(<span class="hljs-params"><span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> performData</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{ <span class="hljs-comment">// @audit - no access controls!</span>
    <span class="hljs-keyword">if</span> (lastRequestId <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-keyword">bytes32</span>(<span class="hljs-number">0</span>)) {
        triggerRequest(); <span class="hljs-comment">// note: triggers a call to autoRedemption()</span>
    }
}
</code></pre>
<p>From here, an attacker can leverage other implementation errors in the logic triggered by the callback to force a revert and permanently disable the functionality. For example, the incorrect <a target="_blank" href="https://github.com/the-standard/smart-vault/blob/bfec2ad17fb7b77ff64642e7ff4834f4670e0b25/contracts/SmartVaultV4.sol#L366-L367">calculation of the debt redeemed</a>, as <a target="_blank" href="https://solodit.cyfrin.io/issues/auto-redemption-logic-can-be-abused-by-an-attacker-due-to-insufficient-access-control-cyfrin-none-the-standard-auto-redemption-markdown">reported here</a> and shown below, causes a panic revert due to underflow when the given vault is fully redeemed but a dust amount of <code>USDs</code> is transferred to the contract beforehand.</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">autoRedemption</span>(<span class="hljs-params">...</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">onlyAutoRedemption</span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span> _redeemed</span>) </span>{
    ...
    _redeemed <span class="hljs-operator">=</span> USDs.balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>)); <span class="hljs-comment">// @audit - dust amount can be sent to inflate this value!</span>
    minted <span class="hljs-operator">-</span><span class="hljs-operator">=</span> _redeemed; <span class="hljs-comment">// @audit - meaning this could revert due to underflow if the vault is fully redeemed.</span>
    ...
}
</code></pre>
<p>As mentioned above, given that there is no other way to reset <code>lastRequestId</code>, this represents a state from which a non-upgradeable contract cannot recover.</p>
<p><strong>Heuristic:</strong> are the <code>AutomationCompatibleInterface</code> functions permissionless? Is the execution of logic within these functions sensitive to on-chain state? Is it problematic if they be called by any account at any time?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/h-3-lack-of-nonreentrant-modifier-in-fillorder-and-modifyorder-allows-attacker-to-steal-funds-sherlock-okus-new-order-types-contract-contest-git">1</a>].</p>
<h2 id="heading-unintendedincomplete-execution">Unintended/Incomplete Execution</h2>
<p>As stated before, the Chainlink Automation DON checks the upkeep condition every block. Once the trigger condition is met and upkeep is required, the DON will send a transaction on-chain. In a sense, this can be thought of as asynchronous execution of a state transition within a finite state machine.</p>
<p>If such a state transition is not completed fully, correctly, or even intentionally, then problems may arise. This scenario could occur in a manner similar to the first example, where unhandled reverts result in an irrecoverable state, or caused through a similar vector in which an attacker additionally leverages incorrect implementation details as in the second example.</p>
<p>In <a target="_blank" href="https://solodit.cyfrin.io/issues/automation-and-redemption-could-be-artificially-manipulated-due-to-use-of-instantaneous-sqrtpricex96-cyfrin-none-the-standard-auto-redemption-markdown">this example</a>, the trigger condition is based on a price oracle that is derived from the instantaneous reserves of a Uniswap v3 pool. As you may have already noted, this oversight can be leveraged by an attacker through manipulation of the pool reserves to trigger upkeep when it is potentially not desired.</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">checkUpkeep</span>(<span class="hljs-params"><span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> checkData</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span> upkeepNeeded, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> performData</span>) </span>{
    (<span class="hljs-keyword">uint160</span> sqrtPriceX96,,,,,,) <span class="hljs-operator">=</span> pool.slot0();
    upkeepNeeded <span class="hljs-operator">=</span> sqrtPriceX96 <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> triggerPrice;
}
</code></pre>
<p>While I recommend reading the full finding linked above for more details about the potential manipulation and considerations around the maintenance of such a manipulation given the latency of upkeep performance, the repeated triggering of upkeep in this manner could result in the subscription being fraudulently billed to exhaustion.</p>
<p><strong>Heuristic:</strong> is every state transition properly accounted for? Can the state be manipulated into an incorrect transition? Is a permissionless call to <code>performUpkeep()</code> safe for all inputs? Should the trigger condition be re-checked when performing upkeep?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/incomplete-implementation-of-state-transition-from-pending-to-expired-cyfrin-none-cyfrin-tunnl-v20-markdown"><strong>1</strong></a><strong>,</strong> <a target="_blank" href="https://solodit.cyfrin.io/issues/h-5-attacker-can-drain-stoplimit-contract-funds-through-bracket-contract-because-it-gives-typeuint256max-allowance-to-bracket-contract-for-input-token-in-performupkeep-function-sherlock-okus-new-order-types-contract-contest-git"><strong>2</strong></a>].</p>
<h2 id="heading-request-authentication-amp-secrets">Request Authentication &amp; Secrets</h2>
<p>With increasingly advanced smart contract applications now commonly leveraging additional off-chain infrastructure, it is important to highlight in <a target="_blank" href="https://solodit.cyfrin.io/issues/chainlink-functions-http-request-is-missing-authentication-cyfrin-none-the-standard-auto-redemption-markdown">this final example</a> that great care must be taken to secure both the on-chain and off-chain components. <a target="_blank" href="https://github.com/the-standard/smart-vault/blob/bfec2ad17fb7b77ff64642e7ff4834f4670e0b25/contracts/AutoRedemption.sol#L35-L36">Here</a>, the <code>source</code> constant defined within <code>AutoRedemption</code> is used to execute the corresponding JavaScript code within the Chainlink Functions DON; however, the target API endpoint is exposed without any form of authentication which allows any observer to send requests.</p>
<pre><code class="lang-solidity"><span class="hljs-keyword">string</span> <span class="hljs-keyword">private</span> <span class="hljs-keyword">constant</span> source <span class="hljs-operator">=</span>
        <span class="hljs-string">"const { ethers } = await import('npm:ethers@6.10.0'); const apiResponse = await Functions.makeHttpRequest({ url: 'https://smart-vault-api.thestandard.io/redemption' }); if (apiResponse.error) { throw Error('Request failed'); } const { data } = apiResponse; const encoded = ethers.AbiCoder.defaultAbiCoder().encode(['uint256', 'address', 'uint256'], [data.tokenID, data.collateral, data.value]); return ethers.getBytes(encoded)"</span>;
</code></pre>
<p>At a minimum, rate limiting should be implemented to mitigate against coordinated DDoS attacks on the API endpoint. Ideally, authentication should be added to the request using <a target="_blank" href="https://docs.chain.link/chainlink-functions/resources/secrets">Chainlink Functions secrets</a>.</p>
<p><strong>Heuristic:</strong> does the Chainlink Functions request leak sensitive information? Are self-hosted API endpoints sufficiently protected? Is the off-chain infrastructure configured to be resilient to DDoS attacks?</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>To summarise, when integrating Chainlink Functions and/or Automation:</p>
<ul>
<li><p>Unhandled reverts should be avoided at all costs. An attacker should not be able manipulate state to force this case and the application should be designed in such a way to avoid irrecoverable execution. If this is not possible, the contract admin should have access to a permissioned function to reset critical paths.</p>
</li>
<li><p>The execution of callback functions should likely not be permissionless, especially if they are sensitive to on-chain state and/or timing that could be manipulated by an attacker. This includes trigger conditions that should also be resistant to manipulation.</p>
</li>
<li><p>Every state transition should be properly accounted for and trigger conditions should generally be re-checked during execution.</p>
</li>
<li><p>Sensitive off-chain information, such as API endpoints and associated secrets, should be sufficiently protected if exposed on-chain.</p>
</li>
</ul>
<p>That is all for this article. I hope it gives developers and security researchers alike some inspiration when it comes to reviewing the integration of these lesser-discussed Chainlink services. Watch out for a couple of additional findings from this audit related to the Uniswap v3 integration and some tick weirdness that will be explored in a future article!</p>
]]></content:encoded></item><item><title><![CDATA[A Brief Introduction to RISC Zero & Steel]]></title><description><![CDATA[2025 is the year of privacy-preserving tech. Here’s a succinct overview of RISC Zero.
RISC Zero
The RISC Zero zkVM is an implementation of the RISC-V architecture as an arithmetic circuit. Specifically, the zkVM implements RV32IM which is the 32-bit ...]]></description><link>https://surfing-solodit.com/a-brief-introduction-to-risc-zero-steel</link><guid isPermaLink="true">https://surfing-solodit.com/a-brief-introduction-to-risc-zero-steel</guid><category><![CDATA[RISC Zero]]></category><category><![CDATA[zero-knowledge]]></category><category><![CDATA[zero-knowledge-proofs]]></category><category><![CDATA[Cryptography]]></category><category><![CDATA[risc-v]]></category><category><![CDATA[zkvm]]></category><category><![CDATA[Blockchain]]></category><category><![CDATA[zk stark]]></category><category><![CDATA[groth16]]></category><category><![CDATA[zk-snark]]></category><dc:creator><![CDATA[Giovanni Di Siena]]></dc:creator><pubDate>Sat, 25 Jan 2025 11:48:44 GMT</pubDate><content:encoded><![CDATA[<p>2025 is the year of privacy-preserving tech. Here’s a succinct overview of RISC Zero.</p>
<h2 id="heading-risc-zero">RISC Zero</h2>
<p>The <strong>RISC Zero zkVM</strong> is an implementation of the <strong>RISC-V architecture</strong> as an <strong>arithmetic circuit</strong>. Specifically, the zkVM implements <code>RV32IM</code> which is the <strong>32-bit base instruction set</strong> along with the <strong>multiplication feature</strong>, extended using the <code>ECALL</code> instruction to add <strong>optimized cryptographic instructions</strong>. Additional technical details can be found <a target="_blank" href="https://dev.risczero.com/api/zkvm/zkvm-specification#the-zkvm-execution-model">here</a>.</p>
<p>Mathematically, the <code>RV32IM</code> circuit encodes RISC-V as <strong>polynomial constraints</strong> within a <strong>STARK-based proving system</strong>. In short, the <strong>memory and register transitions</strong> of the emulated processor at each <strong>clock cycle</strong> are captured within the <strong>execution trace</strong>, providing a complete snapshot of the guest program being executed.</p>
<p><a target="_blank" href="https://github.com/risc0/risc0/blob/main/website/static/diagrams/from-rust-to-receipt.png"><img src="https://dev.risczero.com/assets/images/from-rust-to-receipt-23117368c4f46d78c8cac3b753245a5a.png" alt="zkVM Overview | RISC Zero Developer Docs" /></a></p>
<ol>
<li><p>The <strong>guest program</strong> is compiled to an executable format known as the <strong>ELF binary</strong>.</p>
</li>
<li><p>The untrusted <strong>host program</strong> orchestrates verifiable computation, handling two-way communication with the zkVM environment and transmitting <strong>private inputs</strong> to the guest program.</p>
<ul>
<li><p><a target="_blank" href="https://docs.rs/risc0-zkvm/1.2.1/risc0_zkvm/struct.ExecutorEnvBuilder.html#method.write"><code>ExecutorEnvBuilder::write</code></a> is used to pass <em>private</em> data from the <em>host</em> to the <em>guest</em>.</p>
</li>
<li><p><a target="_blank" href="https://docs.rs/risc0-zkvm/1.2.1/risc0_zkvm/guest/env/fn.write.html"><code>env::write</code></a> is used to pass <em>private</em> data from the <em>guest</em> to the <em>host</em>.</p>
</li>
<li><p><a target="_blank" href="https://docs.rs/risc0-zkvm/1.2.1/risc0_zkvm/guest/env/fn.commit.html"><code>env::commit</code></a> is used to pass <em>public</em> data from the <em>guest</em> to the <em>host</em>.</p>
</li>
</ul>
</li>
<li><p>The <strong>executor</strong> runs the ELF binary and records the execution trace (aka the <strong>session</strong>).</p>
</li>
<li><p>The <strong>prover</strong> checks and proves the validity of the session, producing a cryptographic <strong>receipt</strong> that attests to the execution of the guest program.</p>
 <div data-node-type="callout">
 <div data-node-type="callout-emoji">⚠️</div>
 <div data-node-type="callout-text"><strong>Proving with private data should be done with local proof generation</strong> to ensure it never leaves the machine, as the prover can see all private information involved.</div>
 </div>

<ol>
<li><p>The <strong>receipt claim</strong> portion of the receipt contains the journal and other important details.</p>
<ul>
<li><p>The <strong>journal</strong> portion of the receipt claim contains the <strong>public output</strong> committed by the guest.</p>
</li>
<li><p>The cryptographic <strong>image ID</strong> indicates the method or boot image for zkVM execution.</p>
</li>
<li><p>The guest program <strong>exit status</strong> and <strong>memory state</strong> are also included in the claim.</p>
</li>
</ul>
</li>
<li><p>The <strong>seal</strong> portion of the receipt is a <strong>zk-STARK</strong> that attests to the receipt claim.</p>
<ul>
<li>The <strong>control ID</strong> is the first entry in the seal. It is the <strong>Merkle hash</strong> of the contents of the <strong>control columns</strong>, assumed to be known to the verifier as part of the circuit definition.</li>
</ul>
</li>
</ol>
</li>
<li><p>Verification of the receipt provides a cryptographic assurance of honest journal creation.</p>
<p> <a target="_blank" href="https://github.com/risc0/risc0/blob/main/website/docs/proof-system/assets/proof-system-layers.png"><img src="https://miro.medium.com/v2/resize:fit:1400/1*pxRtmI1Jf_Ldy-MLDsWVGQ.png" alt /></a></p>
<ol>
<li><p>The <strong>RISC-V Circuit</strong> proves zkVM execution by dividing the session into multiple segments and generating a proof for each segment, forming a <strong>composite receipt</strong> of multiple STARKs.</p>
</li>
<li><p>The <strong>Recursion Circuit</strong> uses incrementally verifiable computation to combine the proofs of a composite receipt into a single STARK, generating a <strong>succinct receipt</strong>. It is also used to aggregate and efficiently verify multiple succinct receipts in a similar manner. More details can be found <a target="_blank" href="https://youtu.be/x0-7Y46bQO0?feature=shared&amp;t=1310">here</a> and a full example analogous to verifiable encryption with RSA can be found <a target="_blank" href="https://github.com/risc0/risc0/tree/release-1.2/examples/composition">here</a>.</p>
</li>
<li><p>The <strong>Groth16 Circuit</strong> converts a succinct receipt into a single <strong>Groth16 receipt</strong>, acting as a compact SNARK wrapper around the larger STARK proof that can be verified on-chain by calling <code>IRiscZeroVerifier::verify</code>.</p>
</li>
</ol>
</li>
</ol>
<div data-node-type="callout">
<div data-node-type="callout-emoji">⚠️</div>
<div data-node-type="callout-text"><strong>To keep fake receipts and verification bypass of </strong><code>RISC0_DEV_MODE</code><strong> out of production environments, it is recommended to build with the </strong><code>disable-dev-mode</code><strong> feature flag:</strong></div>
</div>

<div class="hn-table">
<table>
<thead>
<tr>
<td></td><td><code>disable-dev-mode</code> off (default)</td><td><code>disable-dev-mode</code> on</td></tr>
</thead>
<tbody>
<tr>
<td><code>RISC0_DEV_MODE=true</code></td><td>dev-mode activated</td><td>prover panic</td></tr>
<tr>
<td><code>RISC0_DEV_MODE=false</code> or unset</td><td>default project behaviour</td><td>default project behaviour</td></tr>
</tbody>
</table>
</div><h3 id="heading-parity-example">Parity Example</h3>
<p>Suppose we want to prove the parity of a integer value without revealing the value itself. The trivial guest program could look something like this:</p>
<pre><code class="lang-rust"><span class="hljs-meta">#![no_main]</span>
<span class="hljs-meta">#![no_std]</span>

<span class="hljs-keyword">use</span> risc0_zkvm::guest::env;

risc0_zkvm::guest::entry!(main);

<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() {
    <span class="hljs-comment">// Load the number from the host</span>
    <span class="hljs-keyword">let</span> x: <span class="hljs-built_in">u32</span> = env::read();

    <span class="hljs-comment">// Compute the product while being careful with integer overflow</span>
    <span class="hljs-keyword">let</span> is_even: <span class="hljs-built_in">bool</span> = x % <span class="hljs-number">2</span> == <span class="hljs-number">0</span>;

    <span class="hljs-comment">// Commit the result to the journal without exposing the value</span>
    env::commit(&amp;is_even);
}
</code></pre>
<p>Note that it is perfectly fine to panic within the guest, for example with the following snippet if we wanted to consider non-zero integers only:</p>
<pre><code class="lang-rust"><span class="hljs-comment">// Verify the value is non-zero</span>
<span class="hljs-keyword">if</span> x == <span class="hljs-number">0</span> {
   <span class="hljs-built_in">panic!</span>(<span class="hljs-string">"Number is zero"</span>)
}
</code></pre>
<p>We would just need to make sure to handle it appropriately within host, but will omit this for simplicity. The host program can be split into multiple files for some logical separation. Here is an idea of how the proving could look in <code>lib.rs</code>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> is_even_methods::IS_EVEN_ELF;
<span class="hljs-keyword">use</span> risc0_zkvm::{default_prover, ExecutorEnv, Receipt};

<span class="hljs-comment">// Compute whether the value is even inside the zkVM</span>
<span class="hljs-keyword">pub</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">is_even</span></span>(x: <span class="hljs-built_in">u32</span>) -&gt; (Receipt, <span class="hljs-built_in">bool</span>) {
    <span class="hljs-keyword">let</span> env = ExecutorEnv::builder()
        <span class="hljs-comment">// Send x to the guest</span>
        .write(&amp;x)
        .unwrap()
        .build()
        .unwrap();

    <span class="hljs-comment">// Obtain the default prover.</span>
    <span class="hljs-keyword">let</span> prover = default_prover();

    <span class="hljs-comment">// Produce a receipt by proving the specified ELF binary.</span>
    <span class="hljs-keyword">let</span> receipt = prover.prove(env, IS_EVEN_ELF).unwrap().receipt;

    <span class="hljs-comment">// Extract journal of receipt (i.e. output b, where b = x % 2 == 0)</span>
    <span class="hljs-keyword">let</span> b: <span class="hljs-built_in">bool</span> = receipt.journal.decode().expect(
        <span class="hljs-string">"Journal output should deserialize into the same types (&amp; order) that it was written"</span>,
    );

    <span class="hljs-comment">// Report the result</span>
    <span class="hljs-built_in">println!</span>(<span class="hljs-string">"I know the value is {}, and I can prove it!"</span>, b);

    (receipt, b)
}

<span class="hljs-meta">#[cfg(test)]</span>
<span class="hljs-keyword">mod</span> tests {
    <span class="hljs-keyword">use</span> super::*;

    <span class="hljs-meta">#[test]</span>
    <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">test_is_even</span></span>() {
        <span class="hljs-keyword">const</span> TEST_VALUE: <span class="hljs-built_in">u32</span> = <span class="hljs-number">5</span>;
        <span class="hljs-keyword">let</span> (_, result) = is_even(<span class="hljs-number">5</span>);
        <span class="hljs-keyword">let</span> actual: <span class="hljs-built_in">bool</span> = TEST_VALUE % <span class="hljs-number">2</span> == <span class="hljs-number">0</span>;
        <span class="hljs-built_in">assert_eq!</span>(
            result,
            actual,
            <span class="hljs-string">"We expect the zkVM output to be {}"</span>, actual
        );
    }
}
</code></pre>
<p>And the rest of the host program in <code>main.rs</code>:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> rand::Rng;
<span class="hljs-keyword">use</span> is_even_methods::EVEN_ID;

<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() {
    <span class="hljs-comment">// Initialize tracing. To view logs, run `RUST_LOG=info cargo run`</span>
    tracing_subscriber::fmt()
        .with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
        .init();

    <span class="hljs-comment">// Generate a random number</span>
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> rng = rand::thread_rng();
    <span class="hljs-keyword">let</span> x: <span class="hljs-built_in">u32</span> = rng.gen::&lt;<span class="hljs-built_in">u32</span>&gt;();

    <span class="hljs-keyword">let</span> (receipt, _) = is_even(<span class="hljs-number">5</span>);

    <span class="hljs-comment">// Here is where one would send 'receipt' over the network...</span>

    <span class="hljs-comment">// Verify receipt, panic if it's wrong</span>
    receipt.verify(IS_EVEN_ID).expect(
        <span class="hljs-string">"Code you have proven should successfully verify; did you specify the correct image ID?"</span>,
    );
}
</code></pre>
<h2 id="heading-steel">Steel</h2>
<p><strong>Steel</strong> is a production-ready <strong>smart contract execution prover</strong> that enables unbounded runtime for EVM apps by proving correctness of off-chain execution without the need for writing ZK circuits.</p>
<p><a target="_blank" href="https://github.com/risc0/risc0/blob/main/website/static/diagrams/security-model-diagram.svg"><img src="https://dev.risczero.com/assets/images/security-model-diagram-70478725ab3b760ff9bf29eee53a4f74.svg" alt="Security Model Diagram" /></a></p>
<ul>
<li><p>Steel uses <a target="_blank" href="https://docs.rs/revm/latest/revm/">revm</a> for simulation of an <strong>EVM environment</strong> which is constructed and pre-populated in the host program before being <strong>passed as an input</strong> to the guest program.</p>
<ul>
<li><p><strong>EVM state</strong> can be queried within the RISC Zero zkVM using alloy’s <a target="_blank" href="https://alloy.rs/examples/sol-macro/index.html">sol! macro</a>.</p>
</li>
<li><p><strong>Merkle storage proofs</strong> are verified by the guest to validate the <strong>integrity of RPC data</strong>.</p>
</li>
</ul>
</li>
<li><p>The <strong>STARK proofs</strong> produced by the zkVM are <strong>wrapped in circom Groth16 SNARKs</strong>, which are significantly smaller and faster (read: cheaper) to verify on-chain.</p>
<ul>
<li><p>The STARK proof <strong>control root</strong> is passed as a public input to the SNARK, allowing for <strong>updates</strong> to the RISC-V prover <strong>without requiring a new trusted setup ceremony</strong>.</p>
</li>
<li><p>It is recommended to use the <a target="_blank" href="https://github.com/risc0/risc0-ethereum/blob/main/contracts/version-management-design.md#router">router</a> to <strong>forward verification</strong> to the appropriate contract. Additional details and considerations can be found in the on-chain verifier and version management design document <a target="_blank" href="https://github.com/risc0/risc0-ethereum/blob/main/contracts/version-management-design.md">here</a>.</p>
</li>
</ul>
</li>
<li><p><strong>Steel commitments</strong>, comprising a <strong>block identifier</strong> and <strong>block digest</strong>, are validated on-chain to guarantee the correctness of blockchain state associated with the Steel proof.</p>
<ul>
<li><p><strong>Block hash commitments</strong> are verified with the <code>blockhash</code> <strong>opcode</strong>, up to 256 blocks.</p>
</li>
<li><p><strong>Beacon block commitments</strong> are verified with the <a target="_blank" href="https://eips.ethereum.org/EIPS/eip-4788">EIP-4788</a> <strong>beacon root contract</strong>, extending L1 Ethereum validation time to just over 24 hours.</p>
</li>
<li><p><strong>Steel history</strong> separates the <strong>execution and commitment blocks</strong> to enable state older than 24 hours to be queried, up to the Cancun upgrade date of March 13 2024.</p>
</li>
</ul>
</li>
</ul>
<h3 id="heading-counter-example">Counter Example</h3>
<p>Consider the trivial smart contract that allows <code>counter</code> to be incremented if, and only if, the ERC20 balance of the sender is at least one token:</p>
<pre><code class="lang-solidity"><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.20;</span>

<span class="hljs-keyword">import</span> {<span class="hljs-title">IERC20</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"@openzeppelin/contracts/token/ERC20/IERC20.sol"</span>;

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Counter</span> </span>{
    IERC20 <span class="hljs-keyword">public</span> <span class="hljs-keyword">immutable</span> token;

    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> counter;

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> _token</span>) </span>{
            token <span class="hljs-operator">=</span> IERC20(_token);
        }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">increment</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> account</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-built_in">require</span>(account <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, <span class="hljs-string">"Invalid sender"</span>);
        <span class="hljs-built_in">require</span>(IERC20(token).balanceOf(account) <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> token.decimals(), <span class="hljs-string">"Insufficient balance"</span>);

        <span class="hljs-operator">+</span><span class="hljs-operator">+</span>counter;
    }
}
</code></pre>
<p>With Steel, leveraging RISC Zero as a coprocessor for efficient proof generation and verification, this becomes:</p>
<pre><code class="lang-solidity"><span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.20;</span>

<span class="hljs-keyword">import</span> {<span class="hljs-title">IRiscZeroVerifier</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"risc0-ethereum/IRiscZeroVerifier.sol"</span>;
<span class="hljs-keyword">import</span> {<span class="hljs-title">Steel</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"risc0-ethereum/contracts/src/steel/Steel.sol"</span>;
<span class="hljs-keyword">import</span> {<span class="hljs-title">ImageID</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"./ImageID.sol"</span>; <span class="hljs-comment">// auto-generated contract after running `cargo build`.</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">SteelCounter</span> </span>{
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">immutable</span> token;
    IRiscZeroVerifier <span class="hljs-keyword">public</span> <span class="hljs-keyword">immutable</span> verifier;

    <span class="hljs-keyword">uint256</span> <span class="hljs-keyword">public</span> counter;
    <span class="hljs-keyword">mapping</span>(<span class="hljs-keyword">bytes32</span> <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> <span class="hljs-keyword">bool</span>) <span class="hljs-keyword">public</span> processedJournals;

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> _token, <span class="hljs-keyword">address</span> _verifier</span>) </span>{
            token <span class="hljs-operator">=</span> _token;
            verifier <span class="hljs-operator">=</span> IRiscZeroVerifier(_verifier);
        }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">increment</span>(<span class="hljs-params"><span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> journalData, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> seal</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-comment">// Decode the public outputs of the zkVM guest program</span>
        Journal <span class="hljs-keyword">memory</span> journal <span class="hljs-operator">=</span> <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">decode</span>(journalData, (Journal));

        <span class="hljs-comment">// Validate journal data &amp; Steel commitment to ensure the proof can be trusted</span>
        <span class="hljs-built_in">require</span>(journal.token <span class="hljs-operator">=</span><span class="hljs-operator">=</span> token, <span class="hljs-string">"Invalid token address"</span>);
        <span class="hljs-built_in">require</span>(journal.account <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, <span class="hljs-string">"Invalid sender address"</span>);
        <span class="hljs-built_in">require</span>(Steel.validateCommitment(journal.commitment), <span class="hljs-string">"Invalid Steel Commitment"</span>);

        <span class="hljs-comment">// Compute the journal hash</span>
        <span class="hljs-keyword">bytes32</span> journalHash <span class="hljs-operator">=</span> <span class="hljs-built_in">sha256</span>(journalData);

        <span class="hljs-comment">// Mark the journal as processed to prevent replay</span>
        <span class="hljs-built_in">require</span>(<span class="hljs-operator">!</span>processedJournals[journalHash], <span class="hljs-string">"Journal already processed"</span>);
            processedJournals[journalHash] <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;

        <span class="hljs-comment">// Verify the execution proof &amp; increment the counter if successful</span>
        verifier.verify(seal, imageID, journalHash);
        <span class="hljs-operator">+</span><span class="hljs-operator">+</span>counter;
    }
}
</code></pre>
<p>Note that the proof is verified if, and only if, the token address, sender address, and the Steel commitment are valid. Successful verification of the RISC Zero Groth16 proof thus ensures that the account balance is at least one token, so <code>counter</code> is incremented without repeating EVM execution.</p>
<p>With on-chain verification in place, the corresponding guest program could look something like this:</p>
<pre><code class="lang-rust"><span class="hljs-meta">#![no_main]</span>

<span class="hljs-keyword">use</span> alloy_primitives::{Address, U256};
<span class="hljs-keyword">use</span> alloy_sol_types::sol;
<span class="hljs-keyword">use</span> risc0_steel::{
    ethereum::{EthEvmInput, ETH_SEPOLIA_CHAIN_SPEC},
    Commitment, Contract,
};
<span class="hljs-keyword">use</span> risc0_zkvm::guest::env;

risc0_zkvm::guest::entry!(main);

<span class="hljs-comment">// Specify the function to call using the [`sol!`] macro.</span>
<span class="hljs-comment">// This parses the Solidity syntax to generate a struct that implements the `SolCall` trait.</span>
sol! {
    interface IERC20 {
        function balanceOf(address account) public view returns (uint);
        function decimals() public view returns (uint);
    }
}

<span class="hljs-comment">// ABI encodable journal data.</span>
sol! {
    <span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Journal</span></span> {
        Commitment commitment;
        address token;
        address account;
    }
}

<span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() {
    <span class="hljs-comment">// Load the EVM input, token address, and sender address from the host</span>
    <span class="hljs-keyword">let</span> input: EthEvmInput = env::read();
    <span class="hljs-keyword">let</span> contract: Address = env::read();
    <span class="hljs-keyword">let</span> sender: Address = env::read();

    <span class="hljs-comment">// Convert the input into an `EvmEnv` for execution, specifying the chain configuration.</span>
    <span class="hljs-comment">// This checks that the state matches the state root in the header provided in the input.</span>
    <span class="hljs-keyword">let</span> env = input.into_env().with_chain_spec(&amp;ETH_SEPOLIA_CHAIN_SPEC);

    <span class="hljs-comment">// Execute the view call(s), returning the result in the type(s) generated by the `sol!` macro.</span>
    <span class="hljs-keyword">let</span> balance_call = IERC20::balanceOfCall { sender };
    <span class="hljs-keyword">let</span> decimals_call = IERC20::decimalsCall {};
    <span class="hljs-keyword">let</span> contract = Contract::::new(contract, &amp;env);
    <span class="hljs-keyword">let</span> balance = contract.call_builder(&amp;balance_call).call();
    <span class="hljs-keyword">let</span> decimals = contract.call_builder(&amp;decimals_call).call();

    <span class="hljs-comment">// Check that the given account holds at least 1 token.</span>
    <span class="hljs-built_in">assert!</span>(balance._0 &gt;= U256::from(decimals));

    <span class="hljs-comment">// Commit the block hash and number used when deriving `view_call_env` to the journal.</span>
    <span class="hljs-keyword">let</span> journal = Journal {
        commitment: env.into_commitment(),
        token: contract,
        account: sender
    };
    env::commit_slice(&amp;journal.abi_encode());
}
</code></pre>
<p>While the host program, including the preflight calls to prepare the inputs that are required to execute the functions in the guest without RPC access, could look something like this:</p>
<pre><code class="lang-rust"><span class="hljs-keyword">use</span> alloy_primitives::{address, Address};
<span class="hljs-keyword">use</span> alloy_sol_types::{sol, SolCall, SolType};
<span class="hljs-keyword">use</span> anyhow::{Context, <span class="hljs-built_in">Result</span>};
<span class="hljs-keyword">use</span> clap::Parser;
<span class="hljs-keyword">use</span> erc20_methods::ERC20_GUEST_ELF;
<span class="hljs-keyword">use</span> risc0_steel::{
    ethereum::{EthEvmEnv, ETH_SEPOLIA_CHAIN_SPEC},
    Commitment, Contract,
};
<span class="hljs-keyword">use</span> risc0_zkvm::{default_executor, ExecutorEnv};
<span class="hljs-keyword">use</span> tracing_subscriber::EnvFilter;
<span class="hljs-keyword">use</span> url::Url;

sol! {
    <span class="hljs-comment">// ERC-20 interface – must match that in the guest.</span>
    interface IERC20 {
        function balanceOf(address account) public view returns (uint);
        function decimals() public view returns (uint);
    }
}

<span class="hljs-comment">/// Function(s) to call, implements the [SolCall] trait.</span>
<span class="hljs-keyword">const</span> BALANCE_CALL: IERC20::balanceOfCall = IERC20::balanceOfCall {
    account: address!(<span class="hljs-string">"d8dA6BF26964aF9D7eEd9e03E53415D37aA96045"</span>), <span class="hljs-comment">// vitalik.eth</span>
};
<span class="hljs-keyword">const</span> DECIMALS_CALL: IERC20::decimalsCall = IERC20::decimalsCall {};

<span class="hljs-comment">/// Address of the deployed contract to call the function(s) on (USDT contract on Sepolia).</span>
<span class="hljs-keyword">const</span> CONTRACT: Address = address!(<span class="hljs-string">"aA8E23Fb1079EA71e0a56F48a2aA51851D8433D0"</span>);
<span class="hljs-comment">/// Address of the caller.</span>
<span class="hljs-keyword">const</span> CALLER: Address = address!(<span class="hljs-string">"f08A50178dfcDe18524640EA6618a1f965821715"</span>);

<span class="hljs-meta">#[derive(Parser, Debug)]</span>
<span class="hljs-meta">#[command(about, long_about = None)]</span>
<span class="hljs-class"><span class="hljs-keyword">struct</span> <span class="hljs-title">Args</span></span> {
    <span class="hljs-comment">/// URL of the RPC endpoint</span>
    <span class="hljs-meta">#[arg(short, long, env = <span class="hljs-meta-string">"RPC_URL"</span>)]</span>
    rpc_url: Url,
}

<span class="hljs-meta">#[tokio::main]</span>
<span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">fn</span> <span class="hljs-title">main</span></span>() -&gt; <span class="hljs-built_in">Result</span>&lt;()&gt; {
    <span class="hljs-comment">// Initialize tracing. To view logs, run `RUST_LOG=info cargo run`</span>
    tracing_subscriber::fmt()
        .with_env_filter(EnvFilter::from_default_env())
        .init();

    <span class="hljs-comment">// Parse the command line arguments.</span>
    <span class="hljs-keyword">let</span> args = Args::parse();

    <span class="hljs-comment">// Create an EVM environment from an RPC endpoint, defaulting to the latest block.</span>
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> env = EthEvmEnv::builder().rpc(args.rpc_url).build().<span class="hljs-keyword">await</span>?;
    <span class="hljs-comment">// The `with_chain_spec` method is used to specify the chain configuration.</span>
    env = env.with_chain_spec(&amp;ETH_SEPOLIA_CHAIN_SPEC);

    <span class="hljs-comment">// Preflight the call(s) to prepare the input that is required to execute the function in</span>
    <span class="hljs-comment">// the guest without RPC access. It also returns the result of the call.</span>
    <span class="hljs-keyword">let</span> <span class="hljs-keyword">mut</span> contract = Contract::preflight(CONTRACT, &amp;<span class="hljs-keyword">mut</span> env);
    <span class="hljs-keyword">let</span> balance = contract.call_builder(&amp;BALANCE_CALL).from(CALLER).call().<span class="hljs-keyword">await</span>?;
    <span class="hljs-built_in">println!</span>(
        <span class="hljs-string">"Call {} Function by {:#} on {:#} returns: {}"</span>,
        IERC20::balanceOfCall::SIGNATURE,
        CALLER,
        CONTRACT,
        returns._0
    );
    <span class="hljs-keyword">let</span> decimals = contract.call_builder(&amp;DECIMALS_CALL).from(CALLER).call().<span class="hljs-keyword">await</span>?;
    <span class="hljs-built_in">println!</span>(
        <span class="hljs-string">"Call {} Function by {:#} on {:#} returns: {}"</span>,
        IERC20::decimalsCall::SIGNATURE,
        CALLER,
        CONTRACT,
        returns._0
    );

    <span class="hljs-comment">// Finally, construct the input from the environment.</span>
    <span class="hljs-keyword">let</span> input = env.into_input().<span class="hljs-keyword">await</span>?;

    <span class="hljs-built_in">println!</span>(<span class="hljs-string">"Running the guest with the constructed input..."</span>);
    <span class="hljs-keyword">let</span> session_info = {
        <span class="hljs-keyword">let</span> env = ExecutorEnv::builder()
            .write(&amp;input)
            .unwrap()
            .build()
            .context(<span class="hljs-string">"failed to build executor env"</span>)?;
        <span class="hljs-keyword">let</span> exec = default_executor();
        exec.execute(env, COUNTER_GUEST_ELF)
            .context(<span class="hljs-string">"failed to run executor"</span>)?
    };

    <span class="hljs-comment">// The journal should be the ABI encoded commitment.</span>
    <span class="hljs-keyword">let</span> commitment = Commitment::abi_decode(session_info.journal.as_ref(), <span class="hljs-literal">true</span>)
        .context(<span class="hljs-string">"failed to decode journal"</span>)?;
    <span class="hljs-built_in">println!</span>(<span class="hljs-string">"{:?}"</span>, commitment);

    <span class="hljs-literal">Ok</span>(())
}
</code></pre>
<h2 id="heading-resources">Resources</h2>
<ul>
<li><p>A glossary of key terminology is available <a target="_blank" href="https://dev.risczero.com/terminology">here</a>.</p>
</li>
<li><p>The <code>risc0-zkvm</code> crate can be found <a target="_blank" href="https://docs.rs/risc0-zkvm">here</a>. A full list of Rust crates can be found <a target="_blank" href="https://github.com/risc0/risc0#rust-libraries">here</a>.</p>
</li>
<li><p>Compatibility of various crates with zkVM can be found in the nightly <a target="_blank" href="https://reports.risczero.com/crates-validation">Crate Validation Report</a>.</p>
</li>
<li><p>Several example zkVM applications can be found <a target="_blank" href="https://github.com/risc0/risc0/tree/release-1.2/examples">here</a>, with some leveraging Steel found <a target="_blank" href="https://github.com/risc0/risc0-ethereum/tree/main/examples">here</a>.</p>
</li>
</ul>
<h2 id="heading-security">Security</h2>
<ul>
<li><p>Various audits can be found in the <a target="_blank" href="https://github.com/risc0/rz-security/tree/main/audits">rz-security</a> repository.</p>
</li>
<li><p>Several security advisories can be found in the <a target="_blank" href="https://github.com/risc0/risc0/security/advisories">risc0</a> repository.</p>
</li>
<li><p>A bug bounty program can be found on <a target="_blank" href="https://hackenproof.com/programs/risc-zero-zkvm">HackenProof</a>.</p>
</li>
<li><p>A cryptographic security model can be found <a target="_blank" href="https://dev.risczero.com/api/security-model">here</a>, with details of the trusted setup <a target="_blank" href="https://dev.risczero.com/api/trusted-setup-ceremony">here</a>.</p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Hello World Computer]]></title><description><![CDATA[Gm and welcome to Surfing Solodit!
The purpose of this blog is to document my smart contract security research, primarily driven by interesting findings uncovered during publicly shareable private engagements with the Cyfrin Audit Team.
Prior to this...]]></description><link>https://surfing-solodit.com/hello-world-computer</link><guid isPermaLink="true">https://surfing-solodit.com/hello-world-computer</guid><dc:creator><![CDATA[Giovanni Di Siena]]></dc:creator><pubDate>Wed, 15 Jan 2025 10:04:50 GMT</pubDate><enclosure url="https://cdn.hashnode.com/res/hashnode/image/stock/unsplash/Sj5efgWguDs/upload/f3487031105d953dba7886d1bec37a55.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>Gm and welcome to Surfing Solodit!</p>
<p>The purpose of this blog is to document my smart contract security research, primarily driven by interesting findings uncovered during <a target="_blank" href="https://github.com/Cyfrin/cyfrin-audit-reports/blob/main">publicly shareable private engagements</a> with the <a target="_blank" href="https://x.com/cyfrinaudits">Cyfrin Audit Team</a>.</p>
<p>Prior to this, I have written a handful of other threads, articles, and many unfinished drafts, the most interesting of which I have compiled below:</p>
<ul>
<li><p>Security review of Sudoswap v2 which yielded a <a target="_blank" href="https://medium.com/cyfrin/smart-contract-security-audit-sudoswap-v2-4ec1076bbe2">live high-severity bug</a> in the <code>LSSVMRouter</code> contract (featured in the <a target="_blank" href="https://newsletter.blockthreat.io/p/blockthreat-week-31-2023">BlockThreat newsletter</a>).</p>
</li>
<li><p>Comparison of the main <a target="_blank" href="https://www.cyfrin.io/blog/uniswap-v4-vs-v3-architectural-changes-and-technical-innovations-with-code-examples">architectural differences between Uniswap v3 and v4</a> with code examples.</p>
</li>
<li><p>Multiple introductory ZK articles [<a target="_blank" href="https://x.com/giovannidisiena/status/1806714618972110871">1</a>, <a target="_blank" href="https://x.com/giovannidisiena/status/1808844780228464648">2</a>, <a target="_blank" href="https://x.com/giovannidisiena/status/1827043419643937073">3</a>].</p>
</li>
<li><p>Interesting <a target="_blank" href="https://x.com/giovannidisiena/status/1841470522925765114">CDP-type protocol findings</a>, uncovered during a <a target="_blank" href="https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2024-09-13-cyfrin-the-standard-smart-vault-v2.0.pdf">private review of The Standard Smart Vault</a>.</p>
</li>
<li><p>Various thoughts on diagramming complex flows [<a target="_blank" href="https://x.com/giovannidisiena/status/1864348248456167893">1</a>, <a target="_blank" href="https://x.com/giovannidisiena/status/1864358089828221201">2</a>, <a target="_blank" href="https://x.com/giovannidisiena/status/1864360890562187412">3</a>].</p>
</li>
<li><p>Considerations for <a target="_blank" href="https://x.com/giovannidisiena/status/1868418906915791258">integrating Chainlink Functions</a> and weird <a target="_blank" href="https://x.com/giovannidisiena/status/1866874971458302393">Solidity/Uniswap things</a> (watch out for the full article coming soon).</p>
</li>
<li><p><a target="_blank" href="https://x.com/giovannidisiena/status/1876312860780806639">Password management &amp; MFA guide</a>.</p>
</li>
</ul>
<p>Going forward, I intend for this to be the primary medium on which to share my articles. Welcome aboard and see you in the arena.</p>
]]></content:encoded></item></channel></rss>