<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Bogdan Umaniuk</title>
    <description>The latest articles on DEV Community by Bogdan Umaniuk (@umaniukbogdan).</description>
    <link>https://dev.to/umaniukbogdan</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4209034%2F50beb459-c3af-4521-a8fe-d8464e1d8170.jpg</url>
      <title>DEV Community: Bogdan Umaniuk</title>
      <link>https://dev.to/umaniukbogdan</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/umaniukbogdan"/>
    <language>en</language>
    <item>
      <title>Why 0.1 + 0.2 breaks wallets: money math in JavaScript and every other language</title>
      <dc:creator>Bogdan Umaniuk</dc:creator>
      <pubDate>Mon, 26 Oct 2026 15:49:19 +0000</pubDate>
      <link>https://dev.to/umaniukbogdan/why-01-02-breaks-wallets-money-math-in-javascript-and-every-other-language-25lk</link>
      <guid>https://dev.to/umaniukbogdan/why-01-02-breaks-wallets-money-math-in-javascript-and-every-other-language-25lk</guid>
      <description>&lt;p&gt;I work on the mobile app of a crypto exchange, in React Native and native Android. Most of the money bugs I've run into came from small rounding decisions in the client that looked harmless in code review and broke on real balances. The most common one involves the Max button.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Max button that says "insufficient funds"
&lt;/h2&gt;

&lt;p&gt;A user has &lt;code&gt;10.4567&lt;/code&gt; USDT. The screen shows two decimals, so somebody writes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;shown&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;balance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// "10.46"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user taps Max, the field fills with &lt;code&gt;10.46&lt;/code&gt;, and the server answers "insufficient funds". They only have &lt;code&gt;10.4567&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;toFixed&lt;/code&gt; rounds to the nearest value, and &lt;code&gt;.4567&lt;/code&gt; is closer to &lt;code&gt;.46&lt;/code&gt; than to &lt;code&gt;.45&lt;/code&gt;. For anything a user can spend, that's the wrong direction. You need to round down:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;BigNumber&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;bignumber.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;

&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BigNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;10.4567&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;decimalPlaces&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BigNumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ROUND_DOWN&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// "10.45"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Even with the right rounding direction, the number you round is often already off, because of how floats store decimals.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why 0.1 + 0.2 isn't 0.3
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;   &lt;span class="c1"&gt;// 0.30000000000000004&lt;/span&gt;
&lt;span class="mf"&gt;0.07&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;  &lt;span class="c1"&gt;// 7.000000000000001&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Computers store numbers in base 2. In base 2 you can only write fractions exactly when the denominator is a power of two: &lt;code&gt;0.5&lt;/code&gt;, &lt;code&gt;0.25&lt;/code&gt;, &lt;code&gt;0.125&lt;/code&gt;. Values like &lt;code&gt;0.1&lt;/code&gt;, &lt;code&gt;0.07&lt;/code&gt; or &lt;code&gt;0.001&lt;/code&gt; become infinite fractions, the same way &lt;code&gt;1/3&lt;/code&gt; is &lt;code&gt;0.3333...&lt;/code&gt; in base 10. The machine cuts them off at 53 bits and keeps the nearest value it can store.&lt;/p&gt;

&lt;p&gt;This is what actually sits in memory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0.001 → 0.001000000000000000020816682...
0.07  → 0.07000000000000000666133815...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;JavaScript prints &lt;code&gt;0.07&lt;/code&gt; because that's the shortest decimal that maps back to the same stored value. The tail is still there. Multiply by 100 and the tail grows to &lt;code&gt;7.000000000000000666...&lt;/code&gt;. Near 7, a double can only represent values about &lt;code&gt;0.00000000000000089&lt;/code&gt; apart, and the nearest one to that result is &lt;code&gt;7.000000000000000888...&lt;/code&gt;. That gets printed as &lt;code&gt;7.000000000000001&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;In money code that tail shows up in a few predictable places.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five ways floats turn into money bugs
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Converting to cents loses a cent
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="mf"&gt;1.13&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;              &lt;span class="c1"&gt;// 112.99999999999999&lt;/span&gt;
&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.13&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// 112&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The user typed &lt;code&gt;1.13&lt;/code&gt; and the app sent &lt;code&gt;1.12&lt;/code&gt;. Or a "minimum amount is 1.13" check fails on exactly &lt;code&gt;1.13&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  &lt;code&gt;toFixed&lt;/code&gt; rounds the stored value, not the one you see
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.005&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// "1.00"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'd expect &lt;code&gt;1.01&lt;/code&gt;. But &lt;code&gt;1.005&lt;/code&gt; is stored as &lt;code&gt;1.00499999...&lt;/code&gt;, and &lt;code&gt;toFixed&lt;/code&gt; rounds what's stored. The app shows one value and the backend, which uses decimals, calculates another.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sums drift
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="o"&gt;++&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;
&lt;span class="nx"&gt;total&lt;/span&gt;        &lt;span class="c1"&gt;// 0.9999999999999999&lt;/span&gt;
&lt;span class="nx"&gt;total&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten fees of &lt;code&gt;0.1&lt;/code&gt; don't add up to &lt;code&gt;1&lt;/code&gt;. A transaction history total stops matching the balance, and any check like "parts must equal the whole" fails.&lt;/p&gt;

&lt;h3&gt;
  
  
  Crypto amounts don't fit at all
&lt;/h3&gt;

&lt;p&gt;ETH has 18 decimals. One ether is &lt;code&gt;10^18&lt;/code&gt; wei, which is already past the range where a double can hold whole numbers exactly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isSafeInteger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;e18&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;           &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.123456789012345678&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// 1.1234567890123457&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;parseFloat&lt;/code&gt; drops the last digits without any error. For token amounts that means the number you display, or worse, the number you send, is quietly different from the real one.&lt;/p&gt;

&lt;h3&gt;
  
  
  JSON drops precision in transit
&lt;/h3&gt;

&lt;p&gt;If an API returns &lt;code&gt;{ "amount": 0.1234567890123456789 }&lt;/code&gt;, the JSON parser on the client usually turns it into a double and cuts it. The backend kept the precision and the wire threw it away. This is why financial APIs send amounts as strings.&lt;/p&gt;

&lt;h2&gt;
  
  
  A real one: a Max button that offers more than you have
&lt;/h2&gt;

&lt;p&gt;This one came from a P2P trading screen in an app I work on. The user sells crypto for fiat. The Max button worked like this:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Take the crypto balance, multiply it by the price, round to 2 decimals. That's the max fiat amount.&lt;/li&gt;
&lt;li&gt;Take that fiat amount, divide it by the price, round to the crypto precision. That's the crypto amount.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The code already used &lt;code&gt;BigNumber&lt;/code&gt;. The problem was its default rounding mode, &lt;code&gt;ROUND_HALF_UP&lt;/code&gt;: step 1 sometimes rounds the fiat amount up, and step 2 converts that inflated amount back into crypto.&lt;/p&gt;

&lt;p&gt;With a balance of &lt;code&gt;896.117400&lt;/code&gt; and a price of &lt;code&gt;86.16&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;896.1174 × 86.16  = 77209.475184
rounded           = 77209.48     (up)
77209.48 / 86.16  = 896.117456   (more than the balance)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I ran it on 20,000 random balances and prices:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Crypto precision&lt;/th&gt;
&lt;th&gt;Max is above the balance&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;2 decimals&lt;/td&gt;
&lt;td&gt;0%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4 decimals&lt;/td&gt;
&lt;td&gt;~6%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6 decimals&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;8 decimals&lt;/td&gt;
&lt;td&gt;~50%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For coins with 6 or 8 decimals, the button picked a number larger than the balance about half the time. The form only checked that the crypto amount was greater than zero, so nothing on the client caught it. The amount was then sent with &lt;code&gt;Number(values.amount)&lt;/code&gt;, which turns it into a double right before the request. What happens next depends on the backend. Most likely the user gets "insufficient funds" when they submit the deal.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which way to round
&lt;/h2&gt;

&lt;p&gt;For money, rounding should always land on the side that can't create a deficit.&lt;/p&gt;

&lt;p&gt;Anything the user can spend or receive rounds down: available balance, Max, the payout amount. Anything the user pays rounds up: fees, the amount to charge.&lt;/p&gt;

&lt;p&gt;Fees are where the second rule matters. Take a 0.1% fee and a currency with 2 decimals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;transfer 0.5 USDT → fee = 0.0005
round down → 0.00
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That transfer is free. Split 1000 USDT into 2000 transfers of 0.5 and nobody pays anything. This is a known abuse pattern, and even without bad intent, losing a cent on millions of operations adds up.&lt;/p&gt;

&lt;p&gt;Rounding up has its own problem: the fee on &lt;code&gt;0.5&lt;/code&gt; becomes &lt;code&gt;0.01&lt;/code&gt;, which is 2%. That's why exchanges also set a minimum operation amount or a minimum fee.&lt;/p&gt;

&lt;h3&gt;
  
  
  Max depends on where the fee comes from
&lt;/h3&gt;

&lt;p&gt;Same balance, &lt;code&gt;10.4567&lt;/code&gt;, 2 decimals, 0.1% fee.&lt;/p&gt;

&lt;p&gt;If the fee is taken out of the amount, Max is the balance rounded down, &lt;code&gt;10.45&lt;/code&gt;. The fee is &lt;code&gt;0.01045&lt;/code&gt;, rounded up to &lt;code&gt;0.02&lt;/code&gt;, and the recipient gets &lt;code&gt;10.43&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If the fee is charged on top, Max has to satisfy &lt;code&gt;amount + fee ≤ balance&lt;/code&gt;:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Amount&lt;/th&gt;
&lt;th&gt;Fee (2 decimals, up)&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;th&gt;Fits in 10.4567?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10.45&lt;/td&gt;
&lt;td&gt;0.02&lt;/td&gt;
&lt;td&gt;10.47&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10.44&lt;/td&gt;
&lt;td&gt;0.02&lt;/td&gt;
&lt;td&gt;10.46&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10.43&lt;/td&gt;
&lt;td&gt;0.02&lt;/td&gt;
&lt;td&gt;10.45&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If the fee is kept with 8 decimals, as exchanges usually do:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Amount&lt;/th&gt;
&lt;th&gt;Fee (8 decimals)&lt;/th&gt;
&lt;th&gt;Total&lt;/th&gt;
&lt;th&gt;Fits?&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;10.45&lt;/td&gt;
&lt;td&gt;0.01045&lt;/td&gt;
&lt;td&gt;10.46045&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;10.44&lt;/td&gt;
&lt;td&gt;0.01044&lt;/td&gt;
&lt;td&gt;10.45044&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So the same balance gives four different Max values: &lt;code&gt;10.46&lt;/code&gt; (the bug), &lt;code&gt;10.45&lt;/code&gt;, &lt;code&gt;10.44&lt;/code&gt; or &lt;code&gt;10.43&lt;/code&gt;. Which one is right depends on rules the client doesn't own. The backend defines the fee model, the precision of amounts and the precision of fees. The client either mirrors those rules exactly or asks the backend for the max amount directly. I prefer the second option.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rounding up on a double has its own bug
&lt;/h2&gt;

&lt;p&gt;Rounding fees up sounds simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ceil2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the fee on &lt;code&gt;70&lt;/code&gt; at 0.1% should be exactly &lt;code&gt;0.07&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="mi"&gt;70&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mf"&gt;0.001&lt;/span&gt;        &lt;span class="c1"&gt;// 0.07&lt;/span&gt;
&lt;span class="mf"&gt;0.07&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;        &lt;span class="c1"&gt;// 7.000000000000001&lt;/span&gt;
&lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;7.000000000000001&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="c1"&gt;// 0.08&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Amount&lt;/th&gt;
&lt;th&gt;Expected fee&lt;/th&gt;
&lt;th&gt;
&lt;code&gt;Math.ceil&lt;/code&gt; on a double&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;70&lt;/td&gt;
&lt;td&gt;0.07&lt;/td&gt;
&lt;td&gt;0.08&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;140&lt;/td&gt;
&lt;td&gt;0.14&lt;/td&gt;
&lt;td&gt;0.15&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;280&lt;/td&gt;
&lt;td&gt;0.28&lt;/td&gt;
&lt;td&gt;0.29&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1100&lt;/td&gt;
&lt;td&gt;1.10&lt;/td&gt;
&lt;td&gt;1.11&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1300&lt;/td&gt;
&lt;td&gt;1.30&lt;/td&gt;
&lt;td&gt;1.30&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The last row is fine because &lt;code&gt;1.3 * 100&lt;/code&gt; happens to land exactly on &lt;code&gt;130&lt;/code&gt;. So the bug depends on the amount, which makes it hard to reproduce. &lt;code&gt;Math.ceil&lt;/code&gt; does nothing wrong here; it rounds the tail it was given. The fix is to never put money into a double:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
  &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BigNumber&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;times&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;decimalPlaces&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;BigNumber&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ROUND_UP&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toFixed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;fee&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;70&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// "0.07"&lt;/span&gt;
&lt;span class="nf"&gt;fee&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;70.01&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;// "0.08"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  It's a double problem, in every language
&lt;/h2&gt;

&lt;p&gt;JavaScript's &lt;code&gt;number&lt;/code&gt; is an IEEE 754 double, the same format as the default float in almost every mainstream language:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Python
&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;    &lt;span class="c1"&gt;# 0.30000000000000004
&lt;/span&gt;&lt;span class="mf"&gt;0.07&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;   &lt;span class="c1"&gt;# 7.000000000000001
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight ruby"&gt;&lt;code&gt;&lt;span class="c1"&gt;# Ruby&lt;/span&gt;
&lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;    &lt;span class="c1"&gt;# 0.30000000000000004&lt;/span&gt;
&lt;span class="mf"&gt;0.07&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;   &lt;span class="c1"&gt;# 7.000000000000001&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Swift&lt;/span&gt;
&lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.2&lt;/span&gt;    &lt;span class="c1"&gt;// 0.30000000000000004&lt;/span&gt;
&lt;span class="mf"&gt;0.07&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;   &lt;span class="c1"&gt;// 7.000000000000001&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Java and Kotlin &lt;code&gt;Double&lt;/code&gt;, Go &lt;code&gt;float64&lt;/code&gt; and C# &lt;code&gt;double&lt;/code&gt; use the same format. A backend in any of them will charge &lt;code&gt;0.08&lt;/code&gt; instead of &lt;code&gt;0.07&lt;/code&gt; if it rounds fees up on a double.&lt;/p&gt;

&lt;p&gt;Mature financial backends avoid this by doing money math in decimal types:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Language or layer&lt;/th&gt;
&lt;th&gt;Type for money&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Java / Kotlin&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BigDecimal&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Ruby&lt;/td&gt;
&lt;td&gt;&lt;code&gt;BigDecimal&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Python&lt;/td&gt;
&lt;td&gt;&lt;code&gt;decimal.Decimal&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;C#&lt;/td&gt;
&lt;td&gt;&lt;code&gt;decimal&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Go&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;shopspring/decimal&lt;/code&gt;, or integers in minor units&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SQL&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;NUMERIC&lt;/code&gt; / &lt;code&gt;DECIMAL&lt;/code&gt; columns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;JavaScript / TypeScript&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;bignumber.js&lt;/code&gt;, &lt;code&gt;decimal.js&lt;/code&gt;, &lt;code&gt;big.js&lt;/code&gt;, or &lt;code&gt;BigInt&lt;/code&gt; in minor units&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Java's &lt;code&gt;BigDecimal&lt;/code&gt; is the standard tool for money in the language a lot of banking software is written in. So the server is often the safest part of the system. Precision usually gets lost at the edges: when the client does its own math, when amounts travel as JSON numbers, and when a form value goes through &lt;code&gt;Number()&lt;/code&gt; right before a request.&lt;/p&gt;

&lt;h2&gt;
  
  
  BigNumber or BigInt?
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Who uses decimal libraries
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;bignumber.js&lt;/code&gt;, &lt;code&gt;decimal.js&lt;/code&gt; and &lt;code&gt;big.js&lt;/code&gt; were all written by the same developer, Michael Mclaughlin. In the week of October 16–22, 2026, npm counted about 65 million downloads for &lt;code&gt;decimal.js&lt;/code&gt;, 42 million for &lt;code&gt;bignumber.js&lt;/code&gt; and 20 million for &lt;code&gt;big.js&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Several crypto SDKs depend on them. &lt;code&gt;xrpl.js&lt;/code&gt;, the XRP Ledger SDK, and &lt;code&gt;@stellar/stellar-base&lt;/code&gt; depend on &lt;code&gt;bignumber.js&lt;/code&gt;. Aave's &lt;code&gt;@aave/math-utils&lt;/code&gt; expects it as a peer dependency, and &lt;code&gt;@0x/utils&lt;/code&gt; ships with it. Uniswap's &lt;code&gt;@uniswap/sdk-core&lt;/code&gt; uses &lt;code&gt;big.js&lt;/code&gt; together with &lt;code&gt;JSBI&lt;/code&gt;. On the banking side, the same job is done by &lt;code&gt;BigDecimal&lt;/code&gt; in Java and Kotlin backends, and fintech front ends commonly reach for one of these libraries once rates or percentages are involved.&lt;/p&gt;

&lt;p&gt;They're used because they do decimal arithmetic with a precision and a rounding mode you set yourself. Fees and exchange rates need division, and division needs a rounding rule.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why BigInt alone doesn't fit
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;BigInt&lt;/code&gt; is built into the language and handles arbitrarily large integers exactly, but only integers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.07&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;    &lt;span class="c1"&gt;// RangeError: not an integer&lt;/span&gt;
&lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0.07&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// SyntaxError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To use it for money you have to work in minor units: &lt;code&gt;70.00&lt;/code&gt; becomes &lt;code&gt;7000n&lt;/code&gt; cents, &lt;code&gt;1.5 ETH&lt;/code&gt; becomes &lt;code&gt;1500000000000000000n&lt;/code&gt; wei. That's a valid approach, and it brings some work with it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;                     &lt;span class="c1"&gt;// 3n, division truncates, there's no rounding mode&lt;/span&gt;
&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;                      &lt;span class="c1"&gt;// TypeError: Cannot mix BigInt and other types&lt;/span&gt;
&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="c1"&gt;// TypeError: Do not know how to serialize a BigInt&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You track the scale of every currency yourself (2 decimals for USD, 8 for BTC, 18 for ETH), write your own rounding, convert percentages and rates into fixed-point integers, and handle serialization. The conversion into minor units also has to start from a string. &lt;code&gt;BigInt(Math.round(0.07 * 100))&lt;/code&gt; puts a float right back into the chain.&lt;/p&gt;

&lt;p&gt;A minimal version looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// "70.00" -&amp;gt; 7000n, parsed from the string, never from a float&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;toMinor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;decimals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;frac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;frac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;decimals&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;too many decimals&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;BigInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;int&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;frac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padEnd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;decimals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;0&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// division that rounds up&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ceilDiv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;toMinor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;70.00&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;          &lt;span class="c1"&gt;// 7000n&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;fee&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;ceilDiv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;amount&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;// 0.1% = 10 basis points -&amp;gt; 7n, i.e. 0.07&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;BigInt&lt;/code&gt; is a good fit for on-chain values, which are integers by definition. ethers v6 dropped its own &lt;code&gt;BigNumber&lt;/code&gt; class in favor of native &lt;code&gt;BigInt&lt;/code&gt; for wei amounts, which are whole numbers. Once you show those amounts to a person, convert currencies or apply a fee rate, you're back to decimals with rounding rules.&lt;/p&gt;

&lt;p&gt;In practice most teams combine the two. Amounts are stored and sent as strings or integers in minor units, and calculations go through a decimal library with explicit rounding.&lt;/p&gt;

&lt;h2&gt;
  
  
  Checklist
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Money is never a &lt;code&gt;number&lt;/code&gt; (or &lt;code&gt;float&lt;/code&gt;, or &lt;code&gt;double&lt;/code&gt;). Use a decimal type or integers in minor units.&lt;/li&gt;
&lt;li&gt;Amounts travel as strings in JSON, in both directions.&lt;/li&gt;
&lt;li&gt;Never call &lt;code&gt;Number()&lt;/code&gt; or &lt;code&gt;parseFloat()&lt;/code&gt; on an amount right before a request.&lt;/li&gt;
&lt;li&gt;Always pass a rounding mode. &lt;code&gt;bignumber.js&lt;/code&gt; defaults to &lt;code&gt;ROUND_HALF_UP&lt;/code&gt;, which is wrong for both balances and fees.&lt;/li&gt;
&lt;li&gt;Round spendable amounts down and fees up.&lt;/li&gt;
&lt;li&gt;Take the precision of amounts and fees from the backend, per currency. Don't hardcode &lt;code&gt;toFixed(2)&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;For Max, ask the backend or mirror its fee model exactly, and validate &lt;code&gt;amount + fee ≤ balance&lt;/code&gt; on the client too.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The &lt;code&gt;10.4567&lt;/code&gt; from the first example is a good test case. If your Max button shows &lt;code&gt;10.46&lt;/code&gt; for it, you have the bug.&lt;/p&gt;




&lt;p&gt;New posts go to my Telegram channel first: &lt;a href="https://t.me/umaniuk_dev" rel="noopener noreferrer"&gt;t.me/umaniuk_dev&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Questions, or a senior mobile role you think I'd fit? Message me on Telegram: &lt;a href="https://t.me/umaniukbogdan" rel="noopener noreferrer"&gt;@umaniukbogdan&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>reactnative</category>
      <category>fintech</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
