整数除算はどこで丸める?BigIntで検算するSolidityの計算順序
整数の割り算では、掛ける順序と丸める単位で結果が変わります。BigIntのゼロ方向への丸め、切り上げ、Solidityの演算幅、個別計算と合計計算の違いを実行例で整理します。

手数料や持分の計算式を、電卓では合っているのにコードでは再現できない。そんなときは小数点の表示を直す前に、途中の割り算で端数を捨てていないかを確認します。
実数として等しい式でも、整数演算では結果が変わります。この記事ではNode.jsのBigIntで小さな反例を実行し、Solidityの整数型へ移すときに残る上限・符号・中間積の違いまで整理します。アカウント、ブラウザ、RPC、デプロイは使いません。Solidityの挙動は一次仕様を参照し、掲載した結果はNode.jsでの実行結果と区別します。
掲載例はNode.js 22以降で実行できます。各節のコードをexample.mjsへ保存し、同じ節で使う関数定義と呼び出しを合わせてからnode example.mjsを実行してください。再利用できる関数はinteger-rounding.mjsから取得できます。
同じ式に見えても、割る位置で結果が変わる
次のコードは、そのままNode.jsで実行できます。
console.log((1n * 3n) / 2n); // 1n
console.log((1n / 2n) * 3n); // 0n
最初の式は3を2で割り、整数の1を残します。後の式は最初に1を2で割って0にするため、その後で3を掛けても1には戻りません。
これはBigIntの精度が足りない問題ではありません。整数の除算が、ゼロ方向へ丸めた整数を返すためです。小数部分を保持する処理ではないので、後から掛け算をしても、途中で捨てた情報は復元しません。1
掛けてから割る:1 × 3 = 3 → 3 ÷ 2 = 1
割ってから掛ける:1 ÷ 2 = 0 → 0 × 3 = 0
実装では、元の式だけでなく、どの演算がどの単位の整数を返すかを書き出します。数量を最小単位の整数に直す方法は、ERC-20数量計算の記事で扱っています。単位変換と、その後の比率計算の丸めは別の仕様です。
「切り捨て」は負数まで含めると曖昧になる
非負の値では、ゼロ方向への丸めと、数学でいう床関数による切り下げが一致します。しかし、負数では一致しません。
console.log(-5n / 2n); // -2n
console.log(Math.floor(-5 / 2)); // -3
console.log(-5n % 2n); // -1n
-2.5から小数部分を取り除いてゼロへ近づけると-2です。負の無限大へ向けて切り下げると-3です。「端数切り捨て」とだけ書くと、どちらを求める仕様なのか分かりません。1
Solidityの型付き整数の除算もゼロ方向へ丸めます。一方、5 / 2のような数値リテラル同士の式では、型付き整数の実行時演算とは異なり、有理数としての精度を保持することがあります。Solidityを確認する際は、int256(-5) / int256(2)のように型を明示した例と、リテラルだけの式を混同しないようにします。2
以下の参照実装では、符号付きの丸め問題を混ぜないために、入力を非負の整数に限定します。
切り上げは、商と余りを見て決める
非負のa × b ÷ denominatorを考えます。商をq、余りをrとすると、次の関係を満たします。
a × b = q × denominator + r
0 ≤ r < denominator
切り下げならq、切り上げなら余りがあるときだけq + 1です。この規則をそのままコードにすると、割り切れる値やゼロを余分に1増やすことも防げます。
function mulDivUnsigned(a, b, denominator, rounding = 'down') {
const max = (1n << 256n) - 1n;
for (const value of [a, b, denominator]) {
if (typeof value !== 'bigint') throw new TypeError('expected bigint');
if (value < 0n || value > max) throw new RangeError('outside uint256');
}
if (denominator === 0n) throw new RangeError('division by zero');
if (rounding !== 'down' && rounding !== 'up') {
throw new TypeError('rounding must be down or up');
}
const product = a * b;
const quotient = product / denominator;
const remainder = product % denominator;
const result = quotient + (rounding === 'up' && remainder !== 0n ? 1n : 0n);
if (result > max) throw new RangeError('result exceeds uint256');
return result;
}
console.log(mulDivUnsigned(5n, 3n, 2n)); // 7n
console.log(mulDivUnsigned(5n, 3n, 2n, 'up')); // 8n
console.log(mulDivUnsigned(6n, 3n, 2n, 'up')); // 9n
console.log(mulDivUnsigned(0n, 3n, 2n, 'up')); // 0n
これはBigIntを使う検算用の参照実装です。入力と最終結果をuint256の範囲へ制限していますが、中間積まで256ビットに制限してはいません。
切り上げのために(product + denominator - 1) / denominatorと書く方法も、非負かつ正の除数という条件では数学的に成立します。ただし、固定幅整数へ移すと、その足し算でも上限を超える可能性があります。参照実装では商と余りを使い、条件を読み取りやすくしています。
「掛け算を先にする」だけではSolidity側の解決にならない
最大値をM = 2^256 - 1とすると、M × 2 ÷ 2の最終結果はMです。しかし、中間のM × 2はuint256の上限を超えます。
const max = (1n << 256n) - 1n;
console.log(mulDivUnsigned(max, 2n, 2n) === max); // true
console.log(max * 2n > max); // true
BigIntはこの中間積を保持できます。Solidityの通常のuint256乗算ではオーバーフローを検査するため、中間結果が範囲を外れると処理が取り消されます。uncheckedに変えても、元の精度を維持するのではなく、オーバーフロー時の扱いが変わるだけです。2
OpenZeppelinには、中間精度を保持して乗除算するMath.mulDivが用意されています。採用時には使用バージョン、丸め方向、除数ゼロ、最終結果が範囲を超える場合の挙動を確認します。3
この記事のBigInt関数は、そのSolidity実装をコンパイル・実行して検証したものではありません。任意精度で求めた期待値と、コントラクトの実装結果を別々に照合するための基準です。
個別計算の合計と、合計してからの計算は別の結果になる
1単位ずつの入力が10件あり、それぞれに3 / 2を掛けて切り下げるとします。
個別に計算:floor(1 × 3 ÷ 2) × 10 = 10
合計後に計算:floor(10 × 3 ÷ 2) = 15
差の5単位は、計算機の不具合ではありません。10回の除算で端数を捨てるか、最後の1回だけで捨てるかの違いです。
どちらを採用すべきかは、計算の用途によります。請求単位が1件ごとなのか、期間内の合計なのか、余りを次回へ持ち越すのか。その仕様を先に決めます。「精度が高そうだから合計してから計算する」と置き換えると、1件ごとの上限や契約上の丸め規則を変えてしまうことがあります。
小さな入力を何度も処理する場合は、1回だけの誤差ではなく、繰り返しによる差もテストへ含めます。持分の丸めが状態変化へ影響する例は、状態保持型ファジングの比較記事にもつながります。
テストでは期待値だけでなく、不等式も確認する
非負のa・bと正の除数について、切り下げ結果loが次の関係を満たすかを確認します。結果と除数を掛け戻せば元の積を超えず、結果を1増やして掛ければ元の積を超える、という関係です。
lo × denominator ≤ a × b < (lo + 1) × denominator
切り上げと切り下げの差は、割り切れる場合に0、それ以外に1です。これなら、特定の数値を並べたテストだけに依存しません。
そのうえで、0、1、割り切れる値、1単位の余り、uint256の最大値、最大値を超える最終結果、除数ゼロ、負数、Numberの誤入力を個別に確認します。全組合せテストも有限の入力範囲の検証であり、すべての業務要件の正しさを証明するものではありません。
まとめ
整数演算では、単位、符号、丸め方向、丸める時点、中間積と最終結果の上限をセットで決めます。
先に掛ければ途中の端数損失を減らせますが、固定幅整数のオーバーフローは別に残ります。BigIntを検算の正本にしながら、Solidity側では型と演算幅を明示し、ゼロ・境界値・繰り返し処理を確認するのが出発点です。
Footnotes
-
ECMAScript:BigIntの除算・剰余。2026-09-05確認。 ↩ ↩2
-
Solidity:整数型・除算・checked arithmetic。2026-09-05確認。記事内のSolidity説明は仕様参照であり、本検証ではコンパイル・EVM実行をしていません。 ↩ ↩2
-
OpenZeppelin Contracts 5.x:Math.mulDiv。2026-09-05確認。 ↩
確認した一次情報
- ECMAScript:BigIntの除算・剰余確認日: 2026/09/05
- Solidity:整数型・除算・checked arithmetic確認日: 2026/09/05
- OpenZeppelin Contracts 5.x:Math.mulDiv確認日: 2026/09/05



