The Original Problem: Rounding in 401(k) Allocations

Decades ago, when I wrote COBOL routines to allocate earnings across participant 401(k) balances, I ran into a deceptively simple but consequential problem: when dividing a fixed earnings pool amongst thousands of participants, rounding differences add up.

The naive method of computing every participant’s share independently left a few cents on the table (or off the books). But every accounting system must reconcile to the penny.

Our solution was a declining-balance allocation algorithm. The idea is that it maintained conservation between the total earnings and the total asset base throughout the process.

  1. Initialize
    • E_total = total earnings to allocate
    • A_total = total asset base (sum of all participants’ balances)
  2. For each participant i:
    • share = A_i / A_total
    • allocation = floor(E_total × share, 2 decimals)
    • Record allocation
    • Then rebase:
      • E_total ← E_total − allocation
      • A_total ← A_total − A_i
  3. Repeat until the last participant automatically receives the remaining earnings. Thus the total allocated reconciles exactly.

Each participant’s slice comes from the remaining pie, not from a static total, so rounding remainders never accumulate. This is how our COBOL systems achieved penny-perfect allocations decades before blockchain.


Fast-Forward to DeFi: Balancer’s BatchSwap Bug

In August 2025, Balancer suffered a nine-figure exploit that exposed the same precision issue we used to guard against.

Balancer’s batchSwap lets users chain multiple token swaps within a single transaction. Internally, the Vault upscales token balances to 18 decimals using in a routine called upscaleArray:

balances[i] = FixedPoint.mulDown(balances[i], scalingFactors[i]);

The problem? mulDown always rounds down.
Repeated over dozens/hundreds/thousands of swaps, this introduced a systematic downward bias.
Because all swaps settled together, the attacker could loop these operations and extract value from the accumulated “dust,” fueled by flash-loaned liquidity.


The Parallel: Residual-Carry Rounding (Accounting-Style Reconciliation)

The same principle that eliminated our 401(k) rounding errors applies neatly to AMM math.
In this example, Residual-Carry Rounding (RCR) carries forward the fractional remainder so the system stays fully reconciled.

function upscaleArrayRCR(uint256[] memory balances, uint256[] memory scales) internal pure {
    uint256 carry = 0;
    for (uint256 i = 0; i + 1 < balances.length; ++i) {
        uint256 product = balances[i] * scales[i];
        uint256 q = product / 1e18;
        uint256 rem = product % 1e18;
        uint256 sum = carry + rem;
        q += sum / 1e18;
        carry = sum % 1e18;
        balances[i] = q;
    }
    // Final element absorbs residual
    balances[balances.length - 1] += carry / 1e18;
}

Just like our COBOL allocator, each step floors locally but rebases the totals for the next iteration. No dust, no bias, and no exploitable residuals.

To eliminate path-ordering bias, you could also choose which index absorbs the last residual (largest balance, BPT slot, random, etc.).


Compare & Contrast: “Higher Precision” vs Accounting‑Style Rounding Correction

In their post, BlockSec identified the root problem in the Balancer exploit as precision loss caused by inconsistent rounding. They recommend protocols “should employ higher‑precision arithmetic and implement robust validation checks.”

That’s valid advice. Larger bit‑width arithmetic (e.g., 512‑bit math) reduces the size of rounding errors — but it doesn’t eliminate bias if the rounding process itself discards residuals. You can have 100 decimals of precision and still lose value if each step floors and forgets.

By contrast, the COBOL‑inspired residual-carry rounding algorithm guarantees reconciliation through process design rather than numerical depth:

  • Each step allocates proportionally to the remaining base.
  • Every floor operation’s remainder is carried forward.
  • The final allocation absorbs all accumulated dust.

In summary

Approach Mechanism Result
BlockSec Recommendation Increase numeric precision; validate outcomes Smaller per‑step errors, but bias can still accumulate
Residual-Carry Rounding Re‑base totals and carry remainders Zero cumulative bias; invariant preservation by design

Thus, “higher precision” is mitigation, but “rounding reconciliation” is prevention.
In proper accounting terms: if every debit has a credit and the books balance after every step, the number of decimals becomes irrelevant.


Why It Matters

Whether coded in COBOL in 1989 or Solidity in 2025, the rule is the same: precision is policy. A rounding direction is a policy choice — and if “no one keeps the dust” is your goal, you must carry it forward until the system reconciles.

Never round and forget. Round and reconcile.

Credit: ChatGPT helped me author this post, though the ideas were mine.