3MIKAN
仮想通貨直コン

Solidity 0.8でもuint8変換は値を切り捨てる:縮小変換と演算の違い

uint256をuint8へ変換したときの切り捨てを、演算オーバーフローと区別します。255・256の境界、変換前の検査、演算前の拡張、bytes型との違いを検証します。

3MIKANのキャラクターが大きさの異なるブロックと小さな枠を見比べ、変換前の範囲を確認する場面

Solidity 0.8以降は、通常の整数演算で範囲を超えると失敗します。しかし、その知識だけでuint8(value)のような型変換まで安全だと判断すると、値を失うことがあります。

この記事は、小さい整数型で値を保存したり、APIの入力を別の型へ変換したりする開発者向けです。演算のオーバーフロー検査と、明示的な縮小変換は別の規則だと整理し、変換前に何を確かめるべきかを示します。

使うのは整数とローカルのEVMだけです。ウォレット、送金、外部RPC、サービスのアカウントは使いません。

256をuint8へ変えると、どうなるか

uint8の範囲は0から255です。8ビットでは256通りしか表現できないため、256をそのまま保存することはできません。

次のnarrowは、引数を明示的にuint8へ変換します。Solidityの型変換仕様では、大きい整数型から小さい整数型へ変換すると、上位のビットが切り捨てられます。

このコントラクトをNarrowing.solとして保存してください。

// SPDX-License-Identifier: MIT
pragma solidity 0.8.36;

contract Narrowing {
    error OutOfRange(uint256 value);

    function narrow(uint256 value) external pure returns (uint8) {
        return uint8(value);
    }

    function checkedNarrow(uint256 value) external pure returns (uint8) {
        if (value > type(uint8).max) revert OutOfRange(value);
        return uint8(value);
    }

    function addSmall(uint8 a, uint8 b) external pure returns (uint8) {
        return a + b;
    }

    function castAfterAdd(uint8 a, uint8 b) external pure returns (uint256) {
        return uint256(a + b);
    }

    function widenBeforeAdd(uint8 a, uint8 b) external pure returns (uint256) {
        return uint256(a) + uint256(b);
    }

    function integerAndBytes(uint16 value) external pure returns (uint8, bytes1) {
        return (uint8(value), bytes1(bytes2(value)));
    }

    function reinterpretSign(int8 value) external pure returns (uint8) {
        return uint8(value);
    }
}

narrowへ渡す値と結果は、次のように対応します。

引数のuint256 下位8ビットだけを残した結果
0 0
255 255
256 0
257 1
1000 232
type(uint256).max 255

これは「255を超えたら255へ丸める」処理ではありません。非負の整数について、256で割った余りと同じ結果になります。値の上限を検査する用途には使えません。

checkedNarrowは、値を失う前に止める

同じコントラクトのcheckedNarrowは、元のuint256のまま上限を比較します。255以下なら変換し、256以上ならOutOfRangeで失敗します。

比較する順序が大切です。先にuint8へ変換してから「255以下か」を調べても、切り捨て前の情報は残っていません。uint8の値はすでに0から255なので、その比較は元の範囲超過を検出できません。

独自の検査を各所へ書きたくない場合は、OpenZeppelinのSafeCastにも、範囲外なら失敗するtoUint8などがあります。ここで実行する最小例はそのライブラリーを使わず、検査する位置を見える形にしています。

また、型の範囲に収まることと、業務上の上限に収まることは別です。許容値が100までなら、255以下という条件だけでは足りません。

「足してから大きい型へ」では間に合わない

addSmall(255, 1)は、uint8の加算として範囲を超え、通常の検査付き演算では失敗します。これはnarrow(256)が0を返す挙動とは違います。

castAfterAdd(200, 100)も失敗します。先に計算されるa + bの型がuint8だからです。結果をuint256へ変換する前に、加算の範囲超過が起きます。

対してwidenBeforeAdd(200, 100)は300を返します。両方の入力を大きい型へ変えてから計算しているためです。

uint256(a + b)          → 小さい型で足す。その後で変換
uint256(a) + uint256(b) → 先に変換する。大きい型で足す

この例で分かるのは、計算結果の変数型だけでは演算途中の型を決められない、ということです。式の内側から、どの型で計算されるかを確認します。

定数が拒否されたから、変数も拒否されるとは限らない

uint8(256)という範囲外の数値リテラルは、Solidity 0.8.36ではコンパイル時に拒否されます。また、uint256からuint8へ暗黙に代入するコードも拒否されます。

一方、引数として受け取ったuint256 valueuint8(value)と明示的に変換するコードは、有効です。256を渡すと下位8ビットだけが残ります。

エディターで定数の例がエラーになったことを、実行時の入力検査の代わりにしないでください。リテラルの適合性、暗黙の変換、明示的な変換、演算中の検査を分けてテストする必要があります。

整数とbytesでは、残る側が違う

integerAndBytes(0x1234)は、整数側が0x34、バイト列側が0x12になります。

整数の縮小変換は下位ビットを残しますが、固定長バイト列の縮小変換は先頭側を残すためです。どちらも短い型へ変えているからといって、同じ切り取り操作ではありません。

reinterpretSign(-1)も注意例です。同じ8ビットを符号なしとして扱うと255になります。負数をゼロに補正する処理ではないので、非負の値だけを許す用途では、符号付きの元の値を先に検査します。

再現するときは境界と失敗の種類を見る

Foundryでは、上のコントラクトを作成し、返り値をassertで確認できます。失敗が必要な例は、単に失敗したことだけでなく、期待したエラーかを確認します。

// SPDX-License-Identifier: MIT
pragma solidity 0.8.36;
import {Narrowing} from "../src/Narrowing.sol";

contract NarrowingSmokeTest {
    function testBoundary() public {
        Narrowing example = new Narrowing();
        assert(example.narrow(255) == 255);
        assert(example.narrow(256) == 0);
        assert(example.widenBeforeAdd(200, 100) == 300);
        try example.addSmall(255, 1) returns (uint8) {
            revert("expected arithmetic panic");
        } catch Panic(uint256 code) {
            assert(code == 0x11);
        }
    }
}

Foundryを導入したうえで、空の作業フォルダーにsrctestという名前のフォルダーを作ります。Narrowing.solsrcの中へ、上のテストはtestの中にNarrowing.t.solとして保存します。作業フォルダーでforge test --use 0.8.36 --evm-version prague --optimize --optimizer-runs 200を実行してください。コンパイラーを初めて取得するとき以外、チェーンへの接続は不要です。

確認した環境と範囲

項目 使用した条件
確認日 2026年9月5日
コンパイラー Solidity 0.8.36、ビルド識別子8a079791
テスト実行 Foundry 1.8.0
EVM設定 Prague
最適化 有効、200 runs
OS Ubuntu 24.04.4、x86_64

例のコントラクトはコンパイルし、隔離したEVM上で返り値と失敗条件を検証しています。公開チェーンへの接続、実ウォレットの操作、送信はしていません。表示されたガス値の優劣や実運用の安全性を比較する記事ではありません。

まとめ

整数型を小さくするなら、変換前の値で範囲を確認します。大きい型で計算したいなら、計算前に入力を変換します。

「Solidity 0.8だから」という一つの理由で、暗黙の変換、明示的な縮小変換、演算、固定長バイト列の切り取りを同じ扱いにしないことが、値を失わない実装への出発点です。失敗した呼び出しのPanicと独自エラーの違いは、エラーデータの読み方で整理できます。

確認した一次情報