XRPLF / rippled

Decentralized cryptocurrency blockchain daemon implementing the XRP Ledger protocol in C++
https://xrpl.org
ISC License
4.53k stars 1.47k forks source link

Proposal: Eliminate custom issued token math #4120

Open mDuo13 opened 2 years ago

mDuo13 commented 2 years ago

Summary

Would it be possible to eliminate the custom number format the XRPL uses to represent issued tokens, and migrate to strictly integer math? I think it could be possible and we should really consider it.

Motivation

The XRP Ledger's current fungible token math is entirely custom and covers many edge cases—too many edge cases. The result is more pitfalls for developers than useful features.

Different trust lines can hold the same tokens to operate at different scales depending on the amounts involved. At the extremes, I could hold something in excess of 123×1080 of some token, and you could hold 123×10-80 of the same token from the same issuer. Here, let me write those amounts out without scientific notation to really emphasize the insane scale difference between them:

I have: $12,300,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000,000 You have: $0.0000000000000000000000000000000000000000000000000000000000000000000000000000000123

There is simply no reasonable way for these amounts to interact. As a result, weird edge cases happen. Like, if I send you $1020, my balance doesn't even decrease; new tokens are created via rounding up. User interfaces to the XRPL have the challenge of needing to represent all these amounts to users—admittedly, they probably just resort to scientific notation, but there are reasons people don't typically use scientific notation with money.

This custom number format means that it's challenging to do math off-ledger, like summing up the balance of a set of trust lines or offers. Not all programming languages have easy access to a specialized, arbitrary-precision decimal library, but even if you do, you will probably end up with slightly different results than the ledger itself uses. To replicate the ledger's math exactly, you would have to write custom low-level math functions that match the C++ implementations.

This power isn't really necessary; heck, it's estimated there are only about 1080 atoms in the universe, which is less than the amount of tokens you could issue on each of multiple trust lines. Even the most hyperinflated currencies in history didn't really reach those levels. (Fun fact, the largest bank note ever issued was a Hungarian pengo for 1020.)

Goal State

It should be possible to use standard integer math—not the "almost integer math" the XRPL uses now—for fungible tokens. More importantly, it would be nice if all trust lines for a given token used the same scale. This might require a currency definition object, and the semantics for two-way trust lines might be tricky.

Still, we would like if all on-ledger calculations could be replicated using standard integer math—it's possible arbitrary precision might still be needed for some types of calculations, but ideally the XRPL codebase should not have any custom math implementations, and clients should not need any very specific, custom math libraries either. And many situations could be handled safely in most programming languages using built-in integer types.

I like Interledger's approach: all currency amounts are 64-bit unsigned integers with a "scale" factor as metadata. The scale factor simply tells you where to place the decimal when representing the amount to humans or how to compare one token to another token representing a related asset (e.g. different issuers might choose different scales for USD). 264 is already big enough to represent just about anything. This is almost exactly how XRP is already tracked, with a scale factor of 6. (For some reason, the XRPL's serialization format uses the reverse of the standard convention for notating positive vs. negative amounts, which is another quirk that ideally would be removed.)

If you really need to represent larger amounts you could potentially allow for negative "scale" factors. Which, in a way, is actually kind of similar to how the XRPL's fungible token amounts already work: they have 54 bits allocated for significant digits, a sign bit, and an additional 8 bits to represent the exponent—but the way this is serialized is quirky:

The exponent indicates the scale (what power of 10 the significant digits should be multiplied by) in the range -96 to +80 (inclusive). However, when serializing, we add 97 to the exponent to make it possible to serialize as an unsigned integer. Thus, a serialized value of 1 indicates an exponent of -96, a serialized value of 177 indicates an exponent of 80, and so on.

So, the proposed format can probably represent any number the existing format allows—just, not all at the same time because the scale would be a "per token" rule rather than a "per trust line" rule.

Backwards Compatibility and Migration

Switching over is, of course, the biggest challenge. How do we go from the current number format to the new one cleanly? Since amounts in the current representation are currently stored in the ledger's state data, don't we have to continue supporting them forever? That might be, although I welcome suggestions. But I think it would still be beneficial for new fungible tokens to be able to opt in to a more elegant, standard representation. This might be something that could happen alongside an effort to make trust lines more space-efficient in the ledger; I'm not sure.

A migration would probably involve multiple phases, with one phase introducing an amendment that allows tokens to opt into the new format, or maybe even an automatic "on-demand" conversion of trust lines into the new format when transactions modify them. Eventually, a one-time amendment could correct or remove any numbers that can't be migrated to the new format, if there even are any.

xrplfaucet commented 2 years ago

As a result, weird edge cases happen. Like, if I send you $1020, my balance doesn't even decrease; new tokens are created via rounding up.

To clarify, are you saying that in these situations new tokens are truly being created by the sender (the total circulating supply is increased) without the token issuer's permission? And the sender could repeat this as many times as desired, sending more tokens than they have ever received?

mDuo13 commented 2 years ago

@xrplfaucet To clarify, are you saying that in these situations new tokens are truly being created by the sender (the total circulating supply is increased) without the token issuer's permission? And the sender could repeat this as many times as desired, sending more tokens than they have ever received?

Yes, a sender can create new (dust amounts of) tokens this way. Yes, they can repeat this process numerous times, but not an unlimited amount. The most that can be created in a single transaction is something like N×10-16 where N is the sender's trust line balance. To put that in perspective, to create $0.01 worth of a USD token this way, I would have to hold at least $100,000,000,000,000 (that's $100 trillion). Furthermore, each transaction always destroys some amount of XRP for the transaction cost, so this is not profitable unless N×10-16 is worth more than 10 drops of XRP.

To "create" more of a token than they have ever received, the sender would have to send on the order of 1016 transactions, which even at 10 drops each would burn more XRP than exists in total today. Also, if they sent it to the same recipient, at some point it would stop working because the amount the recipient received would be rounded down to 0 for the same reason as the sender's balance is rounded up.

So there is no need for alarm that this is practically exploitable, but it is a "weird little detail" that can surprise you.

xrplfaucet commented 2 years ago

Thanks for clarifying, and that does put the potential for exploitation into perspective.

Although the opportunity for material gain may be limited and the amounts involved trivial, I would like to argue in support of changing this behaviour, as it in principle breaks non trivial expectations: that issuers alone are able to issue their tokens, and that they are able to limit the token's total supply.

In particular, many projects will 'black hole' their issuing account in order to fix the supply, and demonstrate that it is fixed. In the community this is often seen as a key sign of a project's legitimacy. However, this is in fact not strictly true, as more can be created using the process you describe, possibly accidentally or even by a third party. Even though small amounts, any excess to the promised supply could be very damaging for investor/participant trust. For tokenisation of assets, this could also represent breaking a commitment to 100% asset backing (albeit by minuscule amounts), especially if the backing cannot be increased, say for a unique piece of art.

I may be overreacting, but more broadly this may damage trust in the wider XRPL ecosystem. One of the baseless claims that often comes up from detractors is that more XRP can be generated, and therefore XRP and the XRPL should be avoided. This is of course false, but the above means the same cannot in good faith be said for custom XRPL tokens with a claimed fixed supply.

it is a "weird little detail" that can surprise you.

I have to admit that I experienced this in the wild for my own high balance token (thankfully just a pet/satirical project), with hundreds of thousands of extra tokens unexpectedly created by its faucet. Though I did not know why, or if I was interpreting the transactions correctly until reading this issue (perhaps simply the the fault of my own ignorance, though I doubt I am alone in that ignorance).

RichardAH commented 2 years ago

A hack to prevent amount clobbering is to produce a new errorcode: tecPRECISION_LOSS whenever two amounts that are severely incompatible interact.

Something like the following math could be used, where A and B are two IOU respectively: |(((A+B)-B)/A)-1| + |(((B+A)-A)/B)-1| > 0.0001

Or in lay terms: a sum that produces a rounding error exceeding 0.01% is an error.

It doesn't really fix the root problem but it might lead to more consistency on the ledger. Moving to integer math is a good solution but it is a solution to "how to do it in the first place" not "how to fix what already exists on the ledger."

Another solution is actually to expand the mantissa "on demand." This is sort of tricky, but in the most extreme case where two very incompatible numbers are added together the mantissa would only be ~532 bits (if my math is correct) which is actually pretty ok. Given how wasteful trust lines currently are we could probably even get that space back from other wastage. (This edge case would effectively turn the amounts into integers).

The amounts could be rounded for APIs and backwards compatibility but you'd never be creating or losing dust, it's there even if it's rounded to zero for display purposes.

mDuo13 commented 2 years ago

I'm tentatively supportive of a tecPRECISION_LOSS error code. I'd want to be extra sure it can't be abused to keep objects "stuck" in the ledger (e.g. you can't get rid of your trust line balance or offer somehow) just in case, but my initial thought is it would work.

x-Tokenize commented 2 years ago

I just ran into this issue while testing brokered mode for NFTs and it looks like the creation amount is directly proportional to the the size of the buy offer with respect to the balance of the buyer. Additional IC appears to be created when there is a 1E15 precision difference between the balance of a buyer and their respective buy offer.

Base Case: Minter mints an NFT with transferFee= 50000. Minter has 0 'XYZ' currency Account 1 is funded with 1E-15 'XYZ' currency Account 2 is funded with 1 'XYZ' currency Broker is funded with 1E-15 'XYZ' currency

Account 1 creates a sellOffer for 1E-81 Account 2 creates a buyOffer for 1E-15

Broker Brokers the deal with a broker fee of (1E-15-1E-16)

This sequence of events results in the creation of 1E-15 tokens spread between the Minter, Account1 and Broker. Minter Final 'XYZ' currency Balance: 5E-17 Account 1 Final 'XYZ' currency Balance: 1.9E-15 Account 2 Final 'XYZ' currency Balance: 1 Broker Final 'XYZ' currency Balance: 1.05E-15

While looking for a maximum creation value it appears that the brokered transaction fails once attempting to claim a broker fee of 9.99E15.

Point of Failure Case: Minter mints an NFT with transferFee= 50000. Minter has 0 'XYZ' currency Account 1 is funded with 1E80 'XYZ' currency Account 2 is funded with 1E95 'XYZ' currency Broker is funded with 1E80 'XYZ' currency

Account 1 creates a sellOffer for 1E-81 Account 2 creates a buyOffer for 1E80

Broker Brokers the deal with a broker fee of 9.99E15

This sequence of events results in the creation of 1E80