Possible DEX Rounding Bug in Counterwallet OR Counterparty Itself


I placed an order to sell 511 KARMATOKEN at price 0.00139 XCP per KARMATOKEN.

The highest bid was at 0.00140 XCP per KARMATOKEN and the amount was higher than the 511 I wanted to sell.

The order matched but only for 510.99999286 KARMATOKEN. I am left with 0.00000714 KARMATOKEN unsold.

Is there a bug in Counterwallet ???.

In the GUI I specified numbers of tokens to sell and the price, but CW needs to convert these to implied numbers of tokens to give and take.

I guess the CW makes some simple divisions etc, and somewhere inverts these, and then a rounding error may apply.

If I do the simple math, I should want exactly 0.71029 XCP. See there’s less than eight digits, so no rounding would be needed here.

Since the match is at a higher price, I did receive more XCP, as expected, 0.7154051 XCP.

Possibly the bug is with the Counteparty core code ???

Blockscan claims I am selling 511 KARMATOKEN (0.00000714 KARMATOKEN remaining). Hopefully this is a rounding error on Blockscan’s part. These remaining tokens are not left in the order book. Either the core code fails to match the remaining 0.00000714 tokens (it’s a bug, both the buyer and the seller would want this to match) and it cancels the order for the leftover OR as speculated above, the bug lies within Counterwallet.

Rounding errors can’t exist in CP core code (if it did, it’d be a bug).

If you read the source code, all divisible coins are quoted in “satoshis” (1/100th mil fractions).
So if I sell 1 UNDIVISIBLE, my app will set the quantity to 1.
But if I sell 2.41 DIVISIBLE, I’ll set the quantity to 241,000,000 XCP. That’s how divisible assets are accounted for.

Where rounding must happen is obviously in the UI (which would be better) and at the exchange (to “force” rounding, especially since some apps may not to it right).

Some reference points:

Maybe there’s some rounding in the CW Sell and Buy modals as well.
One problem is if you have 1.23456789 SOMECOINs, I can’t force you to sell 1.2345600 just because it’s convenient for my rounding.
Another is, someone may actually want to buy 1.23456789 SOMECOIN.
And the third is what I mentioned above, if I round it and some other guy doesn’t, I may still end up with umatched or unmatchable amounts which ultimately finish on the DEx.

Maybe it’d be nice if destroy was enabled, or a “send dust to dustbin” feature possible, and then people could check that checkbox to auto-destroy any remaining dust if they don’t want to see it return to them from the DEx. With picopayments it may be cheap enough to simply send those to garbage manually.

By the way, Blockscan doesn’t use the API, so for these issues which require precision, use the API or CoinDaddy’s explorer.

I tried to reproduce on Testnet

Made a similar buy order:

Then an identical sell order

Result: All 511 sold. Bug was not reproduced.

Then it occurred to me:

  1. Counterwallet has the amount and total in GUI. You enter 2 of 3 values price, amount, total and the third is automatically generated. If you enter price and amount or total, you only need to worry about rounding if the number of decimals is eight. (And even then the algorithm may make it always round the correct way, which I don’t know if it does or not)
  2. The bug is likely in Counterparty core! Does the code somewhere divide the buy and sell amounts? Even if these both are integers, the result may not be. If this is needed for further logic, the rounding will lead to mistakes such as what just happened when I was left with KARMATOKEN dust.

Hopefully the solution is simple. Like instead of a normal rounding, it’s a ceiling or something like that.

The buy order;
7396.680638 KARMATOKEN
for 10.35542686 XCP
which implies a price of 0.14000099999991374509… which obviously must be rounded.

Digging into the core code, I find this in counterpartylib/lib/util.py

def price (numerator, denominator):
    """Return price as Fraction or Decimal."""
    if CURRENT_BLOCK_INDEX >= 294500 or config.TESTNET: # Protocol change.
        return fractions.Fraction(numerator, denominator)
  1. Obviously, this has been addressed before since they introduced fractions at some point
  2. Well… since I only sold 510.99999286 of 511 tokens, there is still a bug somewhere

fractions.Fraction(numerator, denominator) seems correct - it’s the same as https://en.wikibooks.org/wiki/Mathematics_with_Python_and_Ruby/Fractions_in_Python#Division example.

I’m not saying there isn’t a bug somewhere, but this particular formula doesn’t look wrong.

To your point about being left with unsold dust: isn’t it inevitable? I’ll take another look at this, but logically speaking at some point you have to round up, either the buyer or the seller.
To make this easier, let’s use indivisible. Let’s say (I’ve replaced an insufficiently simple example here):

  • A wants to sell 1,000,000,001 X for 1 Y
  • B wants to buy 1,000,000,000 X for 1 Y

Now in this situation, what can the code do? Either 1 of your or 1 of his units will remain unmatched and if there’s no other orders (e.g. somebody wants to buy 1 QTY of X), the first guy will end up with “indvisible dust”. The same principle applies to divisible, only in that case you’ll end up with real fractional dust units that have been rounded.

Now whether the matching and rounding is done correctly, that’s another thing, so that has to be checked. The counterparty DB (orders, order_matches) is the ultimate reference for that.

Was it similar or was it the same?
You need to make sure the 9th decimal digit appears in A/B, otherwise there won’t be any rounding.

Comment by Adam (here):

Asset quantities are limited to eight decimal places of precision, but not prices. Prices are stored and compared as Fractions of give and get quantities.

I tried to reproduce a similar issue in Counterwallet on testnet:

Buy 100 TESTC for 300 XCP -> Implied price 1/3 XCP.
1/3 cannot be expressed as a computer decimal. It is 0.333… with an infinite amount of 3’s.

It seems the CW devs already thought of this issue. It is impossible to enter Amount=300 and Total=100.

It appears that you must fill in Price and Amount OR Total. If you fill in amount, then total appears automatically, and vice versa.

If you only fill in amount and total, it won’t let you place an order. Furthermore, if you insert a price with more than 8 decimals, it won’t let you place an order.

The devs have clearly thought of this problem.

However, I’m pretty sure the protocol will let you place an order with exactly 300 and 100. So you can always do that as long as you don’t use Countewallet. And then when the order matches there may be a rounding error, leaving some dust.

If dust is the only problem, it’s probably not worthwhile looking more into it at this time. The dust amounts will always be extremely small, we’re talking like a thousandth of a cent’s worth. In CounterTools I made an option not to display dust and any other wallet can implement the same.

If this bug also can lead to orders not matching, then that’s a lot more serious. Say A puts 300 tokens for sale for 100 XCP. Then B sees the price 0.33333333 in the order book. He puts a buy at this price and they won’t match although both appear to be 0.33333333. I do not know if this will happen or not. Should be tested, but need another tool than Counterwallet.

This page (especially at the very bottom) talks about Python limitations (that most other languages share): https://docs.python.org/3.5/tutorial/floatingpoint.html

Right, any price (in fractions) and quantity (in satoshis) can be set, and if someone wants to buy that exact quantity, it will work fine.

I think B would have to offer 0.33333334 (from CW) or at least the same (long number) that’s higher at the last digit that’s still stored by Python, to match A’s asking price of 0.33333333, because A’s asking price is 0.333333333333333333 (whatever number of digits Python can go to), so in reality more than 0.33333333 (by 0.00000000333333333333).
I also think B could offer 100.00001 XCP and set the price to 0.33333333 in CW to get those 300 tokens `(1/3) * 100.0001) = 0.3333333666666667 - which is more than 0.33333333.
Or maybe he could offer 0.3333333333333333333333333333334 (through the API, for example, if Counterwallet doesn’t allow such numbers) to have his order matched.

These are my intuitive expectations, maybe incorrect (and we shouldn’t ignore the effect of CW, which is a separate layer - I would best not use it for testing, I think using the API to transfer the exact prices is more reliable. This just 4 lines in counterparty-client (1 sell by A, plus 3 buy offers by B, and see if there’s a match).

The Python link at the top is very interesting intro to this and this is an interesting area to debug and/or improve (you can probably spend a good part of the rest of this winter on that :smile:).
I have to focus on several other problems that need investigating, but if I were to dive deeper into this I’d first check how others do it (including open source crypto-exchanges and altcoins that have built-in exchanges), and then compare it with our approach and finally see where it differs and if something should be done about that.

Edit: More on this at https://en.bitcoin.it/wiki/Proper_Money_Handling_(JSON-RPC)