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

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

正規URL: https://3mikan.com/archives/6079

著者: みかん
公開: 2026-09-06T00:00:00.000Z
更新: 2026-09-06T00:00:00.000Z

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

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

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

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

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

次の`narrow`は、引数を明示的に`uint8`へ変換します。Solidityの[型変換仕様](https://docs.soliditylang.org/en/v0.8.36/types.html#explicit-conversions)では、大きい整数型から小さい整数型へ変換すると、上位のビットが切り捨てられます。

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

```solidity
// 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](https://docs.openzeppelin.com/contracts/5.x/api/utils#SafeCast)にも、範囲外なら失敗する`toUint8`などがあります。ここで実行する最小例はそのライブラリーを使わず、検査する位置を見える形にしています。

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

```solidity
// 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を導入したうえで、空の作業フォルダーに`src`と`test`という名前のフォルダーを作ります。`Narrowing.sol`は`src`の中へ、上のテストは`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`と独自エラーの違いは、[エラーデータの読み方](/archives/6002)で整理できます。

## 確認した一次情報

- [Solidity: Explicit Conversions](<https://docs.soliditylang.org/en/v0.8.36/types.html#explicit-conversions>): 確認日 2026-09-05
- [Solidity: Integers](<https://docs.soliditylang.org/en/v0.8.36/types.html#integers>): 確認日 2026-09-05
- [OpenZeppelin: SafeCast](<https://docs.openzeppelin.com/contracts/5.x/api/utils#SafeCast>): 確認日 2026-09-05
