イーサリアム上で「トークンを発行したい」と考えたとき、最初に学ぶべき標準規格がERC-20です。USDCやDAI、UNIトークンなど、DeFiで使われる主要なトークンのほぼすべてがERC-20規格に準拠しています。ERC-20を理解することは、スマートコントラクト開発者として必須の知識といえます。
本記事では、ERC-20の仕様を一から解説したうえで、Solidityでのフルスクラッチ実装コードを紹介します。さらに、OpenZeppelinライブラリを活用した効率的な実装方法と、本番デプロイ前に確認すべきセキュリティポイントまで詳しく説明します。
ERC-20の実装を通じて、状態変数の管理・イベントの設計・モディファイアの使い方を実践的に学べます。コードを実際に動かしながら読み進めることをお勧めします。
ERC-20とは何か
ERC-20の仕様と必須関数
ERC-20(Ethereum Request for Comments 20)は、イーサリアム上の代替可能トークン(Fungible Token)の標準インターフェースです。2015年にFabian Vogelstellerによって提案され、現在では事実上の業界標準となっています。ERC-20に準拠したトークンはどのウォレットや取引所でも同一の方法で操作できます。
ERC-20の必須関数は以下の6つです。totalSupply()(総発行量)、balanceOf(address)(残高照会)、transfer(address to, uint256 amount)(直接送金)、allowance(address owner, address spender)(委任残高照会)、approve(address spender, uint256 amount)(送金委任)、transferFrom(address from, address to, uint256 amount)(委任送金)。これらをすべて実装することでERC-20準拠となります。
TransferとApprovalイベントの役割
ERC-20ではTransfer(送金時)とApproval(委任時)の2つのイベントを必ず発行することが規格で定められています。これらのイベントはブロックチェーン上のトランザクションログとして記録され、フロントエンドやブロックエクスプローラーがトークンの動きを追跡するために使います。
EtherscanなどのブロックエクスプローラーはTransferイベントを検索してトークンの移動履歴を表示しています。Transferイベントの発行を忘れると、ブロックエクスプローラー上でトークンの動きが見えなくなるため、必ず実装が必要です。
ERC-20のフルスクラッチ実装
基本コントラクトの構造
以下は、最小限のERC-20トークンをフルスクラッチで実装したコードです。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract MyToken {
string public name = "MyToken";
string public symbol = "MTK";
uint8 public decimals = 18;
uint256 public totalSupply;
mapping(address => uint256) private _balances;
mapping(address => mapping(address => uint256)) private _allowances;
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
constructor(uint256 initialSupply) {
totalSupply = initialSupply * 10 ** decimals;
_balances[msg.sender] = totalSupply;
emit Transfer(address(0), msg.sender, totalSupply);
}
function balanceOf(address account) public view returns (uint256) {
return _balances[account];
}
function transfer(address to, uint256 amount) public returns (bool) {
require(_balances[msg.sender] >= amount, "Insufficient balance");
require(to != address(0), "Transfer to zero address");
_balances[msg.sender] -= amount;
_balances[to] += amount;
emit Transfer(msg.sender, to, amount);
return true;
}
}
コンストラクタでinitialSupplyの分だけトークンをデプロイ者のアドレスに割り当て、Transferイベントをaddress(0)からの発行として記録しています。これはミントの慣例的な表現です。
approveとtransferFromの仕組みとユースケース
approveとtransferFromのセットはDeFiの根幹を成す仕組みです。例えばUniswapでトークンをスワップする際、ユーザーはまずUniswapのコントラクトアドレスに対してapproveで送金権限を委ねます。その後UniswapのコントラクトがtransferFromを呼び出してユーザーのトークンを引き出し、スワップを実行します。
approveの際に過剰な量(type(uint256).max)を承認する「無制限承認」は利便性が高い反面、コントラクトに脆弱性が見つかった場合に全残高を失うリスクがあります。セキュリティ意識の高いユーザーは必要最小限の量のみ承認することが推奨されます。
OpenZeppelinを使った実装
OpenZeppelinとは何か
OpenZeppelinは、スマートコントラクト開発のためのオープンソースライブラリです。ERC-20・ERC-721・アクセス制御・アップグレードパターンなど、よく使われるコントラクトのセキュリティ監査済み実装を提供しています。多くのプロジェクトがOpenZeppelinを使用しており、実際の本番環境での実績が豊富です。
npx hardhat compile前にnpm install @openzeppelin/contractsを実行しておくか、Remixのインポート機能を使ってGitHub経由でインポートします。Remix IDEでは@openzeppelin/contracts/token/ERC20/ERC20.solというパスでインポート可能です。
OpenZeppelinを継承した最小実装
OpenZeppelinのERC20コントラクトを継承するだけで、標準的なERC-20機能のすべてが利用できます。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
contract MyOZToken is ERC20, Ownable {
constructor(uint256 initialSupply) ERC20("MyOZToken", "MOZT") Ownable(msg.sender) {
_mint(msg.sender, initialSupply * 10 ** decimals());
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function burn(uint256 amount) public {
_burn(msg.sender, amount);
}
}
このコードは、mintとburn機能を持ち、mintはonlyOwnerで制限されるERC-20トークンです。OpenZeppelinが用意した_mintや_burnの内部関数を活用することで、安全でシンプルな実装が実現できます。
ERC-20実装のセキュリティ注意点
transferの戻り値チェック問題
ERC-20の仕様では、transfer・transferFromはboolを返すと規定されています。しかし、USDTのような古いトークンはboolを返さない実装になっています。これらのトークンとやり取りするコントラクトがboolの戻り値を前提に書かれていると、静的な呼び出しで問題が生じることがあります。
OpenZeppelinのSafeERC20ライブラリは、戻り値なしのトークンにも対応したsafeTransfer・safeTransferFromを提供しています。外部ERC-20トークンとやり取りするコントラクトではSafeERC20を使うことが強く推奨されます。
承認競合(Approval Race Condition)
approveにはフロントランニングによる承認競合という既知の問題があります。allowanceが100のときにゼロリセットせずに200に変更しようとすると、攻撃者が元の100を先に使ってから新しい200も使うことが理論上可能です。対策としては、一旦ゼロにしてから新しい値を設定するか、EIP-2612(permit)やincrease/decreaseAllowanceを使う方法があります。
EIP-2612のpermitは、署名によってオフチェーンでapproveを行いガスコストをゼロにする仕組みです。ユーザー体験の向上にもつながるため、最新のDeFiプロトコルでは積極的に採用されています。
デプロイとテストの進め方
Hardhatを使ったテストの書き方
Hardhatでは、JavaScriptまたはTypeScriptを使ってテストを記述します。ChaiとEthersを組み合わせて、transfer・approve・transferFromの各機能が正しく動作するかを検証します。境界値(残高不足・ゼロアドレス)のエラーケースも必ずテストに含めましょう。
テスト実行はnpx hardhat testで行います。ガスレポートを取得したい場合はHardhat Gas Reporterプラグインを使うと各関数のガス消費量を一覧で確認できます。デプロイ前にすべてのテストがパスすることを確認してからSepoliaテストネットへデプロイすることが推奨されます。
Etherscanへの検証(verify)
テストネットや本番にデプロイしたコントラクトをEtherscanに検証(verify)することで、コードをオープンソースとして公開できます。Hardhatのhardhat-etherscanプラグインを使えばnpx hardhat verifyコマンドで自動的に検証できます。検証済みコントラクトはユーザーやセキュリティ研究者がコードを確認できるため、信頼性の向上につながります。
検証にはEtherscanのAPIキーが必要です。Etherscanアカウントを作成してAPIキーを取得し、hardhat.config.jsのetherscan.apiKeyに設定します。コンストラクタ引数もverifyコマンドに正確に渡す必要があります。
まとめ
本記事では、ERC-20規格の仕様からフルスクラッチ実装、OpenZeppelinの活用、セキュリティ注意点、デプロイ・テストの流れまでを一気通貫で解説しました。ERC-20の実装を通じて、mapping・イベント・モディファイア・継承といったSolidityの基本要素を実践的に学べます。
本番環境でのトークン発行には、セキュリティ監査の実施や十分なテストが不可欠です。学習段階ではOpenZeppelinをベースにした実装から始め、徐々にコードを読み解いていくことで理解が深まります。ERC-20の次は、ERC-721(NFT)やERC-1155の実装に挑戦してみましょう。
よくある質問
ERC-20のdecimalsは必ず18にしなければなりませんか?
ERC-20の仕様ではdecimalsは必須ではなく、値は任意です。ただし、ウォレットや取引所の多くが18を前提として実装されているため、特別な理由がない限り18を使うことが推奨されます。USDCは6を採用しており、decimalsの違いがトークン間の計算に影響することもあるため注意が必要です。
OpenZeppelinのコントラクトはそのまま本番に使えますか?
OpenZeppelinのコントラクトは継続的にセキュリティ監査が行われており、高い信頼性を持っています。ただし、独自のカスタムロジックを追加した部分については別途監査が必要です。本番デプロイ前には、追加したコードを含めた全体のセキュリティレビューを実施することを強く推奨します。
ERC-20とERC-721の違いは何ですか?
ERC-20は代替可能なトークン(すべてのトークンが同一価値)、ERC-721は非代替可能なトークン(各トークンが固有のID・価値を持つNFT)の規格です。USDCやガバナンストークンはERC-20、デジタルアートやゲームアイテムはERC-721が使われています。ERC-1155は両者の特性を組み合わせたマルチトークン規格です。
※本記事は情報提供を目的としており、投資を推奨するものではありません。暗号資産への投資は元本割れのリスクがあります。投資判断はご自身の責任で行ってください。