2022.04.06
ERC20標準に従ってスマートコントラクトを開発からデプロイまで実装してみる
こんにちは。次世代システム研究室のN.Q.Vです。
最近、スマートコントラクトの話がよく目に入ってくるので、重い腰を上げて僕も触ってみることにしました。今回、開発からテストネットワックにデプロイまで実装しましょう。
1. フレームワークを選ぶ
スマートコントラクトをコーディングするためのフレームワークに関しては、 Truffleは流行しているかとお思いますが、Hardhatを選びました。
理由はHardhat 自体が Ethereum 互換のネットワーク(ローカルイーサリアムネットワーク)を構築できるため、Hardhat のみで Solidity で作ったスマートコントラクトのコンパイル・テスト・デプロイが可能です。
Truffleだと、ローカルイーサリアムネットワークを構成するために、Ganacheをインストールするのは必要です。
2. 開発を着手
2.1 ERC20標準に従ってスマートコントラクトを実装
ERC20標準に従ってスマートコントラクトを構築するには、以下の2つの方法があります。
- 方法① : 「OpenZeppelin」を使って実装
- 方法② : 自分でinterface ERC20を作成( EIP-20: Token Standardでメソッドを参考)
初心者の方は、方法②で進めた方がいいと思います。各メソッドを自分で書くと、メソッドの意味とメソッドの使い方をよく覚えて理解できると思います。
では、interface ERC20を作りましょう。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; interface IERC20 { event Transfer(address indexed _from, address indexed _to, uint256 _value); event Approval( address indexed _owner, address indexed _spender, uint256 _value ); function totalSupply() external view returns (uint256); function balanceOf(address _owner) external view returns (uint256 balance); function transfer(address _to, uint256 _value) external returns (bool success); function transferFrom( address _from, address _to, uint256 _value ) external returns (bool success); function approve(address _spender, uint256 _value) external returns (bool success); function allowance(address _owner, address _spender) external view returns (uint256 remaining); }
次、interface ERC721からSample Tokenを作りましょう。 ここでは、ERC20で作成されたすべてのメソッドを呼び出します。
// SPDX-License-Identifier: MIT pragma solidity ^0.8.4; import "./IERC20.sol"; contract SampleToken is IERC20 { constructor() { _totalSupply = 1000000; _balances[msg.sender] = 1000000; } uint256 private _totalSupply; //mapping{address} -> balance mapping(address => uint256) private _balances; //_allowances[sender][spender] = allowances mapping(address => mapping(address => uint256)) private _allowances; function totalSupply() public view override returns (uint256) { return _totalSupply; } function balanceOf(address account) public view override returns (uint256) { return _balances[account]; } function transfer(address recipient, uint256 amount) public override returns (bool) { require(_balances[msg.sender] >= amount); _balances[msg.sender] -= amount; _balances[recipient] += amount; emit Transfer(msg.sender, recipient, amount); return true; } function transferFrom( address sender, address recipient, uint256 amount ) public override returns (bool) { require(_balances[sender] >= amount); require(_allowances[sender][msg.sender] >= amount); _balances[sender] -= amount; _balances[recipient] += amount; emit Transfer(sender, recipient, amount); return true; } function approve(address spender, uint256 amount) public override returns (bool) { _allowances[msg.sender][spender] = amount; emit Approval(msg.sender, spender, amount); return true; } function allowance(address owner, address spender) public view override returns (uint256) { return _allowances[owner][spender]; } }
2.2 UnitTestを開発
スマートコントラクトを構築する場合、 UnitTestを作成することは非常に重要です。 スマートコントラクトを作成するより、UnitTestを作成する方が時間がかかる場合があります。 セキュリティ漏れる問題が発生すれば、多くのお金を失う可能性があります。
const { expect } = require("chai"); const { ethers } = require("hardhat"); describe("ERC20-BEP20 sample token", function () { let [accountA, accountB, accountC] = []; let token; const amount = 100; const totalSupply = 1000000; beforeEach(async () => { [accountA, accountB, accountC] = await ethers.getSigners(); const Token = await ethers.getContractFactory("SampleToken"); token = await Token.deploy(); await token.deployed(); }); describe("common", function () { it("total supply should return righr value", async function () { expect(await token.totalSupply()).to.be.equal(totalSupply); }); it("balance of account A should return righr value", async function () { expect(await token.balanceOf(accountA.address)).to.be.equal(totalSupply); }); it("balance of account B should return righr value", async function () { expect(await token.balanceOf(accountB.address)).to.be.equal(0); }); it("allowance of account A to account B should return righr value", async function () { expect( await token.allowance(accountA.address, accountB.address) ).to.be.equal(0); }); }); describe("transfer", function () { it("transfer should revert if amount exceeds balance", async function () { await expect(token.transfer(accountB.address, totalSupply + 1)).to.be .reverted; }); it("transfer should work correctly", async function () { const transferTx = await token.transfer(accountB.address, amount); expect(await token.balanceOf(accountA.address)).to.be.equal( totalSupply - amount ); expect(await token.balanceOf(accountB.address)).to.be.equal(amount); await expect(transferTx) .to.emit(token, "Transfer") .withArgs(accountA.address, accountB.address, amount); }); }); describe("transferFrom", function () { it("transferFrom should revert if amount exceeds balance", async function () { await expect( token .connect(accountB) .transferFrom(accountA.address, accountC.address, totalSupply + 1) ).to.be.reverted; }); it("transferFrom should revert if amount exceeds allowance balance", async function () { await expect( token .connect(accountB) .transferFrom(accountA.address, accountC.address, amount) ).to.be.reverted; }); }); it("transferFrom should work correctly", async function () { await token.approve(accountB.address, amount); const transferFromTx = await token .connect(accountB) .transferFrom(accountA.address, accountC.address, amount); expect(await token.balanceOf(accountA.address)).to.be.equal( totalSupply - amount ); expect(await token.balanceOf(accountC.address)).to.be.equal(amount); await expect(transferFromTx) .to.emit(token, "Transfer") .withArgs(accountA.address, accountC.address, amount); }); describe("approve", function () { it("approve should work correctly", async function () { const approveTx = await token.approve(accountB.address, amount); expect( await token.allowance(accountA.address, accountB.address) ).to.be.equal(amount); await expect(approveTx) .to.emit(token, "Approval") .withArgs(accountA.address, accountB.address, amount); }); }); });
3. rinkebyにデプロイ
3.1. hardhat.configを修正
networksのところにrinkebyを追加します。
networks: { rinkeby: { url: process.env.RINKEBY_URL || "", accounts: process.env.PRIVATE_KEY !== undefined ? [process.env.PRIVATE_KEY] : [], }, },
3.2 envを修正
ETHERSCAN_API_KEY, RINKEBY_URL, PRIVATE_KEYを発行してenvに記載します。
- ETHERSCAN_API_KEYについて、https://etherscan.io/ にて会員登録、ログインして、https://etherscan.io/myapikey にてAPIKeyを発行できます。
- RINKEBY_URL について、 「Infura」にて会員登録、ログインして、新規プロジェクトを作成して、「RINKEBY_URL」は「https://rinkeby.infura.io/v3/xxxx」になります。rinkebyにデプロイするので、ENDPOINTSは「rinkeby」を選択してください。
- PRIVATE_KEYについて、Metamaskのアカウントの秘密機を記載してください。
3.3 デプロイファイル準備
scripts/ の中にdeploy.jsファイルを作成して、main.jsに入れてください。
const SampleToken = await hre.ethers.getContractFactory("SampleToken"); const sampleToken = await SampleToken.deploy(); await sampleToken.deployed(); console.log("Sample Token deployed to", sampleToken.address);
3.4 テストとデプロイ実行
- テスト実行
npx hardhat testで実行します。結果は以下のようになります。全部のケースは「passing」だと、OKです。
- デプロイ実行
npx hardhat run –network rinkeby ./scripts/deploy.js で実行します。エラーがなければ、OKです。

- ソースをverify
npx hardhat verify “address” –network rinkebyで実行します。
成功したら、rinkebyでコントラクトのコードを確認出来ます。
コントラクトのコードの以下で確認できます。
https://rinkeby.etherscan.io/address/0x35cAC7b9Abf85Ac49d444839500f1533041ad340#code
最後に
次世代システム研究室では、グループ全体のインテグレーションを支援してくれるアーキテクトを募集しています。アプリケーション開発者の方、次世代システム研究室にご興味を持って頂ける方がいらっしゃいましたら、ぜひ募集職種一覧からご応募をお願いします。
皆さんのご応募をお待ちしています。
グループ研究開発本部の最新情報をTwitterで配信中です。ぜひフォローください。
Follow @GMO_RD