導入
いよいよコントラクトで実際に 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 payable…payableがあるので ETH を添えて呼べる。msg.valueが預けた額。payable(msg.sender).call{value: amount}("")…msg.senderへamountwei を送る、現在推奨の送金方法。戻り値の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 を呼び返して二重・多重に引き出される危険があります(リエントランシー)。だから「先に帳簿を締めてから、最後にお金を動かす」。この順序は必ず身体に染み込ませてください。