🔖
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(); // 抽象コントラクトに定義されていない
機能の制限理由:
- セキュリティの向上: 不要な関数へのアクセスを防止
- コントラクトサイズの削減: 不要な機能を削除
- 意図しない操作の防止: 危険な関数の実行を防止
- 明確な責任分離: トークン販売の責任を明確化
- 保守性の向上: 変更の影響範囲を限定
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 習得した概念
- 抽象コントラクト: 外部コントラクトとの安全な連携
- 型キャスト: アドレス型からコントラクト型への変換
- トークン販売: ETHとERC20トークンの交換システム
- 価格管理: 動的な価格設定と計算
- セキュリティ: リエントランシー攻撃への対策
- ガス最適化: 効率的なコントラクトの実装
14.2 実装スキル
- 抽象コントラクトの設計と実装
- 型キャストの活用
- トークン販売システムの構築
- セキュリティ機能の実装
- ガス最適化の技術
14.3 技術的な理解
- 抽象コントラクト: インターフェースの定義と活用
- 型キャスト: 機能の制限とセキュリティの向上
- トークン計算: 小数点対応と数量調整
- 外部連携: 複数コントラクトとの安全な連携
- イベントログ: 透明性と監査の実現
15. 今後の学習への応用
15.1 発展的な機能
- 自動価格調整: 需要と供給に基づく価格設定
- ステーキング機能: トークンのロックと報酬
- 流動性プール: DEXとの連携
- クロスチェーン: 複数ブロックチェーンでの販売
15.2 セキュリティの向上
- マルチシグ: 複数署名による承認
- タイムロック: 重要な操作の遅延実行
- 緊急停止: 異常時の取引停止
- 監査: 定期的なセキュリティ監査
15.3 パフォーマンスの最適化
- レイヤー2: Polygon、Arbitrumでの実装
- バッチ処理: 複数取引の効率化
- キャッシュ: 頻繁にアクセスする値の最適化
- 非同期処理: 外部APIとの連携
参考:
Discussion