<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:wfw="http://wellformedweb.org/CommentAPI/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:sy="http://purl.org/rss/1.0/modules/syndication/" xmlns:slash="http://purl.org/rss/1.0/modules/slash/" version="2.0">
  <channel>
    <title>soundsonacid</title>
    <link>https://soundsonac.id/</link>
    <atom:link href="https://soundsonac.id/atom.xml" rel="self" type="application/rss+xml"/>
    <description>solana / systems / trading</description>
    <lastBuildDate>Thu, 09 Jul 2026 14:19:18 GMT</lastBuildDate>
    <language>en</language>
    <generator>Lume v3.2.5</generator>
    <item>
      <title>Rent</title>
      <link>https://soundsonac.id/blog/rent/</link>
      <guid isPermaLink="false">https://soundsonac.id/blog/rent/</guid>
      <description>soundsonacid - Rent</description>
      <content:encoded>
        <![CDATA[<p>Someone came to me today with a weird program error in simulation, where an account was throwing <code>InsufficientFundsForRent</code> despite being properly funded.  After a few minutes looking through their test output, I remembered a bug that I found in a <a href="https://github.com/anza-xyz/mollusk">very commonly used test harness</a> a few months ago that caused the same problem.  Turns out that they were using an older-ish version of an <a href="https://github.com/LiteSVM/litesvm">alternative test harness</a> which <em>also</em> had this bug!</p>
<p>What happened here? How did we find this? And..</p>
<h2 id="what-is-rent-anyway" tabindex="-1"><a class="header-anchor" href="https://soundsonac.id/blog/rent/#what-is-rent-anyway"><span>what is rent anyway?</span></a></h2>
<p>In Solana, we have this thing called &quot;rent&quot;.  Basically, when you want to store some state on the blockchain, you have to create an &quot;account&quot; to store it, and you pay <em>rent</em> because storage isn't free. There's been a bunch of changes over the years to how rent works which aren't really relevant here except for <a href="https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0194-deprecate-rent-exemption-threshold.md">SIMD-194</a>.</p>
<p>The Solana runtime stores the current rent parameters in the Rent &quot;sysvar&quot;. <sup class="footnote-ref"><a href="https://soundsonac.id/blog/rent/#fn1" id="fnref1">[1]</a></sup></p>
<h2 id="simd-194" tabindex="-1"><a class="header-anchor" href="https://soundsonac.id/blog/rent/#simd-194"><span>simd-194</span></a></h2>
<p>Before SIMD-194's activation, the Rent sysvar looked like this:</p>
<pre><code class="language-rust">pub struct Rent {
    pub lamports_per_byte_year: u64,
    pub exemption_threshold: f64,
    pub burn_percent: u8,
}
</code></pre>
<p>SIMD-194 changes the rent semantics by:</p>
<ul>
<li>Deprecating the use of the <code>exemption_threshold</code> field</li>
<li>Halving <code>exemption_threshold</code> from 2.0 to 1.0</li>
<li>Renaming <code>lamports_per_byte_year</code> to <code>lamports_per_byte</code></li>
<li>Doubling <code>lamports_per_byte</code> from 3,480 to 6,960</li>
</ul>
<p>The stated goal of 194 is to replace all <code>f64</code> math for rent calculations in official SDKs with <code>u64</code> math, which reduces the compute unit consumption on functions like <code>Rent::minimum_balance</code> from 256 CUs to 8. <sup class="footnote-ref"><a href="https://soundsonac.id/blog/rent/#fn2" id="fnref2">[2]</a></sup> Given that <a href="https://github.com/solana-foundation/solana-improvement-documents/blob/main/proposals/0084-disable-rent-fees-collection.md">SIMD-84</a> disabled rent collection years ago, this change makes a lot of sense.</p>
<p>194 activated on <a href="https://discord.com/channels/428295358100013066/1483498477278203904/1483530546372935741">March 17th</a> on Solana mainnet.</p>
<pre><code class="language-rust">// Post SIMD-194 rent layout
pub struct Rent {
    pub lamports_per_byte: u64,
    #[deprecated(since = &quot;2.X.X&quot;, note = &quot;Use only `lamports_per_byte`
instead&quot;)]
    pub exemption_threshold: f64,
    pub burn_percent: u8,
}
</code></pre>
<h2 id="program-frameworks-test-harnesses" tabindex="-1"><a class="header-anchor" href="https://soundsonac.id/blog/rent/#program-frameworks-test-harnesses"><span>program frameworks &amp; test harnesses</span></a></h2>
<p>If you're writing a Solana program, you're probably using a framework of some type. These abstract away things like manually deserializing accounts &amp; instruction data from the input data pointer which usually don't really matter.  The important thing here is that they also usually provide wrappers for dealing with sysvars, since they're standardized across the cluster.  You call <code>Rent::get()</code>, it makes the <code>sol_get_sysvar</code> syscall, and voilà: you have a <code>Rent</code> that you can do some stuff with.</p>
<p>The <em>value</em> of the sysvar returned by <code>sol_get_sysvar</code> is set by the program's runtime environment.  In a test environment, the sysvars are set by the test harness when it constructs the runtime that your programs will be simulated against.  There's no builtin mechanism for making sure that the value set for a given sysvar is realistic or meaningful at all, so you (or the test harness) can set them to whatever you want.</p>
<h2 id="createaccount-with_minimum_balance" tabindex="-1"><a class="header-anchor" href="https://soundsonac.id/blog/rent/#createaccount-with_minimum_balance"><span>CreateAccount::with_minimum_balance</span></a></h2>
<p><a href="https://github.com/anza-xyz/pinocchio">Pinocchio</a> is a very popular framework for writing performant Solana programs.</p>
<p>Pinocchio's <code>CreateAccount::with_minimum_balance</code> is a nice helper for creating a new account of a given size &amp; funding it with the minimum amount of lamports required for rent exemption.  This function basically invokes the system program to initialize a new account, and importantly, ask the Rent sysvar what the &quot;minimum balance for rent exemption&quot; is for an account of this size.</p>
<p>The way it asks that is <code>Rent::try_minimum_balance</code>, which calls <code>Rent::minimum_balance_unchecked</code>.</p>
<pre><code class="language-rust">// pinocchio 0.11.1
#[inline(always)]
pub fn minimum_balance_unchecked(&amp;self, data_len: usize) -&gt; u64 {
    (ACCOUNT_STORAGE_OVERHEAD /* 128 */ + data_len as u64) * self.lamports_per_byte
}
</code></pre>
<p>The <em>implicit assumption</em> that's made here is that the <code>exemption_threshold</code> on the Rent sysvar is 1.0.  Someone took the easy way out of avoiding floating-point math in program SDKs and didn't bother to check what the <code>exemption_threshold</code> actually <em>was</em> before deciding how many lamports to fund this account with.</p>
<h2 id="sysvar-mismatches" tabindex="-1"><a class="header-anchor" href="https://soundsonac.id/blog/rent/#sysvar-mismatches"><span>sysvar mismatches</span></a></h2>
<p>So what happens if the test harness provides the pre-194 rent layout with <code>lamports_per_byte_year</code> as 3,480 and <code>exemption_threshold</code> as 2.0 to the runtime, and we try to create a token account, which is 165 bytes?</p>
<p>Pinocchio's <code>Rent::try_minimum_balance</code> will return <code>(128 + 165) * 3_480</code>, so the new account will be funded with 1,019,640 lamports.</p>
<p>The <a href="https://github.com/solana-program/token/blob/main/program/src/processor.rs#L83">token program's handler</a> for the <code>InitializeAccount3</code> instruction checks whether or not the to-be-initialized account is <em>rent-exempt</em>, that is, its lamports balance meets or exceeds the minimum cost for this account to stay around.  This value is determined by the Rent sysvar's parameters.  To figure this out, it calls <code>solana_rent::Rent::is_exempt</code>, which similarly to <code>pinocchio::sysvar::Rent</code> <sup class="footnote-ref"><a href="https://soundsonac.id/blog/rent/#fn3" id="fnref3">[3]</a></sup> calls its implementation of <code>minimum_balance_unchecked</code>.</p>
<p>What does <code>solana_rent::Rent::minimum_balance_unchecked</code>  do?</p>
<pre><code class="language-rust">#[inline(always)]
pub fn minimum_balance_unchecked(&amp;self, data_len: usize) -&gt; u64 {
    let bytes = data_len as u64;

    // There are two cases where it is possible to avoid floating-point
    // operations:
    //
    //   1)  exemption threshold is `1.0` (the SIMD-0194 default)
    //   2)  exemption threshold is `2.0` (the current default)
    //
    // In all other cases, perform the full calculation using floating-point
    // operations. Note that on BPF targets, floating-point operations are
    // not supported, so panic in that case.
    #[allow(deprecated)]
    if self.exemption_threshold == SIMD0194_EXEMPTION_THRESHOLD {
        (ACCOUNT_STORAGE_OVERHEAD + bytes) * self.lamports_per_byte
    } else if self.exemption_threshold == CURRENT_EXEMPTION_THRESHOLD {
        2 * (ACCOUNT_STORAGE_OVERHEAD + bytes) * self.lamports_per_byte
    } else {
        #[cfg(not(target_arch = &quot;bpf&quot;))]
        {
            (((ACCOUNT_STORAGE_OVERHEAD + bytes) * self.lamports_per_byte) as f64
                * f64::from_le_bytes(self.exemption_threshold)) as u64
        }
        #[cfg(target_arch = &quot;bpf&quot;)]
        panic!(&quot;Floating-point operations are not supported on BPF targets&quot;);
    }
}
</code></pre>
<p>Oh! This is <em>clearly</em> not what our pinocchio calculation did.  Whoever wrote this code decided wisely against assuming that all consumers forever will use the post-194 layout.  You can see that this handler is also perfectly capable of processing <em>any</em> arbitrary <code>f64</code> value for <code>exemption_threshold</code>.  This is certainly a better design, even if everyone is <em>supposed</em> to be using the post-194 layout.</p>
<p>Unfortunately, the implementation drift in <code>minimum_balance_unchecked</code> causes a bit of a problem for our test environment.</p>
<p>Specifically, if the provided Rent sysvar's exemption threshold is <code>CURRENT_EXEMPTION_THRESHOLD</code> (1.0), the calculation here will be <code>2 * (128 + 165) * 3_480</code>.  The savvy reader may notice that this is twice as much as we initialized it to, and so our transaction will <a href="https://github.com/solana-program/token/blob/main/program/src/processor.rs#L109">fail</a> with <code>TokenError::NotRentExempt</code>.</p>
<p>Fundamentally, the problem here is that while pinocchio's interface assumed the post-194 rent layout, the test harness initialized the runtime environment with the pre-194 rent layout. The token program didn't care either way, as long as the account was rent-exempt. <sup class="footnote-ref"><a href="https://soundsonac.id/blog/rent/#fn4" id="fnref4">[4]</a></sup></p>
<p>There's a couple obvious ways to fix this:</p>
<ul>
<li>Have <code>CreateAccount::with_minimum_balance</code> handle both rent layouts the same way <code>solana_rent::Rent</code> does.</li>
<li>Make the test harness provide the post-194 rent layout to adapt for upcoming mainnet changes.</li>
</ul>
<p>So how was this <em>actually</em> fixed?</p>
<h2 id="program-frameworks-test-harnesses-part-2" tabindex="-1"><a class="header-anchor" href="https://soundsonac.id/blog/rent/#program-frameworks-test-harnesses-part-2"><span>program frameworks &amp; test harnesses, part 2</span></a></h2>
<p>When I found this, I was using <code>pinocchio-system</code> v0.6.0.  Sometime between v0.5.0 and v0.6.0, <a href="https://github.com/anza-xyz/pinocchio/commit/009301423f920fd105bd32a25560d127b6f0bf4f">this PR</a> was merged, which switched the <code>minimum_balance_unchecked</code> semantics to assume a post-194 layout.  Funny enough, v0.5.0's <code>minimum_balance_unchecked</code> actually <em>did</em> handle both layouts correctly.</p>
<p>Either way, this change generally shouldn't have been a problem.  <code>pinocchio-system</code> v0.6.0 released on April 8th, 22 days after 194 activated on mainnet.  One third-party harness, LiteSVM, <a href="https://github.com/LiteSVM/litesvm/pull/307">adapted to the new layout</a> in v0.11.0 3 days post-activation.</p>
<p>Pinocchio is primarily maintained by <a href="https://github.com/anza-xyz">Anza</a>.  Anza is the core dev team behind the Agave validator client, and among other things, the Mollusk test framework.</p>
<p>Mollusk did not switch rent layout until <a href="https://github.com/anza-xyz/mollusk/pull/257">May 26th</a>.  This means that if you upgraded to <code>pinocchio-system</code> v0.6.0 anytime before May 26th and used Mollusk to drive your program test suite, you probably saw some bizarre <code>InsufficientFundsForRent</code> errors caused by this bug.</p>
<h2 id="conclusion" tabindex="-1"><a class="header-anchor" href="https://soundsonac.id/blog/rent/#conclusion"><span>conclusion</span></a></h2>
<p>Weird little bugs like this are not uncommon in Solana.  They probably happen at a much higher rate than in other types of programming.  This is extremely frustrating devex.  We've just seen that using two very popular crates, maintained by the same team, could have forced you into fighting a very sneaky implementation drift bug in an oft-forgotten part of the protocol.</p>
<p>Solana devs typically call this &quot;chewing glass&quot;.</p>
<p>I don't really enjoy the taste of glass.  It's very similar to blood.</p>
<h4 id="example-repo" tabindex="-1"><a class="header-anchor" href="https://soundsonac.id/blog/rent/#example-repo"><span>example repo</span></a></h4>
<p>If you want to see this bug in action, <a href="https://github.com/soundsonacid/rent">this repo</a> shows what happens on <code>pinocchio-system</code> v0.6.0 when simulated against a couple different versions of LiteSVM &amp; Mollusk.</p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
<li id="fn1" class="footnote-item"><p><a href="https://docs.anza.xyz/runtime/sysvars">Sysvars</a> are accounts which hold various ground truths about the cluster, such as the current slot, recent blockhashes, and the last slot at which the cluster restarted. <a href="https://soundsonac.id/blog/rent/#fnref1" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn2" class="footnote-item"><p>A &quot;compute unit&quot; is a measure of how expensive it is for a validator to execute a transaction. 1 sBPF instruction == 1 CU. <a href="https://soundsonac.id/blog/rent/#fnref2" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn3" class="footnote-item"><p><code>solana_rent::Rent</code> is a <a href="https://github.com/anza-xyz/solana-sdk/blob/master/rent/src/lib.rs#L28"><em>distinct type</em></a> from <code>pinocchio::sysvars::Rent</code>. <a href="https://soundsonac.id/blog/rent/#fnref3" class="footnote-backref">↩︎</a></p>
</li>
<li id="fn4" class="footnote-item"><p>In my opinion, this is the best way to handle this. <a href="https://soundsonac.id/blog/rent/#fnref4" class="footnote-backref">↩︎</a></p>
</li>
</ol>
</section>
]]>
      </content:encoded>
      <pubDate>Tue, 07 Jul 2026 00:00:00 GMT</pubDate>
    </item>
  </channel>
</rss>