🔖

Solidity基礎学習19日目(TokenSale, 抽象コントラクト)

に公開

Solidity基礎学習 - トークン販売システムの実装と抽象コントラクト

日付: 2025年9月13日
学習内容: トークン販売システムの実装、抽象コントラクトの活用、型キャスト、外部コントラクトとの連携について

1. トークン販売システムの基本概念

032_TokenSale.solの概要

トークン販売システムの実装について学習します。ETHとERC20トークンの交換、抽象コントラクトの活用、型キャストによる機能制限、外部コントラクトとの安全な連携について理解し、本格的なトークン販売プラットフォームを実装できるようになります。

重要なポイント

トークン販売システムの基本構造

contract DigitalTokenSale {
    // トークンの価格(Wei単位)
    uint public tokenPriceInWei = 1 ether;
    
    // 抽象コントラクトを型として使用
    ERC20 public token;
    address public tokenOwner;
    
    constructor(address _token) {
        tokenOwner = msg.sender;
        token = ERC20(_token);
    }
}

コントラクトの特徴:

  • ETHとトークンの交換: イーサリアムとERC20トークンの取引
  • 抽象コントラクト: 外部コントラクトとの安全な連携
  • 型キャスト: 機能の制限とセキュリティの向上
  • 価格管理: 柔軟なトークン価格の設定

2. 抽象コントラクトの詳細分析

2.1 抽象コントラクトの定義

実装コード

abstract contract ERC20{
    function transferFrom(address _from, address _to, uint256 _value) public virtual returns (bool success);
    function decimals() public virtual view returns(uint8);
}

機能の説明:

  • インターフェース定義: ERC20トークンの必要最小限の機能を定義
  • デプロイ不可: 抽象コントラクトは直接デプロイできない
  • 型として使用: 他のコントラクトで型として活用
  • 機能の制限: 必要な機能のみにアクセスを制限

2.2 抽象コントラクトの利点

セキュリティの向上:

// 抽象コントラクトで定義された機能のみ利用可能
token.transferFrom(tokenOwner, msg.sender, amount);
token.decimals();

// 以下のような危険な機能は利用不可
// token.selfDestruct(); // 抽象コントラクトに定義されていない
// token.transferOwnership(); // 抽象コントラクトに定義されていない

機能の制限理由:

  1. セキュリティの向上: 不要な関数へのアクセスを防止
  2. コントラクトサイズの削減: 不要な機能を削除
  3. 意図しない操作の防止: 危険な関数の実行を防止
  4. 明確な責任分離: トークン販売の責任を明確化
  5. 保守性の向上: 変更の影響範囲を限定

3. 型キャストシステムの実装

3.1 アドレスからコントラクト型へのキャスト

実装コード

constructor(address _token) {
    tokenOwner = msg.sender;
    token = ERC20(_token);
}

機能の説明:

  • 型キャスト: アドレス型をERC20型に変換
  • 機能の制限: 抽象コントラクトで定義された機能のみ利用
  • セキュリティ: 外部コントラクトの危険な機能へのアクセスを防止
  • 明確性: 使用可能な機能を明示的に定義

3.2 型キャストの仕組み

キャスト前後の比較:

// キャスト前(アドレス型)
address tokenAddress = 0x1234...;

// キャスト後(ERC20型)
ERC20 token = ERC20(tokenAddress);

// 利用可能な機能
token.transferFrom(from, to, amount); // 可能
token.decimals(); // 可能
// token.selfDestruct(); // 不可能(抽象コントラクトに定義されていない)

4. トークン購入機能の実装

4.1 購入処理の詳細

実装コード

function purchase() public payable {
    require(msg.value >= tokenPriceInWei, "Insufficient payment");
    uint tokensToTransfer = msg.value / tokenPriceInWei;
    uint remainder = msg.value - tokensToTransfer * tokenPriceInWei;
    token.transferFrom(tokenOwner, msg.sender, tokensToTransfer * 10 ** token.decimals());
    payable(msg.sender).transfer(remainder);
}

機能の説明:

  • 支払い確認: 十分なETHが送信されているかチェック
  • トークン計算: 送信されたETHから購入可能なトークン数を計算
  • 余剰計算: 端数分のETHを計算
  • トークン転送: 所有者から購入者へトークンを転送
  • 余剰返却: 端数分のETHを購入者に返却

4.2 トークン数量の調整

小数点対応:

// トークンの小数点を考慮した数量計算
uint256 adjustedAmount = tokensToTransfer * 10 ** token.decimals();

// 例:decimals() = 18の場合
// 1トークン = 1 * 10^18 = 1000000000000000000 wei
// 0.5トークン = 0.5 * 10^18 = 500000000000000000 wei

具体例:

// 1 ETH = 1トークンの場合
// 1.5 ETHを送信
// tokensToTransfer = 1.5 / 1 = 1(整数除算)
// remainder = 1.5 - 1 * 1 = 0.5 ETH
// 1トークンが転送され、0.5 ETHが返却される

5. 価格管理システムの実装

5.1 動的な価格設定

実装コード

contract FlexibleTokenSale {
    uint public tokenPriceInWei = 1 ether;
    address public tokenOwner;
    ERC20 public token;
    
    // 価格の変更機能
    function setTokenPrice(uint newPrice) public {
        require(msg.sender == tokenOwner, "Only owner can set price");
        require(newPrice > 0, "Price must be greater than zero");
        tokenPriceInWei = newPrice;
    }
}

機能の説明:

  • 価格の変更: 所有者による価格の動的変更
  • アクセス制御: 所有者のみが価格を変更可能
  • バリデーション: 価格の妥当性をチェック
  • イベント発行: 価格変更の記録

5.2 価格の計算例

様々な価格設定:

// 1 ETH = 100トークン
tokenPriceInWei = 0.01 ether;

// 1 ETH = 1000トークン
tokenPriceInWei = 0.001 ether;

// 1 ETH = 0.5トークン
tokenPriceInWei = 2 ether;

6. セキュリティ機能の実装

6.1 包括的なエラーハンドリング

実装コード

function purchase() public payable {
    require(msg.value >= tokenPriceInWei, "Insufficient payment");
    require(msg.value > 0, "Payment must be greater than zero");
    
    uint tokensToTransfer = msg.value / tokenPriceInWei;
    require(tokensToTransfer > 0, "No tokens to transfer");
    
    uint remainder = msg.value - tokensToTransfer * tokenPriceInWei;
    
    // トークンの転送
    bool success = token.transferFrom(tokenOwner, msg.sender, tokensToTransfer * 10 ** token.decimals());
    require(success, "Token transfer failed");
    
    // 余剰の返却
    if (remainder > 0) {
        (bool sent, ) = payable(msg.sender).call{value: remainder}("");
        require(sent, "Failed to send remainder");
    }
}

セキュリティの向上:

  • 支払い額の検証: 0より大きい値の確認
  • トークン数の検証: 転送するトークン数の確認
  • 転送の成功確認: トークン転送の成功を確認
  • 安全な送金: call関数を使用した安全なETH送金

6.2 リエントランシー攻撃への対策

セキュアな実装:

contract SecureTokenSale {
    bool private locked;
    
    modifier noReentrancy() {
        require(!locked, "Reentrancy detected");
        locked = true;
        _;
        locked = false;
    }
    
    function purchase() public payable noReentrancy {
        // 購入処理
    }
}

7. 外部コントラクトとの連携

7.1 複数のトークン対応

実装コード

contract MultiTokenSale {
    mapping(address => uint) public tokenPrices;
    mapping(address => address) public tokenOwners;
    
    function addToken(address tokenAddress, uint price, address owner) public {
        require(msg.sender == owner, "Only token owner can add");
        tokenPrices[tokenAddress] = price;
        tokenOwners[tokenAddress] = owner;
    }
    
    function purchaseToken(address tokenAddress) public payable {
        require(tokenPrices[tokenAddress] > 0, "Token not supported");
        require(msg.value >= tokenPrices[tokenAddress], "Insufficient payment");
        
        ERC20 token = ERC20(tokenAddress);
        uint tokensToTransfer = msg.value / tokenPrices[tokenAddress];
        uint remainder = msg.value - tokensToTransfer * tokenPrices[tokenAddress];
        
        token.transferFrom(tokenOwners[tokenAddress], msg.sender, tokensToTransfer * 10 ** token.decimals());
        
        if (remainder > 0) {
            payable(msg.sender).transfer(remainder);
        }
    }
}

機能の説明:

  • 複数トークン対応: 複数のERC20トークンの販売
  • 動的な追加: 新しいトークンの動的な追加
  • 価格管理: トークンごとの価格設定
  • 所有者管理: トークンごとの所有者管理

7.2 トークンの検証

トークンの妥当性確認:

function isValidToken(address tokenAddress) public view returns (bool) {
    try ERC20(tokenAddress).decimals() returns (uint8) {
        return true;
    } catch {
        return false;
    }
}

8. イベントログの実装

8.1 透明性の確保

実装コード

contract TransparentTokenSale {
    event TokenPurchased(
        address indexed buyer,
        address indexed token,
        uint256 amount,
        uint256 price,
        uint256 remainder
    );
    
    event PriceUpdated(
        address indexed token,
        uint256 oldPrice,
        uint256 newPrice
    );
    
    function purchase() public payable {
        // 購入処理
        emit TokenPurchased(
            msg.sender,
            address(token),
            tokensToTransfer,
            tokenPriceInWei,
            remainder
        );
    }
}

イベントの利点:

  • 透明性: すべての取引の記録
  • 監査: 外部からの取引の監査
  • フロントエンド: アプリケーションでの取引の追跡
  • 分析: 取引パターンの分析

9. ガス最適化の実装

9.1 効率的な計算

実装コード

contract GasOptimizedTokenSale {
    uint public constant TOKEN_PRICE = 1 ether;
    uint public constant DECIMALS = 18;
    
    function purchase() public payable {
        require(msg.value >= TOKEN_PRICE, "Insufficient payment");
        
        uint tokensToTransfer = msg.value / TOKEN_PRICE;
        uint remainder = msg.value % TOKEN_PRICE;
        
        // 定数を使用してガスを削減
        token.transferFrom(tokenOwner, msg.sender, tokensToTransfer * (10 ** DECIMALS));
        
        if (remainder > 0) {
            payable(msg.sender).transfer(remainder);
        }
    }
}

最適化のポイント:

  • 定数の使用: コンパイル時の最適化
  • 剰余演算: 効率的な余剰計算
  • 条件分岐の最適化: 不要な計算の削減
  • ストレージアクセスの削減: 頻繁にアクセスする値の最適化

9.2 バッチ処理の実装

複数購入の効率化:

function batchPurchase(uint256[] memory amounts) public payable {
    uint256 totalCost = 0;
    uint256 totalTokens = 0;
    
    for (uint i = 0; i < amounts.length; i++) {
        totalCost += amounts[i] * tokenPriceInWei;
        totalTokens += amounts[i];
    }
    
    require(msg.value >= totalCost, "Insufficient total payment");
    
    token.transferFrom(tokenOwner, msg.sender, totalTokens * 10 ** token.decimals());
    
    uint remainder = msg.value - totalCost;
    if (remainder > 0) {
        payable(msg.sender).transfer(remainder);
    }
}

10. 実用的な応用

10.1 初期コインオファリング(ICO)

ICOの実装:

contract ICO is ERC20, Ownable {
    uint public constant ICO_PRICE = 0.001 ether;
    uint public constant MAX_SUPPLY = 1000000 * 10**18;
    uint public constant ICO_DURATION = 30 days;
    
    uint public icoStartTime;
    uint public icoEndTime;
    bool public icoEnded = false;
    
    function startICO() public onlyOwner {
        icoStartTime = block.timestamp;
        icoEndTime = icoStartTime + ICO_DURATION;
    }
    
    function purchase() public payable {
        require(block.timestamp >= icoStartTime, "ICO not started");
        require(block.timestamp <= icoEndTime, "ICO ended");
        require(!icoEnded, "ICO already ended");
        
        uint tokensToTransfer = msg.value / ICO_PRICE;
        require(totalSupply() + tokensToTransfer <= MAX_SUPPLY, "Max supply exceeded");
        
        _mint(msg.sender, tokensToTransfer);
    }
}

10.2 デジタル商品の販売

デジタル商品の管理:

contract DigitalProductSale {
    struct Product {
        string name;
        uint256 price;
        string metadataURI;
        bool active;
    }
    
    mapping(uint256 => Product) public products;
    mapping(uint256 => mapping(address => bool)) public purchased;
    
    function purchaseProduct(uint256 productId) public payable {
        Product storage product = products[productId];
        require(product.active, "Product not active");
        require(msg.value >= product.price, "Insufficient payment");
        require(!purchased[productId][msg.sender], "Already purchased");
        
        purchased[productId][msg.sender] = true;
        
        emit ProductPurchased(msg.sender, productId, product.price);
    }
}

11. フロントエンドとの連携

11.1 Web3.jsでの使用

JavaScriptでの実装:

// トークン販売コントラクトの使用
const tokenSale = new web3.eth.Contract(ABI, contractAddress);

// トークンの購入
async function purchaseTokens(amount) {
    try {
        const result = await tokenSale.methods.purchase().send({
            from: userAddress,
            value: web3.utils.toWei(amount, 'ether')
        });
        console.log('Purchase successful:', result);
    } catch (error) {
        console.error('Purchase failed:', error);
    }
}

// トークン価格の取得
async function getTokenPrice() {
    const price = await tokenSale.methods.tokenPriceInWei().call();
    return web3.utils.fromWei(price, 'ether');
}

11.2 イベントの監視

イベントの監視:

// 購入イベントの監視
tokenSale.events.TokenPurchased({
    fromBlock: 'latest'
}, (error, event) => {
    if (error) {
        console.error('Event error:', error);
    } else {
        console.log('Token purchased:', event.returnValues);
    }
});

12. テストの実装

12.1 単体テスト

Hardhatでのテスト:

describe("TokenSale", function () {
    let tokenSale;
    let token;
    let owner;
    let buyer;
    
    beforeEach(async function () {
        [owner, buyer] = await ethers.getSigners();
        
        // トークンのデプロイ
        const Token = await ethers.getContractFactory("MyToken");
        token = await Token.deploy();
        
        // トークン販売のデプロイ
        const TokenSale = await ethers.getContractFactory("TokenSale");
        tokenSale = await TokenSale.deploy(token.address);
        
        // トークンの承認
        await token.approve(tokenSale.address, ethers.utils.parseEther("1000"));
    });
    
    it("Should allow token purchase", async function () {
        const purchaseAmount = ethers.utils.parseEther("1");
        
        await expect(tokenSale.connect(buyer).purchase({ value: purchaseAmount }))
            .to.emit(tokenSale, "TokenPurchased");
    });
});

12.2 統合テスト

複数コントラクトのテスト:

describe("TokenSale Integration", function () {
    it("Should handle multiple purchases", async function () {
        const buyers = await ethers.getSigners();
        
        for (let i = 1; i < 5; i++) {
            await tokenSale.connect(buyers[i]).purchase({
                value: ethers.utils.parseEther("1")
            });
        }
        
        const totalSupply = await token.totalSupply();
        expect(totalSupply).to.equal(ethers.utils.parseEther("4"));
    });
});

13. デプロイメントの実装

13.1 デプロイスクリプト

Hardhatでのデプロイ:

async function main() {
    const [deployer] = await ethers.getSigners();
    
    console.log("Deploying contracts with account:", deployer.address);
    
    // トークンのデプロイ
    const Token = await ethers.getContractFactory("MyToken");
    const token = await Token.deploy();
    await token.deployed();
    
    console.log("Token deployed to:", token.address);
    
    // トークン販売のデプロイ
    const TokenSale = await ethers.getContractFactory("TokenSale");
    const tokenSale = await TokenSale.deploy(token.address);
    await tokenSale.deployed();
    
    console.log("TokenSale deployed to:", tokenSale.address);
    
    // トークンの承認
    await token.approve(tokenSale.address, ethers.utils.parseEther("1000000"));
    console.log("Tokens approved for sale");
}

13.2 環境設定

環境変数の管理:

// hardhat.config.js
require("@nomiclabs/hardhat-waffle");
require("dotenv").config();

module.exports = {
    solidity: "0.8.30",
    networks: {
        sepolia: {
            url: process.env.SEPOLIA_URL,
            accounts: [process.env.PRIVATE_KEY]
        }
    }
};

14. 学習の成果

14.1 習得した概念

  1. 抽象コントラクト: 外部コントラクトとの安全な連携
  2. 型キャスト: アドレス型からコントラクト型への変換
  3. トークン販売: ETHとERC20トークンの交換システム
  4. 価格管理: 動的な価格設定と計算
  5. セキュリティ: リエントランシー攻撃への対策
  6. ガス最適化: 効率的なコントラクトの実装

14.2 実装スキル

  • 抽象コントラクトの設計と実装
  • 型キャストの活用
  • トークン販売システムの構築
  • セキュリティ機能の実装
  • ガス最適化の技術

14.3 技術的な理解

  • 抽象コントラクト: インターフェースの定義と活用
  • 型キャスト: 機能の制限とセキュリティの向上
  • トークン計算: 小数点対応と数量調整
  • 外部連携: 複数コントラクトとの安全な連携
  • イベントログ: 透明性と監査の実現

15. 今後の学習への応用

15.1 発展的な機能

  • 自動価格調整: 需要と供給に基づく価格設定
  • ステーキング機能: トークンのロックと報酬
  • 流動性プール: DEXとの連携
  • クロスチェーン: 複数ブロックチェーンでの販売

15.2 セキュリティの向上

  • マルチシグ: 複数署名による承認
  • タイムロック: 重要な操作の遅延実行
  • 緊急停止: 異常時の取引停止
  • 監査: 定期的なセキュリティ監査

15.3 パフォーマンスの最適化

  • レイヤー2: Polygon、Arbitrumでの実装
  • バッチ処理: 複数取引の効率化
  • キャッシュ: 頻繁にアクセスする値の最適化
  • 非同期処理: 外部APIとの連携

参考:

Discussion