# [ERC8023] コントラクトの所有権移転を3段階で安全に進める仕組みを理解しよう！

> ERC-8023は、スマートコントラクトの owner を新しいアドレスへ移す操作を、すぐに完了させないためのインターフェースです。

- URL: https://cardene.dev/articles/c8fd6c36069536ed7591
- 公開日: 2026-06-04
- 更新日: 2026-06-04
- トピック: Blockchain
- タグ: solidity, smartcontract, ブロックチェーン, erc

## はじめに

初めまして。
『DApps開発入門』という本や色々記事を書いているかるでねです。

https://amzn.asia/d/gxvJ0Pw

以下でも情報発信しているので、興味ある記事があればぜひ読んでみてください！

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

https://twitter.com/cardene777

https://zenn.dev/heku

https://cardene.substack.com/

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

今回は「**ERC-8023**」についてまとめていきます。

ERC-8023は、コントラクトの所有権移転を `initiate`、`confirm`、`accept` の3段階に分ける提案です。
単発の `transferOwnership()` ではなく、待機時間と再確認をはさむことで、誤送信や鍵漏洩時の被害を抑えやすくします。

https://eips.ethereum.org/EIPS/eip-8023

## 概要

**ERC-8023**は、スマートコントラクトの `owner` を新しいアドレスへ移す操作を、すぐに完了させないためのインターフェースです。
現在の `owner` が移転を開始し、バッファ期間の後に同じ移転先を確認し、最後に新しい `owner` が受け入れることで所有権が移ります。

従来の `transferOwnership()` では、指定したアドレスが間違っていても、その呼び出しで所有権が移ってしまいます。
ERC-8023では、移転先アドレスを2回指定させ、さらに最後は移転先本人に受け入れさせます。
これにより、アドレスポイズニングのような誤指定や、秘密鍵が漏れた直後の即時移転に対して、確認と取り消しの余地を作ります。

**ERC-173**は、コントラクトの `owner()` と `OwnershipTransferred` を定義する所有権管理の基本規格です。
ERC-8023は `owner()` をそのまま使うため、既存の所有権確認フローと組み合わせやすい設計になっています。

:::message
**ERC173**については以下の記事を参考にしてください。
:::

[\[ERC173\] コントラクトの所有権を制御できる仕組みを理解しよう！](/articles/0b8cdc156404920a8cbd)

従来方式との違いは以下です。

![ERC-8023 と従来方式の比較](/images/qiita-546e6e58-5e57-4378-adf8-ffa78c41756f.webp)

左側の従来方式では、`transferOwnership()` の1回の呼び出しで `owner` が変わります。
右側のERC-8023では、移転先の記録、待機、再確認、移転先本人による受け入れを分けています。

所有権移転の正常な流れは以下です。

![ERC-8023 の所有権移転フロー](/images/qiita-33046ac4-0f31-4dbc-8bf6-0dbdc9abb9df.webp)

重要なのは、`acceptOwnershipTransfer()` が実行されるまで `owner()` が返すアドレスは変わらない点です。
`preConfirmedOwner()` や `pendingOwner()` は移転手続きの状態を示すだけで、管理権限を持ちません。

## 動機

スマートコントラクトの `owner` は、アップグレード、管理設定、資金移動、緊急停止などの強い権限を持ちます。
DeFiプロトコル、スマートコントラクトウォレット、オンチェーンユーティリティでは、この `owner` の安全性がシステム全体の安全性に直結します。

単発の `transferOwnership()` は実装しやすい一方で、操作ミスに弱いです。
移転先アドレスを1回間違えるだけで、管理権限が意図しないアドレスへ移ります。
また、`owner` の秘密鍵が漏れた時、攻撃者がすぐに所有権を奪える設計では、防御側が資金退避や設定変更を行う時間を取りにくくなります。

ERC-8023は、所有権移転に確認の段階を増やします。
現在の `owner` は、まず移転を開始し、バッファ期間の後に同じ `newOwner` を再度指定して確認します。
移転先が間違っていれば、`cancelPendingOwnershipTransfer()` で手続きを取り消し、最初からやり直せます。

この提案が重視している目的は大きく4つです。

- 操作ミスの確率を下げる。
- 移転先アドレスをオンチェーンで確認する運用を促す。
- 移転処理の途中で取り消せるようにする。
- 所有権管理の仕組みを複雑にしすぎない。

特にバッファ期間は、鍵漏洩時の防御時間として機能します。
攻撃者が `initiateOwnershipTransfer()` を実行しても、すぐに `confirmOwnershipTransfer()` へ進めないため、防御側の `owner` は同じ期間内に再度 `initiateOwnershipTransfer()` を実行して移転先を上書きできます。

鍵漏洩時の時間差は以下です。

![ERC-8023 の鍵漏洩時バッファ](/images/qiita-7cae3f2e-58af-4b28-8721-eb549c469827.webp)

攻撃者が悪意ある `newOwner` を指定しても、バッファ期間があるため即時には確認まで進めません。
防御側の `owner` は、その間に移転先の上書き、資金退避、設定変更などを行えます。

## 仕様

ERC-8023の仕様は、必須の `MultiStepOwnable` と、任意拡張の `UpdateableOwnershipTransferBuffer` に分かれます。
中心になるのは所有権移転の3段階です。
現在の `owner` が `initiateOwnershipTransfer()` と `confirmOwnershipTransfer()` を呼び、確認済みの `pendingOwner` が `acceptOwnershipTransfer()` を呼ぶことで、最後に `owner()` の返すアドレスが変わります。

### `MultiStepOwnable`

複数段階の所有権移転に対応するコントラクトは、以下のインターフェースを実装します。

```js
/// @title Multistep Ownership Standard
interface MultiStepOwnable {
    event OwnershipTransferInitiated(address indexed prevOwner, address indexed newOwner);
    event OwnershipTransferConfirmed(address indexed prevOwner, address indexed newOwner);
    event OwnershipTransferred(address indexed prevOwner, address indexed newOwner);

    function initiateOwnershipTransfer(address newOwner) external;
    function confirmOwnershipTransfer(address newOwner) external;
    function cancelPendingOwnershipTransfer() external;
    function acceptOwnershipTransfer() external;

    function owner() external view returns (address);
    function pendingOwner() external view returns (address);
    function preConfirmedOwner() external view returns (address);
    function getOwnershipTransferBuffer() external view returns (uint256);
}
```

インターフェースは、移転操作、取り消し、参照、イベントをまとめて定義します。
ただし、管理権限を持つのは `owner()` が返すアドレスだけです。

### 関数

#### `initiateOwnershipTransfer`

`initiateOwnershipTransfer()` は、所有権移転の1段階目です。
現在の `owner` が新しい所有者候補を指定し、そのアドレスを `preConfirmedOwner` として記録します。

```js
function initiateOwnershipTransfer(address newOwner) external;
```

- **引数**

| 項目 | 型 | 内容 |
|------|------|------|
| `newOwner` | `address` | 新しい所有者候補のアドレス |

この呼び出しは、所有権をまだ移しません。
`owner()` は引き続き現在の `owner` を返します。
同じ移転先で先へ進めるには、バッファ期間の後に `confirmOwnershipTransfer(newOwner)` を呼びます。

移転先を間違えた時や、攻撃者が別のアドレスを指定した時は、再度 `initiateOwnershipTransfer()` を呼ぶことで手続きをやり直す設計です。
提案では、再実行によって待機時間が最初から数え直されるため、十分に長いバッファ期間があれば防御側が時間を稼げます。

`initiateOwnershipTransfer()` が成功した時は、以下のイベントが発行されます。

```js
event OwnershipTransferInitiated(address indexed prevOwner, address indexed newOwner);
```

- **引数**

| 項目 | 型 | 内容 |
|------|------|------|
| `prevOwner` | `address` | 現在の `owner` |
| `newOwner` | `address` | 所有者候補として記録されたアドレス |

#### `confirmOwnershipTransfer`

`confirmOwnershipTransfer()` は、所有権移転の2段階目です。
現在の `owner` が、開始時と同じ `newOwner` をもう一度指定して、移転先を確認します。

```js
function confirmOwnershipTransfer(address newOwner) external;
```

- **引数**

| 項目 | 型 | 内容 |
|------|------|------|
| `newOwner` | `address` | `preConfirmedOwner` と一致すべきアドレス |

この呼び出しは、`initiateOwnershipTransfer()` からバッファ期間が経過した後に実行されます。
指定された `newOwner` は、開始時に記録した `preConfirmedOwner` と一致する必要があります。
別のアドレスへ移したい時は、確認だけを変えるのではなく、`initiateOwnershipTransfer()` からやり直します。

確認が成功すると、`newOwner` は `pendingOwner` として扱われます。
ただし、この時点でも `owner()` は変わりません。
最終段階の `acceptOwnershipTransfer()` が実行されるまで、現在の `owner` が管理権限を持ち続けます。

`confirmOwnershipTransfer()` が成功した時は、以下のイベントが発行されます。

```js
event OwnershipTransferConfirmed(address indexed prevOwner, address indexed newOwner);
```

- **引数**

| 項目 | 型 | 内容 |
|------|------|------|
| `prevOwner` | `address` | 現在の `owner` |
| `newOwner` | `address` | `pendingOwner` として確認されたアドレス |

#### `cancelPendingOwnershipTransfer`

`cancelPendingOwnershipTransfer()` は、最終受け入れの前に所有権移転を取り消す関数です。
提案では、`pendingOwner` を消し、移転処理をキャンセルすることを求めています。

```js
function cancelPendingOwnershipTransfer() external;
```

取り消し後に所有権を移したい時は、`initiateOwnershipTransfer()` からやり直します。

この関数は、移転先アドレスの誤指定、運用判断の変更、鍵漏洩時の防御で使われます。
提案のセキュリティ条件では、`initiateOwnershipTransfer()`、`confirmOwnershipTransfer()`、`cancelPendingOwnershipTransfer()` を呼べるのは `owner` だけです。

誤った移転先を指定した時の戻し方は以下です。

![ERC-8023 の誤指定時リカバリー](/images/qiita-7c3925c9-795a-493a-9a42-457679dfb619.webp)

`cancelPendingOwnershipTransfer()` を使うと、記録された候補を消して現在の `owner` のまま止められます。
別の移転先へ進めたい時は、再度 `initiateOwnershipTransfer()` を呼び、待機時間を最初から数え直します。

#### `acceptOwnershipTransfer`

`acceptOwnershipTransfer()` は、所有権移転の最終段階です。
`pendingOwner` として確認されたアドレスだけが呼び出せます。

```js
function acceptOwnershipTransfer() external;
```

実装では、たとえば `msg.sender == pendingOwner()` のようなアクセス制御を行います。

この呼び出しが成功した時に、`owner()` が返すアドレスが新しい所有者へ変わります。
そのため、`acceptOwnershipTransfer()` の前後で管理権限の所在が切り替わります。

`acceptOwnershipTransfer()` が成功した時は、以下のイベントが発行されます。

```js
event OwnershipTransferred(address indexed prevOwner, address indexed newOwner);
```

- **引数**

| 項目 | 型 | 内容 |
|------|------|------|
| `prevOwner` | `address` | 以前の `owner` |
| `newOwner` | `address` | 新しい `owner` |

### 参照関数

ERC-8023では、移転手続きの状態を外部から読めるように、3つのアドレス参照関数と1つのバッファ参照関数を定義しています。

#### `owner`

`owner()` は、現在の管理権限を持つアドレスを返します。

```js
function owner() external view returns (address);
```

- **戻り値**

| 項目 | 型 | 内容 |
|------|------|------|
| `owner` | `address` | 現在の所有者アドレス |

提案では、権限を持つのは `owner()` が返すアドレスだけです。
`preConfirmedOwner()` や `pendingOwner()` が値を返していても、管理権限は移っていません。

#### `pendingOwner`

`pendingOwner()` は、確認済みで、受け入れ待ちになっているアドレスを返します。

```js
function pendingOwner() external view returns (address);
```

- **戻り値**

| 項目 | 型 | 内容 |
|------|------|------|
| `pendingOwner` | `address` | 受け入れ待ちの新しい所有者候補 |

`pendingOwner` は `acceptOwnershipTransfer()` を呼ぶ候補です。
一方で、`acceptOwnershipTransfer()` が成功するまでは管理操作を実行できません。

#### `preConfirmedOwner`

`preConfirmedOwner()` は、開始済みで、まだ確認前の所有者候補を返します。

```js
function preConfirmedOwner() external view returns (address);
```

- **戻り値**

| 項目 | 型 | 内容 |
|------|------|------|
| `preConfirmedOwner` | `address` | 開始段階で記録された所有者候補 |

`preConfirmedOwner` は、`confirmOwnershipTransfer(newOwner)` で照合される値です。
このアドレスも管理権限を持ちません。

#### `getOwnershipTransferBuffer`

`getOwnershipTransferBuffer()` は、`initiateOwnershipTransfer()` と `confirmOwnershipTransfer()` の間に置くバッファ時間を秒数で返します。

```js
function getOwnershipTransferBuffer() external view returns (uint256);
```

- **戻り値**

| 項目 | 型 | 内容 |
|------|------|------|
| `buffer` | `uint256` | 所有権移転の開始から確認までに必要な秒数 |

提案では値の範囲を強制していません。
一方で、2日から14日の範囲が強く推奨されています。
短すぎる値では確認や防御の時間が足りず、長すぎる値では正当な所有権移転の運用が重くなります。

権限境界は以下です。

![ERC-8023 の権限境界](/images/qiita-b66ec1a3-be52-4eb1-b41c-a3e3f0ec14a9.webp)

この図で見るべきなのは、`owner`、`preConfirmedOwner`、`pendingOwner` が同じ「所有者候補」に見えても、権限がまったく違う点です。
管理操作を実行できるのは `owner` だけで、`pendingOwner` ができるのは受け入れの実行だけです。

### `UpdateableOwnershipTransferBuffer`

`MultiStepOwnable` コントラクトは、任意で `UpdateableOwnershipTransferBuffer` を実装できます。
この拡張は、所有権移転に使うバッファ期間を後から変更するためのインターフェースです。

```js
/// @title UpdateableOwnershipTransferBuffer. Extension of MultiStepOwnable.
interface UpdateableOwnershipTransferBuffer {
    function initiateOwnershipBufferUpdate(uint256 newBuffer) external;
    function confirmOwnershipBufferUpdate(uint256 newBuffer) external;
}
```

バッファ期間の変更も、開始と確認の2段階で行います。
確認は、既存のバッファ期間が経過してから実行されます。
提案では、この待機を強制しない実装は、`owner` の鍵漏洩時に `ownershipTransferBuffer` の安全性を崩すと説明しています。

### バッファ更新関数

#### `initiateOwnershipBufferUpdate`

`initiateOwnershipBufferUpdate()` は、バッファ期間の変更を開始する関数です。

```js
function initiateOwnershipBufferUpdate(uint256 newBuffer) external;
```

- **引数**

| 項目 | 型 | 内容 |
|------|------|------|
| `newBuffer` | `uint256` | 新しく設定したいバッファ秒数 |

この呼び出しだけでは、バッファ期間を変更しません。
変更内容を記録し、既存のバッファ期間が経過するのを待ちます。

#### `confirmOwnershipBufferUpdate`

`confirmOwnershipBufferUpdate()` は、バッファ期間の変更を確定する関数です。

```js
function confirmOwnershipBufferUpdate(uint256 newBuffer) external;
```

- **引数**

| 項目 | 型 | 内容 |
|------|------|------|
| `newBuffer` | `uint256` | 開始時に指定した新しいバッファ秒数 |

提案では、既存のバッファ期間が経過していない時は `revert` するべきだとしています。
これにより、攻撃者が `owner` の鍵を得た直後にバッファ期間を短くして、所有権移転を一気に進める動きを防ぎます。

バッファ更新の流れは以下です。

![ERC-8023 のバッファ更新フロー](/images/qiita-f3a696ee-958e-4032-87b0-cf671700d50d.webp)

所有権移転だけでなく、待機時間の変更にも待機時間をかける点が重要です。
この制約がないと、バッファ期間そのものが攻撃者に短縮され、3段階移転の防御効果が弱くなります。

## 補足

#### バッファ期間が任意である理由

ERC-8023のバッファ期間は、移転先アドレスをオンチェーンで確認するために導入されています。
ただし、すべてのコントラクトで同じ待機時間が必要とは限りません。
個人管理の小さなコントラクト、DAOが管理する大きなプロトコル、緊急対応が必要なコントラクトでは、適切な待機時間が違います。

そのため提案は、バッファ期間を柔軟に扱えるようにしています。
バッファを使わない実装にすると、`Ownable2Step` に確認段階を1つ加えた設計に近くなります。
一方で、鍵漏洩時の防御時間を重視するなら、十分なバッファ期間を設定する必要があります。

#### `owner` 互換性

ERC-8023は、所有者の取得に `owner()` を使います。
この設計により、既存の所有権確認の仕組みと合わせやすくなります。

外部ツールや別コントラクトが `owner()` を見て管理者を判定している場合でも、ERC-8023は `acceptOwnershipTransfer()` まで `owner()` を変えません。
そのため、移転手続きの途中にある `preConfirmedOwner` や `pendingOwner` が、既存の管理者判定をすり抜けて権限を得ることはありません。

## 互換性

提案の互換性の節は `TBD` とされています。
ただし、仕様上は `owner()` を維持しているため、既存の所有者参照と合わせやすい方向で設計されています。

注意すべき点は、単発の `transferOwnership()` を前提にした運用フローです。
所有権移転がすぐに完了することを前提にした管理画面、スクリプト、監視処理は、開始、確認、受け入れの3段階を扱う必要があります。
また、`pendingOwner` が表示されていても `owner()` は変わっていないため、UIでは「受け入れ待ち」と「所有者変更済み」を分けて表示する必要があります。

## セキュリティ

ERC-8023のセキュリティで最も重要なのは、移転処理の途中で権限を移さないことです。
`preConfirmedOwner()` と `pendingOwner()` は状態の記録であり、管理権限ではありません。
管理権限を持つのは、常に `owner()` が返すアドレスです。

`initiateOwnershipTransfer()`、`confirmOwnershipTransfer()`、`cancelPendingOwnershipTransfer()` は `owner` だけが呼べるようにします。
第三者が開始、確認、取り消しを呼べると、所有権移転の状態を勝手に動かせます。
特に `cancelPendingOwnershipTransfer()` は防御のための関数でもあるため、呼び出し権限を広げると正当な移転を止められます。

`OwnershipTransferBuffer` は、`owner` を設定する時に合わせて設定します。
コンストラクタや `initialize()` で初期化しないまま運用すると、実装によっては待機時間が意図せず短くなります。
バッファ期間を使う実装では、`initiateOwnershipTransfer()` と `confirmOwnershipTransfer()` の間で厳密に待機を強制します。

新しい `owner` が `acceptOwnershipTransfer()` を実行するまでは、既存の `owner` がすべての管理権限を持ち続けます。
これは、移転先がまだ受け入れていない段階で、管理権限だけが宙に浮く状態を避けるためです。

バッファ期間の更新を実装する場合も、変更を即時反映しないことが重要です。
鍵漏洩時に攻撃者がバッファ期間を0へ近づけられると、所有権移転の確認時間が消えます。
そのため `confirmOwnershipBufferUpdate()` では、既存のバッファ期間が経過していることを確認します。

## 最後に

今回は「**ERC-8023**」についてまとめてきました。

ERC-8023は、コントラクト所有権の移転を開始、確認、受け入れの3段階に分ける提案です。
`owner()` の互換性を残しつつ、移転先の再確認、取り消し、バッファ期間を組み合わせることで、誤操作や鍵漏洩時の被害を抑えやすくします。
特に、`pendingOwner` が存在しても `acceptOwnershipTransfer()` までは管理権限が移らない点が、この提案の中心です。

他でも色々記事を書いているのでぜひよろしければ読んでいってください！

https://amzn.asia/d/gxvJ0Pw

https://cardene.notion.site/EIP-2a03fa3ea33d43baa9ed82288f98d4a9

https://zenn.dev/heku

https://mirror.xyz/0xcE77b9fCd390847627c84359fC1Bc02fC78f0e58

https://twitter.com/cardene777

https://cardene.substack.com/
