Round error issue - produce money for free on itBit bitcoin exchange
hackerone.com
hackerone.com
Audits matter. Your books should always sum zero. Time and time again, we've seen bugs like this across the space: there was the infamous Bitcoinica "infinite leverage" bug [1], a rounding error in another's order sizes, and it's theorized that a similar bug was leveraged in the downfall of MtGox to create virtually infinite balances and use it to buy Bitcoin up to $1000 in 2013.
Whether it's in your engine or outside of it, whether it's a Bitcoin product or any other financial product, make sure there is something - somewhere - that is regularly summing up all accounts and ensuring that every cent or satoshi is accounted for.
We run such a system at every execution. If the system doesn't sum zero, we shut down. It's happened three times in two and a half years, each time due to tiny mathematical errors in our liquidation process, each time for less than 10,000 satoshis (~$0.10). We sleep much better at night knowing it's there.
Audits would catch the error caused by float math. And of course, don't do float math.
1. I can't find the original reference to it, but it was originally possible to simply edit the DOM to submit any leverage amount you pleased (such as 100,000x; never trust the client!) and then run your account massively into profit - or bankruptcy.
That's like the industry that epitomizes the usecase for fixed point!
The snippet of what I presume is their API response is interesting:
{"currencyCode":"XBT","balance":100000.00000008,"available":100000.00000008,"onDeposit":0}
The amount fields are JSON numbers. That's fine for integers up to 2^52[1] but can have issues with floating point math. They'd be better off representing everything as satoshis and doing integer math on everything.I bet their entire system is based on floating point math. Tisk tisk ...
[1]: Yes probably not an issue unless your Bill Gates and convert your networth to Zimbabwean dollars...
You will, of course, run into huge trouble if you perform more complex operations. All bets are off once you start doing things like trigonometry or logarithms, unless you implement it yourself.
For Bitcoin applications, I think you'd want to work in integer satoshis.
Basically I think the idea with 4 digits after the decimal is that the smallest denomination you can have is 1 penny, and that allows you to calculate percentages of pennies accurately to a hundredth of a percentage point.
Also note the the way you round usually depends on the context, for some calculations you want to round up, for some down, and for some in yet another way. So you perform explicit rounding with an explicit rounding mode everywhere it is required and don't assume that implicit rounding or the default rounding mode will do the right thing because they won't in general.
EDIT: To expand a bit on the context. If you, for example, charge a percentage fee you can choose between rounding up getting at least the fee you are asking for or rounding down not charging the customer more than you are asking for. Or you can try to be less biased and round either up or down depending on the fractional part. This may still require tie-breaking, either in a direction of your choice, or randomized, or by some rule like round half to even. Different choices will obviously have different biases.
While this may almost seem somewhat pedantic, such things have been exploited, see for example »Computer Capers: Tales of Electronic Thievery, Embezzlement & Fraud«. [1][2] Also note that there may be even legal regulations prescribing how you can and can not round, for example between the Euro and the replaced national currencies. [3] I also have some faint memories of a case against someone systematically exploiting rounding errors but I can not come up with a reference and may even misremember this.
[1] http://www.snopes.com/business/bank/salami.asp
[2] https://www.amazon.com/Computer-Capers-Electronic-Thievery-E...
Someone else in the comments mentioned arbitrary precision arithmetics, but even those don't solve all your problems magically. Not only are they comparatively slow and more cumbersome to deal with due to their variable length nature, but they still fail to correctly deal with one third, so rounding will still be required unless you manage to avoid all the bad numbers.
You may then be tempted to switch to a rational number type with arbitrary precision integers as numerator and denominator. But while you now solved the problem with the one third, you still have to approximate irrational numbers and transcendental functions, for example Euler's constant, which is not unlikely to appear in interest calculations. So you still have to think about rounding, even if possibly in less places.
fee = accountX * 0.01
accountY = accountY + fee
accountX = accountX - fee
vs accountY = accountY + accountX * 0.01
accountX = accountX * 0.99
I've seen a fair bit of (non financial) accounting go wrong like this, where fractions just slip through the cracks.No floats. Not even once.
When doing math in finance, we need to be explicit about the precision we use, because we're counting finite things. Things like significant figures and rounding matter, and can't be ignored by imagining we have an "infinite precision" that throws away information, as floating point math does.
Using fixed-point decimal math as default helps mitigate this class of mistakes in this domain. It's similar to using a memory safe language outside embedded programming- if there's no value in the other option, and it's a source of bugs, eliminate it.
NB: Using fixed-point decimal math is similar to the advice to use integers, though it's a bit more flexible (eg keeping amounts in "BTC" with a fixed 8 decimal places instead of "satoshis").
Do you mean no decimal floating point numbers or did you misread my comment as advocating binary floating point numbers plus careful rounding?
Fixed point decimal numbers are of course also okay, it just changes what you have to pay attention to. And often fixed point decimal numbers are in some sense secretly floating point decimal numbers, they just do not automatically adjust the decimal point [1] but require doing so explicitly.
And this may of course be useful because you will run into an overflow and not accidentally lose precision when numbers become large. On the other hand you may lose precision if numbers become small and you only have a fixed precision.
[1] I am not sure what the important factor is for calling a number type fixed point or floating point. Is it the ability to move the decimal point around or is it doing so automatically?
Fixed-point is a way of doing math, as well as a data representation. Think of it as keeping a very large integer, plus another int that represents where the decimal place should go. Hell, you can even think of it as a string of numbers and an optional '.', removing the issue of overflow as well.
When you do fixed-point math, you have to be explicit about rounding and precision changes, or set a default way to handle those situations. When two numbers are added, do we inherit the precision of the more or less precise number? What about division, where we run into necessarily repeating decimals?
Floating point math, on the other hand, uses a mantissa and necessarily loses precision. As a floating point number is an approximation from 0 to infinity using a fixed number of bits (often 32, 64, 128), there are a ton of amounts that it can't exactly represent. But that's not an acceptable situation in finance- we don't need infinity, but we do need countability within a range with some minimum step size.
An example- let's say we're adding amounts of bitcoin in Python
> 1000.1234 + 1000.1234
20000.2468
> 10000.1234 * 3
30000.3702
Looks good, but now we have a whale, a high frequency trader, or some HackerOne bounty hunter with API access > 10000.1234 * 50
500006.17000000004
That calculation, at face value, just created .00000000004 bitcoin out of thin air. That's less than a satoshi, but if you keep going with a strategy like this on an automated platform, you can turn it into real money.Instead, you want to use fixed-point.
> from decimal import Decimal
> Decimal('1000.1234') * Decimal('50')
Decimal('50006.1700')
Note in this case the precision was expanded to the most precise in multiplication, and no money was created out of thin air.You can also set precision explicitly (`Decimal('1000.1234').quantize('0.00000001')`), handle rounding per operation or across calculations, etc.
More generally: represent the percentage as a rational number, then multiply the amount by the numerator and then divide by the denominator.
If you wanted your fees to round up then the actual code would look something like:
fee = round_up_multiply(amount, 1, 300)But the other commenter was discussing arbitrary precision arithmetic libraries, not fixed-point decimals.
And an array of single precision floats is a zero-terminated UTF-8 encoded string. Using an actual fixed point decimal number type is still preferable over going straight to the underlying integer, even if it is only for the easier to read source code with decimal points where they belong instead of having to perform the transformation in your head while reading.
But the other commenter was discussing arbitrary precision arithmetic libraries, not fixed-point decimals.
Fair point.
If your language of choice has a good fixed-point decimal library, or you can switch to one that does, then I agree that would definitely be the superior solution.
I was afraid of that response and thought about using an integers array and ASCII but it is so nicely abstruse, I could not resist.
This isn't a fixed point number system, because you are now using a non-fixed point number representation (e.g. two numbers; which can't be represented in fix point because of their repeating nature) to represent your fraction. Trying to get all of that behavior right is more error prone than just using an arbitrary precision library in the first place.
I ran across this once when I implemented a rational number type with arbitrary precision integers and wanted to print the decimal representation. I wanted to determine the repeating and non-repeating fractional parts but failed to find any applicable math or algorithm.
If I am not misremembering and did not do my research to poorly, this is an open problem and at best you can start expanding and look if you return to a state you were in before. But at some point you have to give up because you can not event determine the length of the repeating and non-repeating fractional parts in advance and even some relatively small numerators and denominators can produce some pretty long decimal representations until they start repeating.
http://speleotrove.com/decimal/ etc.
https://hn.algolia.com/?query=speleotrove&sort=byDate&dateRa...
My order of preference:
* Fixed-point arithmetic: All-around safe, though you'll probably need to take a maintenance window later if you need extra precision.
* Floating point arithmetic: Sounds dangerous, but you can eliminate most of the risk using database checks.
* Arbitrary-precision arithmetic: Almost any place they're used opens you up to DoS attacks; it's very difficult to detect the problem. I'd ensure my database and application server have debugging symbols installed, replace libgmp with something that alerts by generating core dumps when a timing threshold is passed, use a language where backtraces are easy to read in gdb, and ensure that all arbitrary-precision arithmetic is written against gmp. I'm not sure what I'd do when I detected the problem though: If I truncated 1 + 1/(10^10)! to 1 to resolve the issue, it's equivalent to using floating point in the first place.
Only if you store the arbitrary precision number. Last I checked you can't actually have a tenth of a cent, so it has to be rounded at some point. Like when you store it. I was advocating calculation be done with arbitrary precision numbers which do not have this problem.
> Fixed-point arithmetic
This is a decent solution as long as you have enough precision and a strategy for detecting lost cents.
> Floating point arithmetic
Which is non-deterministic based off of optimization level and hardware. Waits for the bank to switch to a new cluster and watch all of their numbers change for the lols. No wonder banks don't know how much money they have.
11:34:40.979 SecurityError: The operation is insecure. 1 frontend.fe083880.js:15
e.exports<.initialize https://hackerone.com/assets/frontend.fe083880.js:15:31757
t.Model https://hackerone.com/assets/vendor.b247925b.js:31:4648
e.exports<.constructor https://hackerone.com/assets/frontend.fe083880.js:2:20109
q/r< https://hackerone.com/assets/vendor.b247925b.js:31:22455
<anonymous> https://hackerone.com/assets/frontend.fe083880.js:19:31012
t https://hackerone.com/assets/vendor.b247925b.js:1:96
n https://hackerone.com/assets/frontend.fe083880.js:26:14283
forEach self-hosted
<anonymous> https://hackerone.com/assets/frontend.fe083880.js:96:23586
t https://hackerone.com/assets/vendor.b247925b.js:1:96
window.webpackJsonp https://hackerone.com/assets/vendor.b247925b.js:1:418
<anonymous> https://hackerone.com/assets/frontend.fe083880.js:1:1
Fail, Hacker One.“Yeah, they did it in Superman III.”
Apparently Fintech startups have no need for the wisdom built up over the course of 500 years of modern banking, because this stuff is Banking 101 for any engineer at a bank.