Ich hatte in letzter Zeit viel mit Nachkommastellen in Solidity mit OpenZeppelin zu tun und wollte meine Erkenntnisse in einem kurzen Blogpost zusammenfassen. Ich treffe hierbei einige Annahmen die auf intensivem Testen beruhen oder potentiell unseriösen Quellen -> gerne in den Kommentaren auf Fehler hinweisen!
- OpenZeppelin-Solidity nutzt
- web3js nutzt
- VERALTETEN BigNumber.js fork github:frozeman/bignumber.js-nolookahead sieht man in web3 package.json dependency
- tests nutzen
- const BigNumber = web3.BigNumber (=BigNumber von frozeman)
- const should = chai.use(require(‚chai-bignumber‘)(BigNumber)).should() (=optional anscheinend…)
- migrations nutzen
- gar nichts! Kann aber als require eingebunden werden
- web3js nutzt
Smart Contracts in Solidity die auf der EVM ausgeführt werden haben eine Präzision von 18. Ich meine damit z.B. 0.123456789012345678
Die Zahl an Dezimalstelle 18 wird auf der EVM laut diesem Stackoverflow Beitrag verworfen
Die Präzision von plain Javascript ist 15, daher ist für Tests mit den letzten Nachkommastellen die BigNumber library absolut notwendig! Hierbei auch beachten, dass statt BigNumber(0.123456789012345678) der Parameter als String, also BigNumber(„0.123456789012345678“) übergeben werden muss (sonst letzte 3 Zahlen nicht genau)
Berechnet man eine erwartete Zahl mit BigNumber und vergleicht sie dann, so kann aufgrund des Unterschiedes in der Präzision (BigNumber 20, EVM 18) ein unerwarteter Fehler beim Vergleich/Assert auftreten. Dieser ist schwer zu prüfen weil z.B. der Taschenrechner unter macOS nur 15 Nachkommastellen anzeigt. Hier kann der online Full Precision Calculator mit 200 Nachkommastellen sehr hilfreich sein.
Man muss also die Präzision in BigNumber von maximal 20 auf maximal 18 reduzieren und dabei die letzten 2 Nachkommastellen gegebenenfalls verwerfen. Aber was heißt verwerfen denn BigNumber kennt 9 verschiedene Rundungsarten, also welche nehmen? Die meisten Rundungsarten sind hier mit etwas mehr Text und einer Tabelle aufgeschlüsselt. ROUND_DOWN zeigt das gewünschte Verhalten ABER bitte noch weiter lesen!
Will man nun wirklich auf 18 abschneiden so kann man das so tun:
//note: 17 instead of 18 produces 18 decimals after 0.* const reducedPrecision = number.toPrecision(17, BigNumber.ROUND_DOWN)
Will man mit Kommazahlen multiplizieren so benötigt man die Library ds-math
Probiert man in ds-math die Berechnung 1 / 74 = 0.0135135135135135135 wo ein 5er an der 19. Nachkommastelle kommt, so stellt man fest, dass an dieser Stelle aber anscheinend doch kaufmännisch gerundet wird. Das Ergebnis nach ds-math wdiv(1*1e18, 74) ist nämlich 0.013513513513513514
Also ist ROUND_HALF_UP, also kaufmännisches Runden wohl das Richtige…
Disclaimer: Könnte auch ein Bug in ganache-cli sein. Getestet in Version:
Ganache CLI v6.2.3 (ganache-core: 2.3.1)
UPDATE:
openzeppelin hat die BigNumber Unterstützung aufgelöst und verwendet nun nurnoch web3.utils.BN, also BN.js. Hier sind wie in den Contracts keine Dezimal-Multiplikationen etc. möglich. Falls notwendig einfach von DSMath wmul() Funktion abschauen…
Schreibe einen Kommentar