本文へスキップ
BecomeCoder

Solidityコース · 第4章 制御・お金・安全性 ― require・payable・event · レッスン18

payable と送金 ― ETHを受け取り・送る

ブラウザで完結

導入

いよいよコントラクトで実際に ETH をやり取りします。受け取りには payable、送り出しには .call などを使います。ここは事故が多い箇所なので、安全な順序も一緒に学びます。

説明

ETH を預けて(deposit)引き出せる(withdraw)、簡単な金庫です。

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

contract Vault {
    mapping(address => uint256) public balances;

    // ETH を受け取って残高に記録
    function deposit() public payable {
        require(msg.value > 0, "send some ETH");
        balances[msg.sender] += msg.value;
    }

    // 自分の残高を引き出す
    function withdraw(uint256 amount) public {
        require(balances[msg.sender] >= amount, "not enough");

        // 1) 先に状態を更新(チェック・エフェクト・インタラクション)
        balances[msg.sender] -= amount;

        // 2) 最後に送金
        (bool ok, ) = payable(msg.sender).call{value: amount}("");
        require(ok, "transfer failed");
    }
}
  • function deposit() public payablepayable があるので ETH を添えて呼べる。msg.value が預けた額。
  • payable(msg.sender).call{value: amount}("")msg.senderamount wei を送る、現在推奨の送金方法。戻り値の bool で成否を確認する。
  • 送金の順序が超重要:先に残高を減らし(エフェクト)、その後で送金する(インタラクション)。この順を守るのが Checks-Effects-Interactions パターンで、次章のリエントランシー攻撃を防ぎます。
sequenceDiagram
    participant U as 利用者
    participant V as Vault
    U->>V: deposit()(1 ETH 添付)
    V->>V: balances[U] += 1 ETH
    U->>V: withdraw(1 ETH)
    V->>V: 1) balances[U] -= 1 ETH(先に減らす)
    V-->>U: 2) 1 ETH を送金
    Note over V: 順序を逆にすると<br/>再入攻撃の隙が生まれる

やってみよう

「送ってから残高を減らす」順にすると、送金先が悪意あるコントラクトの場合、送金の途中でもう一度 withdraw を呼び返して二重・多重に引き出される危険があります(リエントランシー)。だから「先に帳簿を締めてから、最後にお金を動かす」。この順序は必ず身体に染み込ませてください。

実際に動かしてみよう

下のエディタのコントラクトを「コンパイル & デプロイ」すると、ブラウザ内で本物のSolidityコンパイラ(solc)がコンパイルし、EVM(@ethereumjs)にデプロイされます。デプロイ後は public な関数がボタンになって並ぶので、引数を入れて呼び出すと戻り値・イベント・require のエラー(revert理由)まで確認できます。本文のコントラクトを書き換えて動きを試しながら進めましょう(コンパイラ本体は初回だけ読み込みに数秒かかります)。

Solidity — ブラウザ内でコンパイル & 実行

ブラウザ内で本物のSolidityコンパイラ(solc)とEVM(@ethereumjs)を動かす環境を読み込みます(コンパイラ本体は初回のコンパイル時に読み込むため、最初の実行だけ数秒かかります)。
スクロールして表示された時点でも自動で読み込まれます。