dimesum

Home / Blog / Denaro

Denaro

Sei bug di denaro nelle spese multivaluta

· 9 min di lettura ·

Un audit del 2026-08-21 ha trovato sei difetti valutari superati dai test, causati da una frase in un documento che aveva silenziosamente smesso di essere vera.

Una frase ormai superata in un documento è costata sei bug di denaro. Un audit multivaluta di Dimesum del 2026-08-21 li ha trovati in codice che le nostre suite di test superavano senza errori, inclusa una pagina di richiesta che mostrava un debito di ¥6,000 come -₹60.00. La frase era "i gruppi sono ancorati a INR". Vera fino alla W7, falsa nel momento in cui è entrata in produzione, e ancora presente in FOLLOWUPS.md una settimana dopo.

Una frase superata è più pericolosa dell'assenza di documentazione. Un audit successivo le crede e salta i percorsi che copre, così una riga sbagliata fa danni che il silenzio non potrebbe mai fare. Un documento vuoto ti rimanda al sorgente. Uno sbagliato ti manda da tutt'altra parte.

Un'annotazione che si è avverata è peggio di una ancora aperta

Dimesum parcheggia il lavoro rinviato in FOLLOWUPS.md, una riga per decisione con il motivo del rinvio. La voce A17 diceva che i gruppi erano ancorati a INR, che un viaggio all'estero non poteva essere registrato nella valuta in cui era avvenuto, e che la correzione attendeva una decisione dei fondatori. La W7 ha comunque rilasciato il multivaluta: una spesa può essere in qualsiasi valuta, e i saldi sono indicizzati su (group_id, member_id, currency) dalla migrazione ledger/00005. Nessuno ha cancellato la riga.

L'audit successivo ha letto A17, ha concluso che l'area non era stata costruita, e non ha mai aperto il codice di liquidazione, delle richieste o della pagina. Tre percorsi di denaro ciechi alla valuta hanno continuato a essere rilasciati sulla base di una sola frase. Nessun problema difficile lo impediva. La docstring di app/amount.py riportava la stessa affermazione, così un lettore se lo sentiva dire due volte.

La regola che abbiamo adottato

Cancella un'annotazione nello stesso commit che la chiude. Una riga che descrive codice che ha smesso di funzionare così è un rimando lontano dal file che dovevi leggere, e costa più che non dire nulla.

Lo stesso 100 hardcoded è comparso in tre file diversi

Il denaro qui è espresso in unità minori int64 più un codice ISO 4217, e i float non lo toccano mai. Gli interi sono esatti per costruzione, quindi l'unica aritmetica rischiosa che resta è la conversione tra unità maggiori e minori. L'unità minore non è sempre un centesimo. JPY non ne ha affatto, KWD ha tre decimali, e ogni 100 hardcoded è una scommessa sul fatto che l'utente sia rimasto a casa.

L'app/amount.py del sidecar Python conteneva il primo avvistamento, rimosso prima dell'audit come una mina più che come un difetto attivo. Moltiplicava per 100 qualunque cifra leggesse in una frase, così una ricevuta da ¥1,200 diventava 120,000 unità minori. Che si legge come ¥120,000. Su una valuta senza unità minore la costante gonfia il conto di qualcuno di cento volte, e ora la funzione consulta invece l'esponente della valuta richiesta.

Una ricevuta da 1,200 yen convertita da un moltiplicatore hardcoded pari a 100 e dalla tabella degli esponenti ISO 4217 ¥1,200 valuta: JPY PRIMA minore = maggiore x 100 costante nel modulo 120000 unità minori si legge come ¥120,000 DOPO minore = maggiore x 10^exp esponente JPY = 0 1200 unità minori si legge come ¥1,200
La trappola dell'esponente in un'immagine. Un moltiplicatore costante pari a 100 è corretto per INR e sbagliato di un fattore 100 per JPY, che non ha unità minore; la stessa costante è sbagliata di dieci per KWD, che ha tre decimali.

L'audit ha trovato la stessa costante nella pagina in cui una persona decide se accettare un saldo. formatMinor, dietro la pagina di richiesta a /m/{token}, divideva per 100 e incollava davanti un simbolo di rupia. Un debito di ¥6,000 veniva stampato come -₹60.00: simbolo sbagliato, un centesimo dell'importo. Il suo gemello JSON /v1/claims falliva insieme a lui, appiattendo in una sola le righe per valuta di un membro e mantenendo l'ultima valuta scritta dalla mappa.

Il terzo file è internal/ingestion/store.go, e la W8 lo ha trovato estendendo il rilevamento dei duplicati, non l'audit. La sua tolleranza di "più o meno una rupia" era la costante 100, una rupia contata in paise, una banda di più o meno ¥100 dove JPY non ha alcuna unità minore. Ora tutti e tre consultano l'esponente.

Confrontare interi nudi faceva sembrare uno yen una rupia

Il livello di deduplica di Dimesum confronta una nuova acquisizione con le spese recenti, così una ricevuta importata non può addebitare due volte un ordine inserito a mano. La query accanto a quella banda di tolleranza non aveva alcun filtro di valuta. Così ¥1,000 corrispondeva a ₹1,000, e un'importazione proponeva di sostituire una spesa con cui non aveva nulla a che fare. I gruppi erano multivaluta fin da ledger/00005, quindi il caso era raggiungibile e non teorico.

Le unità minori nude non sono confrontabili tra valute, e nemmeno le tolleranze. Ora la proiezione filtra per valuta, e ricava la sua banda da un'unità maggiore della valuta confrontata.

Due motori rispondevano a "chi paga chi", e il secondo era cieco alla valuta

Il difetto costoso è strutturale, non aritmetico. Il percorso di scrittura della liquidazione pianificava tramite internal/platform/simplify, un secondo motore di min-cash-flow la cui struct Balance non aveva un campo valuta. La somma zero per valuta rende zero anche la somma piatta, così un debito di ¥300,000 e un debito di ₹500 si annullavano a nulla e nessun controllo rifiutava il piano. Il piano quindi accoppiava un creditore in yen con un debitore in rupie.

La scrittura peggiorava le cose. Il servizio timbrava Currency: g.DefaultCurrency su ogni liquidazione, così un debito di ¥300,000 autorizzava una registrazione in INR, e il debito in yen non poteva essere saldato affatto.

Gli stessi quattro saldi instradati dal motore simplify cieco alla valuta e dal motore settle consapevole della valuta SALDI Asha +¥300,000 Bhavna -¥300,000 Chetan +₹500 Dev -₹500 simplify: Balance{MemberID, Minor} nessuna valuta sulla struct, così tutti e quattro si compensano piatti 300000 + (-300000) + 500 + (-500) = 0 piano: Bhavna paga Chetan un debitore in yen mandato a un creditore in rupie settle: Balance{MemberID, money.Amount} partiziona per valuta, poi instrada dentro ciascuna JPY: Bhavna paga Asha ¥300,000 INR: Dev paga Chetan ₹500 la liquidazione dichiara la valuta che salda simplify è eliminato, non corretto
Quattro saldi, due motori. La somma zero per valuta implica una somma piatta pari a zero, così un motore cieco alla valuta vede un gruppo in pareggio e instrada con sicurezza un pagamento tra due persone che non si devono nulla.

simplify è eliminato e /settle-plan è ritirato. internal/platform/settle è ora l'unico motore: partiziona le valute in convertibili e non convertibili, converte i saldi netti una sola volta, e instrada ogni valuta non convertibile nella propria denominazione. Una liquidazione dichiara la valuta che salda, e quel campo è obbligatorio quando un gruppo ne contiene più di una.

Un limite pari a zero veniva letto come nessun limite

Il controllo anti-sovrapagamento rifiuta un pagamento più grande del debito che salda. Il controllo leggeva outstanding > 0 && amount > outstanding, così un limite pari a zero saltava del tutto il confronto. Qualcuno che registra un pagamento contro un debito che non esiste è l'unico caso per cui la regola esiste, ed era proprio il caso che passava indisturbato.

La correzione è un tipo, non una condizione. OutstandingMinor è ora un *int64, così "nessuno l'ha calcolato" non può scriversi allo stesso modo di "la risposta è zero". Nil salta il controllo e significa davvero sconosciuto; qualsiasi chiamante con accesso al registro passa un numero reale.

Perché una suite verde non dimostrava nulla

Ogni fixture in questi percorsi usava INR. Un confronto cieco alla valuta è invisibile sotto un test a valuta singola, perché con una sola valuta non c'è nulla da confondere. Le suite non erano deboli, erano strette, e l'audit che le avrebbe ampliate era stato allontanato da A17.

Il verificatore end-to-end del registro condivideva la stessa cecità, ed è la parte che vale la pena tenere a mente. Il verificatore sommava le registrazioni per membro senza raggruppare per valuta, così gridava al lupo su un sano gruppo a due valute e sommava a zero su un gruppo rotto due volte. Un falso negativo è la direzione pericolosa. Un controllore costruito sulla stessa assunzione del codice sarà sempre d'accordo con il codice.

Ogni difetto trovato dall'audit multivaluta del 2026-08-21, cosa otteneva un gruppo in yen, e cosa è stato rilasciato.
DifettoDoveCosa otteneva un gruppo JPYCorrezione
Una struct Balance senza valutaplatform/simplifyun creditore in yen accoppiato con un debitore in rupiesimplify eliminato; settle partiziona per valuta
Il default del gruppo timbrato su ogni liquidazioneservizio di liquidazioneun debito di ¥300,000 autorizzava una registrazione in INRuna liquidazione dichiara la valuta che salda
outstanding > 0 nel controllo anti-sovrapagamentoservizio di liquidazioneun limite pari a zero diventava nessun limite*int64: nil significa sconosciuto, zero significa zero
Divisione per 100 con un simbolo di rupiaformatMinor del gatewayun debito di ¥6,000 stampato come -₹60.00simbolo e decimali dalla valuta
Saldi per valuta appiattiti in una sola riga/v1/claimsl'ultima valuta scritta dalla mappauna voce per valuta, coerente con GET /balances
Registrazioni sommate per membro, valuta ignorataverificatore end-to-end del registroun gruppo rotto due volte segnalato come in pareggioraggruppa per membro e valuta

Fai grep del tuo codice di denaro cercando il letterale 100

Cercalo per quella costante, e per ogni confronto che mette due importi affiancati senza una valuta accanto. Poi correggi la cosa più economica, quella che previene i prossimi sei: cancella un'annotazione nello stesso commit che la chiude. E elimina un motore duplicato invece di ripararlo. Due risposte a "chi paga chi" sono il modo in cui una delle due resta sbagliata.

Domande frequenti

Che cos'è la divisione delle spese multivaluta?

La divisione delle spese multivaluta registra ogni spesa nella valuta in cui è avvenuta e tiene un saldo separato per valuta invece di convertire tutto in una sola. Dimesum indicizza i saldi su gruppo, membro e valuta, così un debito in yen e un debito in rupie non si fondono mai in un unico numero. La conversione è una vista usata per un piano di liquidazione, mai un importo memorizzato.

Perché un 100 hardcoded è pericoloso nel codice che gestisce il denaro?

Un 100 hardcoded presume che ogni valuta abbia due decimali, e diverse non li hanno. JPY non ha unità minore, quindi moltiplicare per 100 trasforma una ricevuta da ¥1,200 in 120,000 unità minori, un'inflazione di cento volte più che un errore di arrotondamento. KWD ha tre decimali, quindi la stessa costante sbaglia di dieci. Leggi invece l'esponente ISO 4217.

Come si evita che due motori di liquidazione siano in disaccordo?

Elimina uno dei due motori invece di riconciliarli, perché una seconda risposta a chi paga chi è il modo in cui la prima resta sbagliata. Dimesum eseguiva simplify e settle in parallelo finché un audit non ha scoperto che il primo non aveva la valuta sul suo tipo di saldo, cosa che permetteva a un piano di accoppiare un creditore in yen con un debitore in rupie. simplify è stato rimosso e /settle-plan ritirato invece che corretto.

Perché la suite di test è rimasta verde nonostante sei bug di denaro?

La suite di test è rimasta verde perché ogni fixture nei percorsi interessati usava una sola valuta, e un confronto cieco alla valuta non può fallire quando c'è una sola valuta. Il verificatore end-to-end del registro faceva la stessa assunzione: sommava le registrazioni per membro senza raggruppare per valuta, così segnalava come in pareggio un gruppo rotto due volte. Un controllore costruito sull'assunzione stessa del codice è d'accordo con il codice.

Cosa fare con un'annotazione di follow-up una volta rilasciata la funzionalità?

Cancella un'annotazione di follow-up nello stesso commit che chiude il lavoro che descrive. Un'annotazione che si è avverata in silenzio è peggio di una ancora aperta, perché indirizza un lettore successivo lontano dal codice che descriveva. Dimesum ha lasciato una voce che diceva che i gruppi erano ancorati a INR per una settimana dopo il rilascio del multivaluta, e l'audit successivo ha saltato tre percorsi di denaro sulla sua parola.