👌
Solidity基礎学習8日目(マッピングと構造体)
Solidity基礎学習 - マッピングと構造体の実践的理解
日付: 2025年8月29日
学習内容: Solidityのマッピング(Mapping)の実用的な応用と構造体(Struct)の活用方法について
1. マッピングの実用的な応用
014_Mapping2.solの概要
Solidityのマッピングデータ構造を活用した、ETHの送金・受取・管理機能について学習します。実用的なスマートコントラクトの設計パターンを理解します。
重要なポイント
コントラクトの基本構造
contract ExampleMappingWithdrawals{
mapping(address => uint) public balanceReceived;
function sendMoney() public payable {
balanceReceived[msg.sender] += msg.value;
}
function getBalance() public view returns(uint){
return address(this).balance;
}
function withdrawAllMoney(address payable _to) public{
uint balanceToSentOut = balanceReceived[msg.sender];
balanceReceived[msg.sender] = 0;
_to.transfer(balanceToSentOut);
}
}
コントラクトの特徴:
-
個別残高管理:
mapping(address => uint)
による各ユーザーの送金残高の追跡 -
ETH受取機能:
payable
修飾子によるETHの受け取りと個別残高の更新 - 残高確認: コントラクトの現在のETH残高を取得
- 安全な引き出し: リエントランシー攻撃対策を実装した引き出し機能
- ユーザー別管理: 各ユーザーが送金した金額のみを引き出し可能
2. 各機能の詳細分析
2.1 ETH受取機能(sendMoney)
実装コード
function sendMoney() public payable {
balanceReceived[msg.sender] += msg.value;
}
機能の説明:
-
public payable
: 外部からETHを受け取ることができる関数 -
payable
修飾子: 関数がETHを受け取れることを示す -
個別残高管理:
balanceReceived[msg.sender]
で送金者の残高を追跡 -
累積加算:
+= msg.value
で送金金額を既存の残高に加算
使用例:
// 外部からETHを送金
contract.sendMoney{value: 1 ether}();
// 送金者のアドレスと送金額
// msg.sender: 送金者のアドレス
// msg.value: 送金されたETHの量(wei単位)
2.2 残高確認機能(getBalance)
個別残高の確認
mapping(address => uint) public balanceReceived;
// 特定のアドレスの送金残高を確認
uint userBalance = contract.balanceReceived(userAddress);
個別残高管理の特徴:
- ユーザー別追跡: 各アドレスの送金残高を個別に管理
-
自動生成getter:
public
修飾子により自動的にgetter関数が生成 - 累積管理: 複数回の送金が累積される
実装コード
function getBalance() public view returns(uint){
return address(this).balance;
}
機能の説明:
-
address(this)
: 現在のコントラクトのアドレスを指す -
.balance
: そのアドレスが保持するETHの残高 -
view
修飾子: 状態を変更しない読み取り専用関数 -
戻り値:
uint
型でETH残高を返す(wei単位)
使用例:
// コントラクトの残高を確認
uint currentBalance = contract.getBalance();
// 例: 1000000000000000000 (1 ETH = 10^18 wei)
2.3 全額引き出し機能(withdrawAllMoney)
実装コード
function withdrawAllMoney(address payable _to) public{
uint balanceToSentOut = balanceReceived[msg.sender];
balanceReceived[msg.sender] = 0;
_to.transfer(balanceToSentOut);
}
機能の説明:
-
address payable _to
: ETHを受け取れるアドレス型のパラメータ -
balanceReceived[msg.sender]
: 送金者の個別残高を取得 - リエントランシー対策: 先に残高を0にしてから送金を実行
- 個別残高送金: 送金者が送金した金額のみを引き出し可能
使用例:
// 送金者の個別残高を引き出し
address payable recipient = 0x1234...; // 受取アドレス
contract.withdrawAllMoney(recipient);
// 送金者の個別残高が0になり、指定アドレスに送金される
// リエントランシー攻撃を防ぐため、先に残高を0にしてから送金
3. 構造体(Struct)の活用
015_Struct.solの概要
Solidityの構造体(Struct)を使用したデータ管理と、Child Smart Contractとの比較について学習します。両方のアプローチの利点と欠点を理解します。
重要なポイント
2つの実装アプローチ
アプローチ1: Child Smart Contract
contract Wallet{
PaymentReceived public payment;
function payContract() public payable{
payment = new PaymentReceived(msg.sender, msg.value);
}
}
contract PaymentReceived{
address public from;
uint public amount;
constructor(address _from, uint _amount){
from = _from;
amount = _amount;
}
}
アプローチ2: 構造体(Struct)
contract Wallet2 {
struct PaymentReceivedStruct {
address from;
uint amount;
}
PaymentReceivedStruct public payment;
function payContract() public payable {
payment.from = msg.sender;
payment.amount = msg.value;
}
}
4. Child Smart Contract vs 構造体の比較
4.1 Child Smart Contractの特徴
実装の詳細
function payContract() public payable{
payment = new PaymentReceived(msg.sender, msg.value);
}
new
キーワードの役割:
-
新しいインスタンス作成:
PaymentReceived
コントラクトの新しいインスタンスを動的に作成 - 動的生成: 関数が呼ばれるたびに新しいコントラクトが作成される
- 独立性: 各支払いが独立したコントラクトとして存在
メリット:
- 独立性: 各支払い情報が独立したコントラクトとして管理
- 拡張性: 将来的に機能を追加しやすい
- 再利用性: 他のコントラクトでも使用可能
- 分離: 支払い処理のロジックを完全に分離
デメリット:
- ガス代が高い: 新しいコントラクトを作成するため
- 履歴の損失: 前の支払い情報が失われる
- 複雑性: 複数のコントラクトの管理が必要
4.2 構造体(Struct)の特徴
実装の詳細
function payContract() public payable {
payment.from = msg.sender;
payment.amount = msg.value;
}
構造体の特徴:
- 単一コントラクト: 1つのコントラクト内で完結
-
直接アクセス:
payment.from
やpayment.amount
で直接アクセス - シンプル: 複雑なコントラクト間の関係がない
メリット:
- ガス代が安い: 新しいコントラクトを作成しない
- 直接アクセス: 構造体のフィールドに直接アクセス可能
- シンプル: 1つのコントラクト内で完結
- 高速: 外部コントラクト呼び出しがない
デメリット:
- 履歴の制限: 1つの支払い情報のみ保持
- 拡張性: 機能追加時に構造体の変更が必要
- 独立性: 支払い処理のロジックが分離されていない
5. 実装のポイント
5.1 セキュリティ設計
基本的なセキュリティ考慮事項
function withdrawAllMoney(address payable _to) public{
// 適切な検証を追加
require(_to != address(0), "Invalid recipient address");
require(getBalance() > 0, "No balance to withdraw");
_to.transfer(getBalance());
}
セキュリティの考慮点:
- アドレスの検証: 有効なアドレスかどうかの確認
- 残高の確認: 引き出し可能な残高があるかの確認
- リエントランシー対策: 先に状態を更新してから送金を実行
- 個別残高管理: 各ユーザーが自分の送金分のみ引き出し可能
- エラーハンドリング: 適切なエラーメッセージの提供
5.2 ガスコストの最適化
効率的な実装
// 読み取り専用関数はview修飾子を使用
function getBalance() public view returns(uint) {
return address(this).balance;
}
// 状態変更は必要最小限に
function payContract() public payable {
payment.from = msg.sender;
payment.amount = msg.value;
}
最適化のポイント:
-
view
修飾子: 状態変更しない関数でのガス節約 - 効率的な状態更新: 必要最小限の処理のみ実行
- 適切な関数設計: 各機能を適切に分離
6. 実用的な応用
6.1 拡張可能な機能
権限管理の追加
contract SecureWallet {
mapping(address => bool) public authorizedUsers;
address public owner;
constructor() {
owner = msg.sender;
authorizedUsers[msg.sender] = true;
}
modifier onlyAuthorized() {
require(authorizedUsers[msg.sender], "Not authorized");
_;
}
function withdrawAllMoney(address payable _to) public onlyAuthorized {
require(_to != address(0), "Invalid recipient address");
_to.transfer(getBalance());
}
}
拡張機能:
- 権限管理: 承認されたユーザーのみが利用可能
- 所有者制御: 管理者によるユーザー管理
- セキュリティ強化: 未承認ユーザーからの操作を拒否
6.2 より柔軟な支払い管理
複数の支払い情報の管理
struct PaymentInfo {
address from;
uint amount;
uint timestamp;
string description;
}
mapping(uint => PaymentInfo) public payments;
uint public paymentCount;
function recordPayment(string memory _description) public payable {
payments[paymentCount] = PaymentInfo(
msg.sender,
msg.value,
block.timestamp,
_description
);
paymentCount++;
}
複合データの利点:
- 柔軟性: 複数の情報を1つの構造体で管理
- 効率性: 関連データの一元管理
- 拡張性: 新しいフィールドの追加が容易
7. 学習の成果
7.1 習得した概念
- マッピングの実用: 実用的なスマートコントラクトでの活用
- ETH管理: コントラクトでのETHの受取・送金・管理
-
個別残高管理:
mapping(address => uint)
によるユーザー別残高の追跡 - リエントランシー対策: セキュアな引き出し処理の実装
- 構造体: 複合データの効率的な管理
- Child Smart Contract: 動的なコントラクトインスタンスの作成
- 設計パターン: 異なるアプローチの比較と選択
7.2 実装スキル
- ETH管理機能の実装能力
- 個別残高管理の実装とマッピングの活用
- リエントランシー攻撃対策の実装
- 構造体を使用したデータ管理
- Child Smart Contractの設計と実装
- セキュリティを考慮したETH操作
7.3 技術的な理解
-
payable
修飾子: ETHを受け取る関数の実装 -
mapping(address => uint)
: ユーザー別残高の効率的な管理 - リエントランシー対策: 状態更新→送金の順序によるセキュリティ確保
-
new
キーワード: 動的なコントラクトインスタンスの作成 - 構造体: 複合データの効率的な管理方法
- ガス最適化: 効率的なコントラクト設計
8. 今後の学習への応用
8.1 発展的な機能
- 複数の支払い履歴: 構造体配列を使用した履歴管理
- 条件付き引き出し: 特定の条件を満たした場合のみ引き出し可能
- 手数料システム: 取引手数料の自動計算と徴収
8.2 セキュリティの向上
- リエントランシー攻撃: 再入攻撃への対策
- オーバーフロー: 数値計算の安全性確保
- アクセス制御: 適切な権限管理の実装
8.3 パフォーマンスの最適化
- ガスコスト: 効率的なETH操作
- メモリ管理: 適切なデータ構造の選択
- スケーラビリティ: 大量の支払い情報の効率的な処理
9. 実践的な応用例
9.1 マルチシグウォレット
contract MultiSigWallet {
struct Transaction {
address to;
uint amount;
bool executed;
uint confirmations;
}
mapping(uint => Transaction) public transactions;
mapping(address => bool) public owners;
uint public requiredConfirmations;
function submitTransaction(address _to, uint _amount) public returns(uint) {
// トランザクションの提出
}
function confirmTransaction(uint _txId) public {
// トランザクションの承認
}
}
9.2 クラウドファンディング
contract Crowdfunding {
struct Campaign {
address creator;
uint goal;
uint raised;
uint deadline;
bool closed;
}
mapping(uint => Campaign) public campaigns;
function contribute(uint _campaignId) public payable {
// 寄付の処理
}
function withdrawFunds(uint _campaignId) public {
// 資金の引き出し
}
}
参考:
Discussion