👌

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.frompayment.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 習得した概念

  1. マッピングの実用: 実用的なスマートコントラクトでの活用
  2. ETH管理: コントラクトでのETHの受取・送金・管理
  3. 個別残高管理: mapping(address => uint)によるユーザー別残高の追跡
  4. リエントランシー対策: セキュアな引き出し処理の実装
  5. 構造体: 複合データの効率的な管理
  6. Child Smart Contract: 動的なコントラクトインスタンスの作成
  7. 設計パターン: 異なるアプローチの比較と選択

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