Sei bug di denaro nelle spese multivaluta
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.
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.
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.
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.
| Difetto | Dove | Cosa otteneva un gruppo JPY | Correzione |
|---|---|---|---|
Una struct Balance senza valuta | platform/simplify | un creditore in yen accoppiato con un debitore in rupie | simplify eliminato; settle partiziona per valuta |
| Il default del gruppo timbrato su ogni liquidazione | servizio di liquidazione | un debito di ¥300,000 autorizzava una registrazione in INR | una liquidazione dichiara la valuta che salda |
outstanding > 0 nel controllo anti-sovrapagamento | servizio di liquidazione | un limite pari a zero diventava nessun limite | *int64: nil significa sconosciuto, zero significa zero |
| Divisione per 100 con un simbolo di rupia | formatMinor del gateway | un debito di ¥6,000 stampato come -₹60.00 | simbolo e decimali dalla valuta |
| Saldi per valuta appiattiti in una sola riga | /v1/claims | l'ultima valuta scritta dalla mappa | una voce per valuta, coerente con GET /balances |
| Registrazioni sommate per membro, valuta ignorata | verificatore end-to-end del registro | un gruppo rotto due volte segnalato come in pareggio | raggruppa 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.
Articoli più letti
- Il registro append-only per spese condivise esatte9 min di lettura
- Perché modificare una spesa ridefinisce la divisione9 min di lettura
- Saldare le spese di gruppo in pochi bonifici5 min di lettura
- Come dividere un conto con un piatto non condiviso9 min di lettura
- Come dividere l'affitto tra coinquilini in modo equo6 min di lettura