💭
Solidity基礎学習25日目(ERC20 vs ERC777, ERC721 vs ERC1155)
Solidity基礎学習 - ERC規格の比較分析
日付: 2025年9月22日
学習内容: ERC20とERC777の共通点・相違点、ERC721とERC1155の共通点・相違点について
1. ERC規格の概要
1.1 ERC規格とは
Ethereum Request for Comments(ERC)は、Ethereumネットワーク上での標準的なスマートコントラクトの実装方法を定義する規格です。これらの規格により、異なるプロジェクト間での互換性と相互運用性が確保されます。
主要なERC規格:
- ERC20: 同質化トークン(Fungible Token)の標準
- ERC777: ERC20の改良版、より高度な機能を持つ同質化トークン
- ERC721: 非同質化トークン(NFT)の標準
- ERC1155: マルチトークン標準、同質化と非同質化トークンを統合
1.2 トークンの種類
// 同質化トークン(Fungible Token)
// - 同じ価値を持つ、交換可能なトークン
// - 例:ETH、USDC、DAI
// 非同質化トークン(Non-Fungible Token)
// - 固有の価値を持つ、交換不可能なトークン
// - 例:アート作品、ゲームアイテム、デジタル資産
2. ERC20とERC777の比較分析
2.1 ERC20の基本構造
ERC20の必須関数
interface IERC20 {
// 必須関数
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
// 必須イベント
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
ERC20の実装例
contract SimpleERC20 is IERC20 {
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
uint256 private _totalSupply;
string public name;
string public symbol;
uint8 public decimals;
constructor(string memory _name, string memory _symbol, uint8 _decimals) {
name = _name;
symbol = _symbol;
decimals = _decimals;
_totalSupply = 1000000 * 10**_decimals;
_balances[msg.sender] = _totalSupply;
}
function transfer(address to, uint256 amount) public override returns (bool) {
_balances[msg.sender] -= amount;
_balances[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
function approve(address spender, uint256 amount) public override returns (bool) {
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
function transferFrom(address from, address to, uint256 amount) public override returns (bool) {
_allowances[from][msg.sender] -= amount;
_balances[from] -= amount;
_balances[to] += amount;
emit Transfer(from, to, amount);
return true;
}
}
2.2 ERC777の基本構造
ERC777の拡張機能
interface IERC777 {
// ERC20の機能を継承
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
// ERC777の追加機能
function name() external view returns (string memory);
function symbol() external view returns (string memory);
function granularity() external view returns (uint256);
function defaultOperators() external view returns (address[] memory);
function isOperatorFor(address operator, address tokenHolder) external view returns (bool);
function authorizeOperator(address operator) external;
function revokeOperator(address operator) external;
function send(address to, uint256 amount, bytes calldata userData) external;
function operatorSend(address from, address to, uint256 amount, bytes calldata userData, bytes calldata operatorData) external;
function burn(uint256 amount, bytes calldata data) external;
function operatorBurn(address from, uint256 amount, bytes calldata data, bytes calldata operatorData) external;
// ERC777のイベント
event Sent(address indexed operator, address indexed from, address indexed to, uint256 amount, bytes data, bytes operatorData);
event Minted(address indexed operator, address indexed to, uint256 amount, bytes data, bytes operatorData);
event Burned(address indexed operator, address indexed from, uint256 amount, bytes data, bytes operatorData);
event AuthorizedOperator(address indexed operator, address indexed tokenHolder);
event RevokedOperator(address indexed operator, address indexed tokenHolder);
}
2.3 ERC20とERC777の共通点
共通の機能
// 1. 基本的なトークン機能
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address to, uint256 amount) external returns (bool);
// 2. 承認システム
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
// 3. 基本的なイベント
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
共通の設計思想
// 1. 同質化トークンの特性
// - 同じ価値を持つ
// - 交換可能
// - 数量で管理
// 2. 残高管理システム
mapping(address => uint256) private _balances;
// 3. 承認システム
mapping(address => mapping(address => uint256)) private _allowances;
2.4 ERC20とERC777の相違点
主要な相違点
| 機能 | ERC20 | ERC777 |
|---|---|---|
| 基本機能 | 基本的なトークン機能のみ | ERC20の機能 + 拡張機能 |
| オペレーター機能 | なし | あり(第三者による操作可能) |
| フック機能 | なし | あり(送金前後の処理) |
| データ送信 | なし | あり(送金時にデータ添付可能) |
| バーン機能 | なし(実装次第) | 標準化されたバーン機能 |
| メタデータ | オプション | 必須(name, symbol, granularity) |
オペレーター機能の違い
// ERC20: 承認ベースのシステム
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(address from, address to, uint256 amount) external returns (bool);
// ERC777: オペレーターベースのシステム
function authorizeOperator(address operator) external;
function revokeOperator(address operator) external;
function operatorSend(address from, address to, uint256 amount, bytes calldata userData, bytes calldata operatorData) external;
フック機能の違い
// ERC20: フック機能なし
function transfer(address to, uint256 amount) external returns (bool);
// ERC777: フック機能あり
interface IERC777Recipient {
function tokensReceived(address operator, address from, address to, uint256 amount, bytes calldata userData, bytes calldata operatorData) external;
}
interface IERC777Sender {
function tokensToSend(address operator, address from, address to, uint256 amount, bytes calldata userData, bytes calldata operatorData) external;
}
2.5 ERC777の実装例
contract AdvancedERC777 is IERC777 {
mapping(address => uint256) private _balances;
mapping(address => mapping(address => bool)) private _operators;
mapping(address => mapping(address => uint256)) private _allowances;
uint256 private _totalSupply;
string private _name;
string private _symbol;
uint256 private _granularity;
address[] private _defaultOperators;
constructor(string memory name_, string memory symbol_, uint256 granularity_) {
_name = name_;
_symbol = symbol_;
_granularity = granularity_;
_totalSupply = 1000000 * granularity_;
_balances[msg.sender] = _totalSupply;
}
function send(address to, uint256 amount, bytes calldata userData) external override {
_send(msg.sender, msg.sender, to, amount, userData, "", true);
}
function operatorSend(address from, address to, uint256 amount, bytes calldata userData, bytes calldata operatorData) external override {
require(isOperatorFor(msg.sender, from), "ERC777: caller is not an operator");
_send(msg.sender, from, to, amount, userData, operatorData, true);
}
function _send(address operator, address from, address to, uint256 amount, bytes memory userData, bytes memory operatorData, bool requireReceptionAck) internal {
require(from != address(0), "ERC777: send from the zero address");
require(to != address(0), "ERC777: send to the zero address");
// 送信前フック
_callTokensToSend(operator, from, to, amount, userData, operatorData);
_balances[from] -= amount;
_balances[to] += amount;
emit Sent(operator, from, to, amount, userData, operatorData);
// 受信後フック
_callTokensReceived(operator, from, to, amount, userData, operatorData, requireReceptionAck);
}
function _callTokensToSend(address operator, address from, address to, uint256 amount, bytes memory userData, bytes memory operatorData) private {
address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(from, _TOKENS_SENDER_INTERFACE_HASH);
if (implementer != address(0)) {
IERC777Sender(implementer).tokensToSend(operator, from, to, amount, userData, operatorData);
}
}
function _callTokensReceived(address operator, address from, address to, uint256 amount, bytes memory userData, bytes memory operatorData, bool requireReceptionAck) private {
address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(to, _TOKENS_RECIPIENT_INTERFACE_HASH);
if (implementer != address(0)) {
IERC777Recipient(implementer).tokensReceived(operator, from, to, amount, userData, operatorData);
} else if (requireReceptionAck) {
require(!to.isContract(), "ERC777: token recipient contract has no implementer for ERC777TokensRecipient");
}
}
}
3. ERC721とERC1155の比較分析
3.1 ERC721の基本構造
ERC721の必須関数
interface IERC721 {
// 必須関数
function balanceOf(address owner) external view returns (uint256 balance);
function ownerOf(uint256 tokenId) external view returns (address owner);
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
function safeTransferFrom(address from, address to, uint256 tokenId) external;
function transferFrom(address from, address to, uint256 tokenId) external;
function approve(address to, uint256 tokenId) external;
function setApprovalForAll(address operator, bool approved) external;
function getApproved(uint256 tokenId) external view returns (address operator);
function isApprovedForAll(address owner, address operator) external view returns (bool);
// 必須イベント
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
}
ERC721の実装例
contract SimpleERC721 is IERC721 {
mapping(uint256 => address) private _owners;
mapping(address => uint256) private _balances;
mapping(uint256 => address) private _tokenApprovals;
mapping(address => mapping(address => bool)) private _operatorApprovals;
string public name;
string public symbol;
constructor(string memory _name, string memory _symbol) {
name = _name;
symbol = _symbol;
}
function mint(address to, uint256 tokenId) public {
require(to != address(0), "ERC721: mint to the zero address");
require(!_exists(tokenId), "ERC721: token already minted");
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(address(0), to, tokenId);
}
function transferFrom(address from, address to, uint256 tokenId) public override {
require(_isApprovedOrOwner(msg.sender, tokenId), "ERC721: transfer caller is not owner nor approved");
_transfer(from, to, tokenId);
}
function _transfer(address from, address to, uint256 tokenId) internal {
require(ownerOf(tokenId) == from, "ERC721: transfer from incorrect owner");
require(to != address(0), "ERC721: transfer to the zero address");
_approve(address(0), tokenId);
_balances[from] -= 1;
_balances[to] += 1;
_owners[tokenId] = to;
emit Transfer(from, to, tokenId);
}
}
3.2 ERC1155の基本構造
ERC1155の必須関数
interface IERC1155 {
// 必須関数
function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external;
function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external;
function balanceOf(address account, uint256 id) external view returns (uint256);
function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) external view returns (uint256[] memory);
function setApprovalForAll(address operator, bool approved) external;
function isApprovedForAll(address account, address operator) external view returns (bool);
// 必須イベント
event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value);
event TransferBatch(address indexed operator, address indexed from, address indexed to, uint256[] ids, uint256[] values);
event ApprovalForAll(address indexed account, address indexed operator, bool approved);
event URI(string value, uint256 indexed id);
}
ERC1155の実装例
contract MultiTokenERC1155 is IERC1155 {
mapping(uint256 => mapping(address => uint256)) private _balances;
mapping(address => mapping(address => bool)) private _operatorApprovals;
mapping(uint256 => string) private _uris;
string private _uri;
constructor(string memory uri_) {
_setURI(uri_);
}
function mint(address to, uint256 id, uint256 amount, bytes memory data) public {
require(to != address(0), "ERC1155: mint to the zero address");
address operator = msg.sender;
_balances[id][to] += amount;
emit TransferSingle(operator, address(0), to, id, amount);
_doSafeTransferAcceptanceCheck(operator, address(0), to, id, amount, data);
}
function safeTransferFrom(address from, address to, uint256 id, uint256 amount, bytes calldata data) external override {
require(from == msg.sender || isApprovedForAll(from, msg.sender), "ERC1155: caller is not owner nor approved");
require(to != address(0), "ERC1155: transfer to the zero address");
address operator = msg.sender;
uint256 fromBalance = _balances[id][from];
require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
_balances[id][from] = fromBalance - amount;
_balances[id][to] += amount;
emit TransferSingle(operator, from, to, id, amount);
_doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data);
}
function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external override {
require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch");
require(from == msg.sender || isApprovedForAll(from, msg.sender), "ERC1155: transfer caller is not owner nor approved");
require(to != address(0), "ERC1155: transfer to the zero address");
address operator = msg.sender;
for (uint256 i = 0; i < ids.length; i++) {
uint256 id = ids[i];
uint256 amount = amounts[i];
uint256 fromBalance = _balances[id][from];
require(fromBalance >= amount, "ERC1155: insufficient balance for transfer");
_balances[id][from] = fromBalance - amount;
_balances[id][to] += amount;
}
emit TransferBatch(operator, from, to, ids, amounts);
_doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data);
}
}
3.3 ERC721とERC1155の共通点
共通の機能
// 1. 基本的なNFT機能
function balanceOf(address owner) external view returns (uint256 balance);
function ownerOf(uint256 tokenId) external view returns (address owner); // ERC721のみ
// 2. 転送機能
function safeTransferFrom(address from, address to, uint256 tokenId, bytes calldata data) external;
function transferFrom(address from, address to, uint256 tokenId) external;
// 3. 承認システム
function setApprovalForAll(address operator, bool approved) external;
function isApprovedForAll(address owner, address operator) external view returns (bool);
// 4. 基本的なイベント
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId);
event ApprovalForAll(address indexed owner, address indexed operator, bool approved);
共通の設計思想
// 1. 非同期化トークンの特性
// - 固有の価値を持つ
// - 交換不可能
// - 個別に管理
// 2. 所有者管理システム
mapping(uint256 => address) private _owners; // ERC721
mapping(uint256 => mapping(address => uint256)) private _balances; // ERC1155
// 3. 承認システム
mapping(address => mapping(address => bool)) private _operatorApprovals;
3.4 ERC721とERC1155の相違点
主要な相違点
| 機能 | ERC721 | ERC1155 |
|---|---|---|
| トークン種類 | 非同質化トークンのみ | 同質化・非同質化両対応 |
| 数量管理 | 1個ずつ(固有ID) | 複数個(数量指定可能) |
| バッチ転送 | なし | あり(複数トークン同時転送) |
| URI管理 | tokenURI関数 | URIイベント |
| メタデータ | 個別のURI | 統合されたURIシステム |
| ガス効率 | 低い(個別処理) | 高い(バッチ処理) |
数量管理の違い
// ERC721: 1個ずつの管理
mapping(uint256 => address) private _owners; // 1トークン = 1所有者
function ownerOf(uint256 tokenId) external view returns (address owner);
// ERC1155: 数量での管理
mapping(uint256 => mapping(address => uint256)) private _balances; // トークンID => 所有者 => 数量
function balanceOf(address account, uint256 id) external view returns (uint256);
バッチ処理の違い
// ERC721: 個別処理のみ
function transferFrom(address from, address to, uint256 tokenId) external;
// 複数転送時は個別に実行する必要がある
// ERC1155: バッチ処理対応
function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external;
// 複数トークンを一度に転送可能
URI管理の違い
// ERC721: 個別のURI
function tokenURI(uint256 tokenId) external view returns (string memory) {
require(_exists(tokenId), "ERC721Metadata: URI query for nonexistent token");
return string(abi.encodePacked(_baseURI(), tokenId.toString()));
}
// ERC1155: 統合URIシステム
event URI(string value, uint256 indexed id);
function uri(uint256 id) public view returns (string memory) {
return string(abi.encodePacked(_uri, id.toString()));
}
4. 実装の詳細比較
4.1 ガス効率の比較
ERC20 vs ERC777
// ERC20: シンプルな実装
function transfer(address to, uint256 amount) external returns (bool) {
_balances[msg.sender] -= amount;
_balances[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
// ERC777: フック機能付き(ガス消費が多い)
function send(address to, uint256 amount, bytes calldata userData) external {
_send(msg.sender, msg.sender, to, amount, userData, "", true);
// フック関数の呼び出しでガス消費が増加
}
ERC721 vs ERC1155
// ERC721: 個別処理(ガス効率が低い)
function batchTransfer(address[] calldata to, uint256[] calldata tokenIds) external {
for (uint256 i = 0; i < tokenIds.length; i++) {
transferFrom(msg.sender, to[i], tokenIds[i]);
}
}
// ERC1155: バッチ処理(ガス効率が高い)
function safeBatchTransferFrom(address from, address to, uint256[] calldata ids, uint256[] calldata amounts, bytes calldata data) external {
// 複数トークンを一度に処理
}
4.2 セキュリティの比較
フック機能のセキュリティ
// ERC777: フック機能によるセキュリティリスク
contract MaliciousRecipient {
function tokensReceived(address operator, address from, address to, uint256 amount, bytes calldata userData, bytes calldata operatorData) external {
// 悪意のあるコードを実行可能
IERC777(msg.sender).transfer(operator, amount);
}
}
// 対策: フック機能の適切な実装
contract SecureERC777 {
function _callTokensReceived(address operator, address from, address to, uint256 amount, bytes memory userData, bytes memory operatorData, bool requireReceptionAck) private {
address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(to, _TOKENS_RECIPIENT_INTERFACE_HASH);
if (implementer != address(0)) {
IERC777Recipient(implementer).tokensReceived(operator, from, to, amount, userData, operatorData);
} else if (requireReceptionAck) {
require(!to.isContract(), "ERC777: token recipient contract has no implementer for ERC777TokensRecipient");
}
}
}
4.3 互換性の比較
下位互換性
// ERC777はERC20と互換性がある
contract ERC777Compatible is IERC777 {
// ERC20の関数も実装
function transfer(address to, uint256 amount) external override returns (bool) {
send(to, amount, "");
return true;
}
function approve(address spender, uint256 amount) external override returns (bool) {
// ERC20の承認システム
_allowances[msg.sender][spender] = amount;
emit Approval(msg.sender, spender, amount);
return true;
}
}
// ERC1155はERC721と部分的に互換性がある
contract ERC1155Compatible is IERC1155 {
// ERC721風の関数を実装
function ownerOf(uint256 tokenId) external view returns (address) {
require(balanceOf(msg.sender, tokenId) > 0, "ERC1155: query for nonexistent token");
return msg.sender; // 簡略化
}
}
5. 使用場面の比較
5.1 ERC20 vs ERC777の使用場面
ERC20が適している場面
// 1. シンプルなトークン
contract SimpleToken is ERC20 {
constructor() ERC20("Simple Token", "STK") {
_mint(msg.sender, 1000000 * 10**18);
}
}
// 2. 基本的なDeFiプロトコル
contract BasicDeFi {
IERC20 public token;
function deposit(uint256 amount) external {
token.transferFrom(msg.sender, address(this), amount);
}
}
ERC777が適している場面
// 1. 高度な機能が必要なトークン
contract AdvancedToken is ERC777 {
constructor() ERC777("Advanced Token", "ADV", new address[](0)) {
_mint(msg.sender, 1000000 * 10**18, "", "");
}
}
// 2. 自動化された処理
contract AutomatedContract {
function onTokensReceived(address operator, address from, address to, uint256 amount, bytes calldata userData, bytes calldata operatorData) external {
// トークン受信時の自動処理
if (amount > 1000) {
// 大きな送金時の特別処理
}
}
}
5.2 ERC721 vs ERC1155の使用場面
ERC721が適している場面
// 1. アート作品のNFT
contract ArtNFT is ERC721 {
struct Artwork {
string title;
string artist;
uint256 creationDate;
string ipfsHash;
}
mapping(uint256 => Artwork) public artworks;
function mintArtwork(address to, uint256 tokenId, Artwork memory artwork) external {
_mint(to, tokenId);
artworks[tokenId] = artwork;
}
}
// 2. ゲームのキャラクター
contract GameCharacter is ERC721 {
struct Character {
uint256 level;
uint256 experience;
string name;
uint256[] abilities;
}
mapping(uint256 => Character) public characters;
}
ERC1155が適している場面
// 1. ゲームアイテム(複数種類)
contract GameItems is ERC1155 {
// アイテムID: 1=剣, 2=盾, 3=薬, 4=宝石
function mintWeapon(address to, uint256 amount) external {
_mint(to, 1, amount, ""); // 剣をamount個
}
function mintPotion(address to, uint256 amount) external {
_mint(to, 3, amount, ""); // 薬をamount個
}
// バッチで複数アイテムを同時に転送
function transferItems(address to, uint256[] calldata ids, uint256[] calldata amounts) external {
safeBatchTransferFrom(msg.sender, to, ids, amounts, "");
}
}
// 2. マルチトークンプラットフォーム
contract MultiTokenPlatform is ERC1155 {
// 同質化トークン(ID: 1-100)
// 非同質化トークン(ID: 101-1000)
function mintFungibleToken(address to, uint256 id, uint256 amount) external {
require(id >= 1 && id <= 100, "Invalid fungible token ID");
_mint(to, id, amount, "");
}
function mintNFT(address to, uint256 id) external {
require(id >= 101 && id <= 1000, "Invalid NFT ID");
_mint(to, id, 1, ""); // NFTは数量1
}
}
6. 学習の成果
6.1 習得した概念
- ERC規格の理解: 各ERC規格の目的と特徴の把握
- 同質化トークン: ERC20とERC777の機能比較と実装方法
- 非同質化トークン: ERC721とERC1155の機能比較と実装方法
- 互換性: 規格間の互換性と移行方法
- ガス効率: 各規格のガス消費量と最適化方法
- セキュリティ: 各規格のセキュリティ考慮事項
6.2 実装スキル
- ERC20トークンの基本的な実装
- ERC777トークンの高度な機能実装
- ERC721NFTの個別管理システム
- ERC1155マルチトークンの統合管理システム
- フック機能の安全な実装
- バッチ処理の効率的な実装
6.3 技術的な理解
- 同質化と非同質化の概念と実装の違い
- オペレーター機能による権限管理
- フック機能による自動化処理
- バッチ処理によるガス効率の向上
- URI管理によるメタデータの統合
- 互換性による既存システムとの連携
7. 今後の学習への応用
7.1 発展的な機能
- ERC1155の拡張: より複雑なマルチトークンシステム
- フック機能の活用: 自動化されたDeFiプロトコル
- メタデータ管理: IPFSとの連携による分散ストレージ
- ガス最適化: バッチ処理による効率化
7.2 実用的な応用
- DeFiプロトコル: トークン標準を活用した金融アプリケーション
- ゲーム開発: NFTとマルチトークンを活用したゲームアイテム
- アートプラットフォーム: デジタルアートのNFT化
- サプライチェーン: トークン化による資産管理
7.3 セキュリティの向上
- フック機能の安全な実装: リエントランシー攻撃への対策
- 権限管理の強化: オペレーター機能の適切な使用
- メタデータの検証: URIの信頼性確保
- バッチ処理の安全性: 配列の境界チェック
参考:
Discussion