dimesum

Etusivu / Blogi / Raha

Raha

Kuusi rahavirhettä monivaluuttaisessa kulujen jaossa

· 7 min lukuaika ·

Auditointi 2026-08-21 löysi kuusi valuuttavikaa, jotka testit olivat päästäneet läpi vihreinä, ja syyksi paljastui yksi lause dokumentaatiossa, joka oli hiljaa lakannut olemasta tosi.

Yksi vanhentunut lause dokumentaatiossa maksoi kuusi rahavirhettä. Dimesumin monivaluutta-auditointi 2026-08-21 löysi ne koodista, jonka läpi testisarjamme olivat menneet vihreinä, mukaan lukien vaatimussivu joka esitti ¥6,000 velan muodossa -₹60.00. Lause kuului "ryhmät on kiinnitetty INR:ään". Tosi versioon W7 asti, epätosi heti julkaisun jälkeen, ja yhä FOLLOWUPS.md-tiedostossa viikkoa myöhemmin.

Vanhentunut lause on vaarallisempi kuin dokumentaation puute. Myöhempi auditointi uskoo sen ja ohittaa sen kattamat polut, joten väärä rivi aiheuttaa vahinkoa jota hiljaisuus ei koskaan voisi. Tyhjä dokumentti ohjaa sinut lähdekoodiin. Väärä ohjaa sinut aivan muualle.

Toteutunut jatkotoimenpide on pahempi kuin avoin

Dimesum pysäköi lykätyn työn FOLLOWUPS.md-tiedostoon, yksi rivi päätöstä kohti ja mukana syy miksi se pysäköitiin. Merkintä A17 kertoi että ryhmät oli kiinnitetty INR:ään, ettei ulkomaanmatkaa voinut kirjata siinä valuutassa jossa se tapahtui, ja että korjaus odotti perustajan päätöstä. W7 julkaisi monivaluutan silti: kulu voi olla missä tahansa valuutassa, ja saldot on avainnettu muodossa (group_id, member_id, currency) migraatiossa ledger/00005. Kukaan ei poistanut riviä.

Seuraava auditointi luki A17:n, päätteli että alue oli rakentamatta, eikä koskaan avannut tasaus-, vaatimus- tai laskeutumiskoodia. Kolme valuuttasokeaa rahapolkua jatkoi julkaisua yhden lauseen varassa. Mikään vaikea ongelma ei ollut esteenä. Tiedoston app/amount.py docstring kantoi samaa väitettä, joten lukija sai tiedon kahdesti.

Sääntö jonka otimme käyttöön

Poista jatkotoimenpide samassa commitissa joka sulkee sen. Rivi joka kuvaa koodia joka lakkasi toimimasta siten on uudelleenohjaus pois tiedostosta jonka olisit tarvinnut lukea, ja se maksaa enemmän kuin mitään sanominen.

Sama kovakoodattu 100 löytyi kolmesta eri tiedostosta

Raha on täällä int64-alayksiköitä ja ISO 4217 -koodi, eivätkä liukuluvut kosketa sitä koskaan. Kokonaisluvut ovat rakenteeltaan tarkkoja, joten ainoa jäljellä oleva riskialtis laskutoimitus on muunnos pää- ja alayksiköiden välillä. Alayksikkö ei ole aina sadasosa. JPY:llä ei ole sitä lainkaan, KWD:llä on kolme desimaalia, ja jokainen kovakoodattu 100 on veto siitä että käyttäjä pysyi kotona.

Python-sivukomponentin app/amount.py sisälsi ensimmäisen havainnon, poistettuna ennen auditointia miinana eikä aktiivisena vikana. Se kertoi minkä tahansa lauseesta lukemansa luvun 100:lla, joten ¥1,200 kuitista tuli 120,000 alayksikköä. Se näkyy muodossa ¥120,000. Valuutassa jolla ei ole alayksikköä vakio paisuttaa jonkun laskun satakertaiseksi, ja funktio hakee nyt sen sijaan eksponentin pyydetylle valuutalle.

A 1,200 yen receipt converted by a hardcoded multiplier of 100 and by the ISO 4217 exponent table ¥1,200 currency: JPY BEFORE minor = major x 100 constant in the module 120000 minor units reads as ¥120,000 AFTER minor = major x 10^exp JPY exponent = 0 1200 minor units reads as ¥1,200
Eksponenttiansa yhdessä kuvassa. Vakiokerroin 100 on oikein INR:lle ja väärässä sadalla kertoimella JPY:lle jolla ei ole alayksikköä, ja sama vakio heittää kymmenellä KWD:lle jolla on kolme desimaalia.

Auditointi löysi saman vakion sivulta jolla henkilö päättää hyväksyykö hän saldon. formatMinor, joka on osoitteessa /m/{token} olevan vaatimussivun takana, jakoi 100:lla ja liimasi rupiamerkin eteen. ¥6,000 velka tulostui muodossa -₹60.00: väärä merkki, sadasosa summasta. Sen JSON-sisar /v1/claims epäonnistui sen rinnalla, litistäen jäsenen valuuttakohtaiset rivit yhdeksi ja säilyttäen sen valuutan jonka kartta kirjoitti viimeisenä.

Kolmas tiedosto on internal/ingestion/store.go, ja W8 löysi sen laajentaessaan kaksoiskappaleiden tunnistusta, ei auditoinnissa. Sen "plus miinus yksi rupia" -toleranssi oli vakio 100, yksi rupia laskettuna paise-yksikköinä, plus miinus ¥100 kaista vaikka JPY:llä ei ole lainkaan alayksikköä. Kaikki kolme lukevat nyt eksponentin.

Paljaiden kokonaislukujen vertailu sai jenin näyttämään rupialta

Dimesumin kaksoiskappaleiden poistokerros tarkistaa uuden tallennuksen viimeaikaisia kuluja vasten, jottei tuotu kuitti voi veloittaa kahdesti tilausta jonka joku kirjasi käsin. Kyselyllä tuon toleranssikaistan vieressä ei ollut lainkaan valuuttasuodatinta. Niinpä ¥1,000 täsmäsi ₹1,000 kanssa, ja tuonti tarjoutui korvaamaan kulun jonka kanssa sillä ei ollut mitään tekemistä. Ryhmät olivat olleet monivaluuttaisia ledger/00005:stä lähtien, joten tapaus oli saavutettavissa eikä teoreettinen.

Paljaat alayksiköt eivät ole vertailukelpoisia valuuttojen välillä, eivätkä myöskään toleranssit. Projektio suodattaa nyt valuutan mukaan ja johtaa kaistansa vertailtavan valuutan yhdestä pääyksiköstä.

Kaksi moottoria vastasi kysymykseen "kuka maksaa kenelle", ja jälkimmäinen oli valuuttasokea

Kallis vika on rakenteellinen, ei laskennallinen. Tasauksen kirjoituspolku suunnitteli internal/platform/simplify:n varassa, toisen min-cash-flow-moottorin jonka Balance-rakenteessa ei ollut valuuttakenttää. Valuuttakohtainen nollasumma tekee myös tasaisesta summasta nollan, joten ¥300,000 velka ja ₹500 velka kumosivat toisensa tyhjäksi eikä mikään vartija hylännyt suunnitelmaa. Suunnitelma yhdisti sitten jenivelkojan rupiavelalliseen.

Kirjoitus pahensi tilannetta. Palvelu leimasi Currency: g.DefaultCurrency jokaiseen tasaukseen, joten ¥300,000 velka valtuutti INR-kirjauksen, eikä jeniä voinut selvittää lainkaan.

The same four balances routed by the currency-blind simplify engine and by the currency-aware settle engine BALANCES Asha +¥300,000 Bhavna -¥300,000 Chetan +₹500 Dev -₹500 simplify: Balance{MemberID, Minor} no currency on the struct, so all four net flat 300000 + (-300000) + 500 + (-500) = 0 plan: Bhavna pays Chetan a yen debtor sent to a rupee creditor settle: Balance{MemberID, money.Amount} partition by currency, then route inside each JPY: Bhavna pays Asha ¥300,000 INR: Dev pays Chetan ₹500 the settlement states the currency it clears simplify is deleted, not fixed
Neljä saldoa, kaksi moottoria. Valuuttakohtainen nollasumma tuottaa myös tasaisen summan nolla, joten valuuttasokea moottori näkee tasapainoisen ryhmän ja ohjaa luottavaisesti maksun kahden ihmisen välillä jotka eivät ole toisilleen velkaa.

simplify on poistettu ja /settle-plan lopetettu. internal/platform/settle on nyt ainoa moottori: se jakaa valuutat muunnettaviin ja muuntamattomiin, muuntaa nettosaldot kerran, ja ohjaa jokaisen muuntamattoman valuutan omassa nimellisarvossaan. Tasaus ilmoittaa valuutan jonka se selvittää, ja tuo kenttä on pakollinen heti kun ryhmässä on useampi kuin yksi.

Nollaraja tulkittiin rajattomuudeksi

Ylimaksuvartija hylkää maksun joka on suurempi kuin velka jonka se tasaa. Vartija luki outstanding > 0 && amount > outstanding, joten nollaraja ohitti vertailun kokonaan. Joku joka kirjaa maksun velkaa vastaan jota ei ole olemassa on juuri se tapaus jota varten sääntö on olemassa, ja se oli tapaus joka meni läpi.

Korjaus on tyyppi, ei ehto. OutstandingMinor on nyt *int64, joten "kukaan ei laskenut tätä" ei voi kirjoittua samoin kuin "vastaus on nolla". Nil ohittaa tarkistuksen ja tarkoittaa aidosti tuntematonta, ja mikä tahansa kutsuja jolla on pääsy pääkirjaan välittää oikean luvun.

Miksi vihreä testisarja ei todistanut mitään

Jokainen kiinnike näissä poluissa käytti INR:ää. Valuuttasokea vertailu on näkymätön yhden valuutan testissä, koska yhdellä valuutalla ei ole mitään sekoitettavaa. Testisarjat eivät olleet heikkoja, ne olivat kapeita, ja auditointi joka olisi laajentanut niitä oli lähetetty pois A17:n toimesta.

Päästä päähän -pääkirjan tarkistin jakoi saman sokeuden, ja se on osa jonka arvo kannattaa säilyttää. Tarkistin summasi kirjaukset jäsentä kohti ryhmittelemättä valuutan mukaan, joten se huusi susi terveestä kahden valuutan ryhmästä ja summasi nollaan ryhmän yli joka oli rikki kahdesti. Väärä negatiivinen on vaarallinen suunta. Tarkistin joka on rakennettu samalle oletukselle kuin koodi on aina koodin kanssa samaa mieltä.

Kaikki viat jotka 2026-08-21 monivaluutta-auditointi löysi, mitä jeniryhmä sai, ja mitä julkaistiin.
VikaMissäMitä JPY-ryhmä saiKorjaus
Balance-rakenne ilman valuuttaaplatform/simplifyjenivelkoja yhdistettynä rupiavelalliseensimplify poistettu; settle jakaa valuutan mukaan
Ryhmän oletus leimattuna jokaiseen tasaukseentasauspalvelu¥300,000 velka valtuutti INR-kirjauksentasaus ilmoittaa valuutan jonka se selvittää
outstanding > 0 ylimaksuvartijassatasauspalvelunollarajasta tuli ei rajaa lainkaan*int64: nil tarkoittaa tuntematonta, nolla tarkoittaa nollaa
Jako 100:lla rupiamerkin kanssayhdyskäytävä formatMinor¥6,000 velka tulostui muodossa -₹60.00merkki ja desimaalit valuutasta
Valuuttakohtaiset saldot litistetty yhdeksi riviksi/v1/claimsse valuutta jonka kartta kirjoitti viimeisenäyksi merkintä valuuttaa kohti, vastaten GET /balances
Kirjaukset summattu jäsentä kohti, valuutta pudotettupäästä päähän -pääkirjan tarkistinkahdesti rikkoutunut ryhmä raportoitu tasapainoisenaryhmittele jäsenen ja valuutan mukaan

Etsi rahakoodistasi literaali 100

Etsi siitä tuo vakio, ja jokainen vertailu joka asettaa kaksi summaa vierekkäin ilman valuuttaa niiden vieressä. Korjaa sitten halvempi asia, se joka estää seuraavat kuusi: poista jatkotoimenpide samassa commitissa joka sulkee sen. Ja poista kaksoismoottori sen sijaan että korjaisit sen. Kaksi vastausta kysymykseen "kuka maksaa kenelle" on tapa jolla toinen niistä pysyy väärässä.

Usein kysytyt kysymykset

Mitä on monivaluuttainen kulujen jako?

Monivaluuttainen kulujen jako kirjaa jokaisen kulun siinä valuutassa jossa se tapahtui ja pitää erillisen saldon valuuttaa kohti sen sijaan että muuntaisi kaiken yhdeksi. Dimesum avainnaa saldot ryhmän, jäsenen ja valuutan mukaan, joten jenivelka ja rupiavelka eivät koskaan sulaudu yhdeksi luvuksi. Muunnos on näkymä jota käytetään tasaussuunnitelmaan, ei koskaan tallennettu summa.

Miksi kovakoodattu 100 on vaarallinen rahakoodissa?

Kovakoodattu 100 olettaa että jokaisella valuutalla on kaksi desimaalia, ja useilla ei ole. JPY:llä ei ole alayksikköä, joten 100:lla kertominen muuttaa ¥1,200 kuitin 120,000 alayksiköksi, satakertaiseksi paisumiseksi eikä pyöristysvirheeksi. KWD:llä on kolme desimaalia, joten sama vakio heittää kymmenellä. Lue sen sijaan ISO 4217 -eksponentti.

Miten estän kahta tasausmoottoria olemasta eri mieltä?

Poista toinen kahdesta moottorista sen sijaan että sovittaisit niitä yhteen, koska toinen vastaus siihen kuka maksaa kenelle on tapa jolla ensimmäinen pysyy väärässä. Dimesum ajoi simplify:n ja settle:n rinnakkain kunnes auditointi löysi ettei ensimmäisellä ollut valuuttaa saldotyypissään, mikä salli suunnitelman yhdistää jenivelkojan rupiavelalliseen. simplify poistettiin ja /settle-plan lopetettiin sen sijaan että sitä olisi paikattu.

Miksi testit pysyivät vihreinä kuudesta rahavirheestä huolimatta?

Testisarja pysyi vihreänä koska jokainen kiinnike koskevissa poluissa käytti yhtä valuuttaa, eikä valuuttasokea vertailu voi epäonnistua kun valuuttoja on vain yksi. Päästä päähän -pääkirjan tarkistin piti samaa oletusta: se summasi kirjaukset jäsentä kohti ryhmittelemättä valuutan mukaan, joten se raportoi kahdesti rikkoutuneen ryhmän tasapainoisena. Tarkistin joka on rakennettu koodin omalle oletukselle on koodin kanssa samaa mieltä.

Mitä jatkotoimenpiteelle pitäisi tehdä ominaisuuden julkaisun jälkeen?

Poista jatkotoimenpidemerkintä samassa commitissa joka sulkee sen kuvaaman työn. Jatkotoimenpide joka on hiljaa toteutunut on pahempi kuin avoin, koska se ohjaa myöhemmän lukijan pois koodista jota se ennen kuvasi. Dimesum jätti merkinnän jonka mukaan ryhmät oli kiinnitetty INR:ään viikoksi monivaluutan julkaisun jälkeen, ja seuraava auditointi ohitti kolme rahapolkua sen sanan varassa.