
こんにちは!CryptoGamesというブロックチェーンゲーム企業でエンジニアをしているかるでねです!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。
CryptoGamesは、「クリプトスペルズ」をはじめとするブロックチェーンゲームの開発・運営や、ブロックチェーン関連の開発を行っている会社です。
現在エンジニアを中心に積極採用中ですので、少しでも興味を持った方はぜひお話しだけでもできたら嬉しいです!
以下のWantedlyのサイトで会社について詳しく知ることができます!
https://www.wantedly.com/companies/cryptogames
以下のストーリーでは、実際にCryptoGamesで働いているメンバーのリアルな声を知ることができます。
https://www.wantedly.com/companies/cryptogames/stories
ぜひ上記Wantedlyからか、以下のCardeneのTwitter などから連絡してください!
今回の記事では「ERC20の預け入れと引き出し」についてまとめていきます。
本記事は、「ERC20がどんなものかわかったけど、実際にどう使うの?」という方向けの記事になっています。
ERC20を理解したエンジニアの次のステップとしては、実際にERC20を用いて何か作ることです。
ERC20トークンの預け入れと引き出しを知ることで、UniswapなどのDeFiへの理解も深まるのでぜひ学んだいってください!
前置きは早々に早速中身を確認していきましょう!
今回の記事は以下のコードをめちゃくちゃ参考にしています。
https://gist.github.com/BlockmanCodes/83d995ec9de0c4f5201c3556cba489e0
ERC20とは?

「そもそもERC20って何?」という方もいると思います。
ERC20とは、「Ethereumブロックチェーンで動作する統一されたトークン規格の1つ」です。
「統一されたトークンの規格」というのは、「共通のルールを持っているトークン」という事です。
共通のルールを持っていないと、トークンごとにウォレットが必要になります。
新しいトークンを触るごとにウォレットを作っていては、だいぶめんどくさいですよね。
そこでERC20というトークン規格に沿って設計することで1つのウォレットで管理できるようになります。
また、ERC20は「代替性トークン」とも呼ばれています。
「代替性トークン」とは、あるトークンと同じ価値を持つ他のトークンと交換できるということです。
反対に交換できないのでNFTを代表する「非代替性トークン」です。
もっと詳しく知りたい方は以下の記事を読んでください。
非エンジニアでも分かるように丁寧にまとめています。
環境構築

ERC20の預け入れと引き出しを行う前に環境の構築をしていきます。
Node.js
とnpm
はインストールしてある前提で進めていきます。
インストールしていない方は以下の記事などを参考にインストールしてください。
https://www.sejuku.net/blog/72545
まずはTerminalを開いて好きな場所で以下を実行してください。
$ mkdir erc20
$ cd erc20
ではHardhatをインストールしていきます。
HardhatとはSolidityの開発環境です。
$ npm install --save-dev hardhat
added 300 packages in 27s
61 packages are looking for funding
run `npm fund` for details
上記のように出力されたら以下を実行してHardhat雛形を作っていきます。
$ npx hardhat
いくつか質問されますが全てEnterキーを押してください。
✔ What do you want to do? · Create a JavaScript project
✔ Hardhat project root: · /Users/.../ERC20
✔ Do you want to add a .gitignore? (Y/n) · y
✔ Do you want to install this sample project's dependencies with npm (@nomicfoundation/hardhat-toolbox)? (Y/n) · y
To address all issues (including breaking changes), run:
npm audit fix --force
Run `npm audit` for details.
✨ Project created ✨
See the README.md file for some example tasks you can run
Give Hardhat a star on Github if you're enjoying it! 💞✨
https://github.com/NomicFoundation/hardhat
Please take a moment to complete the 2022 Solidity Survey: https://hardhat.org/solidity-survey-2022
上記のように出力されたら雛形が作成され、以下のようなディレクトリ構造になっているはずです。
あまりに長いので省略しています。
.
├── README.md
├── artifacts
│ ├── build-info
│ │ └── ....json
│ └── contracts
│ └── Lock.sol
│ ├── Lock.dbg.json
│ └── Lock.json
├── cache
│ └── solidity-files-cache.json
├── contracts
│ └── Lock.sol
├── hardhat.config.js
├── node_modules
│ └── ...
├── package-lock.json
├── package.json
├── scripts
│ └── deploy.js
└── test
└── Lock.js
追加で以下のライブラリをインストールしてください。
$ npm i chai
$ npm i @openzeppelin/contracts
ではまずファイルをコンパイルしていきましょう。
以下のコマンドを実行してください。
$ npx hardhat compile
Compiled 1 Solidity file successfully
上記のように出力されたらコンパイル成功です。
最後にテストを実行してみましょう。
$ npx hardhat test
以下のように出力されたらテストは成功です。
Lock
Deployment
✔ Should set the right unlockTime (1206ms)
✔ Should set the right owner
✔ Should receive and store the funds to lock
✔ Should fail if the unlockTime is not in the future (39ms)
Withdrawals
Validations
✔ Should revert with the right error if called too soon
✔ Should revert with the right error if called from another account
✔ Shouldn't fail if the unlockTime has arrived and the owner calls it
Events
✔ Should emit an event on withdrawals
Transfers
✔ Should transfer the funds to the owner
9 passing (1s)
これで環境構築は終わりです!
コントラクト作成

環境構築ができたので、早速ERC20の預け入れと引き出しができるコントラクトを作成していきましょう!
Token.sol
まずはERC20トークンを作成しましょう。
Terminalで以下を実行してください。
$ touch contracts/Token.sol
contracts
ディレクトリの下にToken.sol
というファイルが作成されました。
作成したToken.sol
に以下のコードを貼り付けてください。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract CardeneToken is ERC20 {
constructor() ERC20("CARD", "Cardene Token") {
_mint(msg.sender, 10000);
}
}
1つずつ解説していきます。
読み込み
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
Solidityのバージョンを定義し、ERC20のコントラクを読み込んでいます。
ERC20トークン作成
contract CardeneToken is ERC20 {
constructor() ERC20("CARD", "Cardene Token") {
_mint(msg.sender, 3000);
}
}
ERC20トークンを作成しています。
トークン名はCardene Token
で、トークンのシンボルはCARD
です。
このトークンを3000
ほど発行しています。
Bank.sol
では次にERC20トークンを預けたり、引き出したりできるコントラクトを作成していきましょう!
Terminalで以下を実行してください。
$ touch contracts/Bank.sol
contracts
ディレクトリの下にBank.sol
というファイルが作成されました。
作成したBank.sol
に以下のコードを貼り付けてください。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract Bank {
// コントラクトをデプロイしたアドレス
address public owner;
// 「トークンのシンボル => トークンのアドレス」の配列。
// ownerにしか追加できない。
mapping(bytes32 => address) public whitelistedTokens;
// 「ウォレットアドレス => (トークンのシンボル => トークンの量)」の配列。
mapping(address => mapping(bytes32 => uint256)) public accountBalances;
constructor() {
owner = msg.sender;
}
/// @notice whitelistedTokens配列に要素をトークンを追加する関数。
/// ownerしか実行できない。
/// @param symbol トークンのシンボル。
/// @param tokenAddress トークンのアドレス。
function whitelistToken(bytes32 symbol, address tokenAddress) external {
require(msg.sender == owner, 'This function is not public');
// 「トークンのシンボル => トークンのアドレス」として配列に追加。
whitelistedTokens[symbol] = tokenAddress;
}
/// @notice whitelistedTokensにトークンが登録されているか確認する関数。
/// @param token トークンのシンボル。
/// @return トークンのアドレス。
function getWhitelistedTokenAddresses(bytes32 token) external view returns(address) {
return whitelistedTokens[token];
}
/// @notice トークンを預ける関数。
/// @param amount 預けるトークンの量。
/// @param symbol 預けるトークンのシンボル。
function depositTokens(uint256 amount, bytes32 symbol) external {
accountBalances[msg.sender][symbol] += amount;
// トークンのアドレスを取得して、ウォレットからこのコントラクトにトークンをおくる。
ERC20(whitelistedTokens[symbol]).transferFrom(msg.sender, address(this), amount);
}
/// @notice トークンを引き出す関数。
/// @dev 引き出すトークン量より預けたトークンの量の方が多い必要がある。
/// @param amount 引き出すトークンの量。
/// @param symbol 引き出すトークンのシンボル。
function withdrawTokens(uint256 amount, bytes32 symbol) external {
require(accountBalances[msg.sender][symbol] >= amount, 'Insufficent funds');
// ユーザーが預けているトークンの量の記録を変更。
accountBalances[msg.sender][symbol] -= amount;
// トークンのアドレスを取得して、コントラクトからユーザーウォレットにトークンをおくる。
ERC20(whitelistedTokens[symbol]).transfer(msg.sender, amount);
}
}
長いですね...。
1つずつ説明していくので安心してついてきてください!
読み込み
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
こちらはToken.sol
と同じですね。
Solidityのバージョンを定義し、ERC20のコントラクを読み込んでいます。
コントラクトと変数の定義
contract Bank {
// コントラクトをデプロイしたアドレス
address public owner;
// 「トークンのシンボル => トークンのアドレス」の配列。
// ownerにしか追加できない。
mapping(bytes32 => address) public whitelistedTokens;
// 「ウォレットアドレス => (トークンのシンボル => トークンの量)」の配列。
mapping(address => mapping(bytes32 => uint256)) public accountBalances;
コントラクトの定義と3つの変数が定義されていますね。
owner
は、コントラクトをデプロイしたウォレットのアドレスが格納されます。
whitelistedTokens
には、預け入れや引き出しができるトークンを記録しています。
accountBalances
には、特定のウォレットアドレスがどのトークンのシンボルのどのくらいのトークンの量を預けたかを記録しています。
constructor
constructor() {
owner = msg.sender;
}
constructor
はコントラクトをデプロイした時に一度だけ実行される関数です。
owner
変数にコントラクトをデプロイしたユーザーのウォレットアドレスを格納しています。
whitelistToken
/// @notice whitelistedTokens配列に要素をトークンを追加する関数。
/// ownerしか実行できない。
/// @param symbol トークンのシンボル。
/// @param tokenAddress トークンのアドレス。
function whitelistToken(bytes32 symbol, address tokenAddress) external {
require(msg.sender == owner, 'This function is not public');
// 「トークンのシンボル => トークンのアドレス」として配列に追加。
whitelistedTokens[symbol] = tokenAddress;
}
トークンのシンボルとトークンのアドレスを引数にとって、whitelistedTokens
配列に要素を追加する関数です。
whitelistedTokens
にトークンを記録することで、そのトークンは預けたり引き出したりできるようになります。
ちなみにこの関数はowner
(コントラクトをデプロイしたユーザー)のみ実行できます。
getWhitelistedTokenAddresses
/// @notice whitelistedTokensにトークンが登録されているか確認する関数。
/// @param token トークンのシンボル。
/// @return トークンのアドレス。
function getWhitelistedTokenAddresses(bytes32 token) external view returns(address) {
return whitelistedTokens[token];
}
トークンのシンボルを引数にとり、whitelistedTokens
配列にトークンが登録されているか確認する関数です。
depositTokens
/// @notice トークンを預ける関数。
/// @param amount 預けるトークンの量。
/// @param symbol 預けるトークンのシンボル。
function depositTokens(uint256 amount, bytes32 symbol) external {
accountBalances[msg.sender][symbol] += amount;
// トークンのアドレスを取得して、ウォレットからこのコントラクトにトークンをおくる。
ERC20(whitelistedTokens[symbol]).transferFrom(msg.sender, address(this), amount);
}
預けたいトークンの量とトークンのシンボルを引数に渡して、ERC20トークンをコントラクトに預けることができる関数です。
特定のウォレットアドレスがどのトークンをどのくらい預けているか把握するために、accountBalances
配列に記録しています。
whitelistedTokens[symbol]
で、預け入れや引き出しの許可がされているERC20トークンのアドレスを取得し、そのアドレスをERC20()
で囲むことで、コントラクトアドレス内のtransferFrom
関数を呼び出しています。
transferFrom
関数は実行されると、ユーザーのウォレットからBankコントラクトにamount
で指定したトークン量分のERC20トークンを預け入れます。
ついてこれましたか?
今回はTokenコントラクトのアドレスを取得する想定なので、Tokenコントラクト内のtransferFrom
関数を実行しています。
「いやいや。TokenコントラクトにtransferFrom
関数は定義していないでしょ?」
このように思う方もいると思いますが、実は定義してあるのです。
以下の部分でERC20コントラクトを継承することで、ERC20コントラクト内のtransferFrom
関数を実行できているのです。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.17;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
contract CardeneToken is ERC20 {
constructor() ERC20("CARD", "Cardene Token") {
_mint(msg.sender, 10000);
}
}
ERC20コントラクトのコードがみたい方は以下を見てください。
https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC20/ERC20.sol
transferFrom
関数が定義されているのを確認できるはずです。
withdrawTokens
/// @notice トークンを引き出す関数。
/// @dev 引き出すトークン量より預けたトークンの量の方が多い必要がある。
/// @param amount 引き出すトークンの量。
/// @param symbol 引き出すトークンのシンボル。
function withdrawTokens(uint256 amount, bytes32 symbol) external {
require(accountBalances[msg.sender][symbol] >= amount, 'Insufficent funds');
// ユーザーが預けているトークンの量の記録を変更。
accountBalances[msg.sender][symbol] -= amount;
// トークンのアドレスを取得して、コントラクトからユーザーウォレットにトークンをおくる。
ERC20(whitelistedTokens[symbol]).transfer(msg.sender, amount);
}
}
引き出したいトークンの量とトークンのシンボルを引数に渡して、ERC20トークンをコントラクトから引き出すことができる関数です。
預けている以上のトークンを引き出されては困るため、accountBalances
配列を確認して預けているトークン量よりも引き出そうとしているトークン量の方が少ないことを確認しています。
どのトークンをどのくらい預けているかという記録を更新しないといけないので、accountBalances
配列のデータを書き換えています。
Bank.sol
ファイルについて説明してきましたが理解できましたか?
次の章ではこの章で作成したコントラクトをテストするコードを確認していきます。
テストファイルの作成

ではこの章でコントラクトを実行してテストするファイルを作成していきましょう。
以下のコマンドを実行してください。
$ touch test/Bank.js
test
ディレクトリにテストで使用するためのBank.js
というファイルが作成されました。
では、Bank.js
に以下を貼り付けてください。
const { expect } = require("chai");
describe('Bank', function () {
beforeEach(async function() {
[owner, wallet1, wallet2] = await ethers.getSigners();
// BankコントラクトとTokenコントラクトを取得。
Bank = await ethers.getContractFactory('Bank', owner);
CardeneToken = await ethers.getContractFactory('CardeneToken', wallet1);
// BankコントラクトとTokenコントラクトをデプロイ。
bank = await Bank.deploy();
token = await CardeneToken.deploy();
// wallet1アドレスから、wallet2アドレスににCARDトークンを1000送る。
token.connect(wallet1).transfer(wallet2.address, 1000);
// CARDトークンを発行したwallet1アドレスから、
// Bankコントラクトアドレスに対して5000CARDトークンの転送許可を与える。
await token.connect(wallet1).approve(
bank.address,
2000
);
// CARDトークンを発行したwallet1アドレスから、
// wallet2アドレスに対して1000CARDトークンの転送許可を与える。
await token.connect(wallet2).approve(
bank.address,
1000
);
// TokenコントラクトをBytes32型に変換。
CARD_TOKEN = ethers.utils.formatBytes32String('CardeneToken');
// BankコントラクトのwhitelistToken関数を呼び出して、
// CARDトークンの預け入れ・引き出しができるように登録。
await bank.whitelistToken(
CARD_TOKEN,
token.address
);
});
describe('deployment', function () {
// wallet1アドレスが5000CARDトークンを取得しているかチェック。
it('should mint tokens to wallet 1', async function () {
expect(await token.balanceOf(wallet1.address)).to.equal(2000);
})
// wallet2アドレスが1000CARDトークンを取得しているかチェック。
it('should transfer tokens to wallet 2', async function () {
expect(await token.balanceOf(wallet2.address)).to.equal(1000);
})
// BankコントラクトのwhitelistedTokens関数を実行して、
// 預け入れ・引き出しができるトークンがCARDトークンであるかチェック。
it('should whitelist CARD token on the contract', async function () {
expect(
await bank.whitelistedTokens(CARD_TOKEN)
).to.equal(token.address);
})
})
describe('depositTokens', function () {
it('should deposit CARD token', async function () {
// wallet1から100CARDトークンを預け入れる。
await bank.connect(wallet1).depositTokens(
500,
CARD_TOKEN,
);
// wallet2から50CARDトークンを預け入れる。
await bank.connect(wallet2).depositTokens(
300,
CARD_TOKEN,
);
// wallet1とwallet2のトークン残高を確認。
expect(await token.balanceOf(wallet1.address)).to.equal(1500);
expect(await token.balanceOf(wallet2.address)).to.equal(700);
// Bankコントラクトにしっかり預け入れているか確認。
expect(
await bank.accountBalances(wallet1.address, CARD_TOKEN)
).to.equal(500);
expect(
await bank.accountBalances(wallet2.address, CARD_TOKEN)
).to.equal(300);
});
})
describe('withdraw', function () {
it('should withdraw CARD token from the contract', async function () {
// wallet1から追加で600CARDトークンを預け入れる。
await bank.connect(wallet1).depositTokens(
1000,
CARD_TOKEN,
);
// 100CARDトークンを引き出す。
await bank.connect(wallet1).withdrawTokens(
500,
CARD_TOKEN,
);
// 600CARDトークンを預け入れ、100CARDトークンを引き出しので、
// wallet1は4500CARDトークンを保持していはずなので確認。
expect(await token.balanceOf(wallet1.address)).to.equal(1500);
// wallet1がBankコントラクトにどのくらいCARDトークンを預けているか確認。
expect(
await bank.accountBalances(wallet1.address, CARD_TOKEN)
).to.equal(500);
})
// 預け入れているよりも多くのCARDトークンを引き出してエラーが起きるかチェック。
it('should not allow withdrawing more than has been deposited', async function () {
await expect(
bank.connect(wallet1).withdrawTokens(1000, CARD_TOKEN)
).to.be.revertedWith("Insufficent funds")
}
)
})
})
だいぶ長いですが、1つずつ紹介していくのでついてきてください!
読み込み
const { expect } = require("chai");
chaiというライブラリを読み込んでいます。
chai
とはスマートコントラクトのテストを書きやすく、読みやすくしてくれるライブラリです。
テスト前提
beforeEach(async function() {
// アカウント3つ作成
[owner, wallet1, wallet2] = await ethers.getSigners();
// BankコントラクトとTokenコントラクトを取得。
Bank = await ethers.getContractFactory('Bank', owner);
CardeneToken = await ethers.getContractFactory('CardeneToken', wallet1);
// BankコントラクトとTokenコントラクトをデプロイ。
bank = await Bank.deploy();
token = await CardeneToken.deploy();
// wallet1アドレスから、wallet2アドレスににCARDトークンを1000送る。
token.connect(wallet1).transfer(wallet2.address, 1000);
// CARDトークンを発行したwallet1アドレスから、
// Bankコントラクトアドレスに対して5000CARDトークンの転送許可を与える。
await token.connect(wallet1).approve(
bank.address,
2000
);
// CARDトークンを発行したwallet1アドレスから、
// wallet2アドレスに対して1000CARDトークンの転送許可を与える。
await token.connect(wallet2).approve(
bank.address,
1000
);
// TokenコントラクトをBytes32型に変換。
CARD_TOKEN = ethers.utils.formatBytes32String('CardeneToken');
// BankコントラクトのwhitelistToken関数を呼び出して、
// CARDトークンの預け入れ・引き出しができるように登録。
await bank.whitelistToken(
CARD_TOKEN,
token.address
);
});
ここでは他のテストが実行されるための前提となる部分を実装しています。
この部分だけでもだいぶ多いですが、やっていることは大まかに以下のことになります。
やっていること
- アカウントの作成
- TokenコントラクトとBankコントラクトのデプロイ。
- wallet1とwallet2がトークンを所有。
CARDトークン
をBankコントラクトのwhitelistedTokens
配列に追加して、預け入れと引き出しができるようにする。
前提部分のチェック
describe('deployment', function () {
// wallet1アドレスが5000CARDトークンを取得しているかチェック。
it('should mint tokens to wallet 1', async function () {
expect(await token.balanceOf(wallet1.address)).to.equal(2000);
})
// wallet2アドレスが1000CARDトークンを取得しているかチェック。
it('should transfer tokens to wallet 2', async function () {
expect(await token.balanceOf(wallet2.address)).to.equal(1000);
})
// BankコントラクトのwhitelistedTokens関数を実行して、
// 預け入れ・引き出しができるトークンがCARDトークンであるかチェック。
it('should whitelist CARD token on the contract', async function () {
expect(
await bank.whitelistedTokens(CARD_TOKEN)
).to.equal(token.address);
})
})
これ以降はdescribe
から始まる部分を一塊として説明していきます。
このdescribe
の塊ごとにテストが実行されていると考えてください。
「前提部分 + describe
」とういう構成で各describe
同士はデータを共有しません。
つまりdescribe
が3つあるとして、「describe1
」、「describe2
」、「describe3
」とします。
まずは「前提部分」が実行された状態で、describe1
が実行されます。
次に、describe1
が実行された状態は無視されて、describe2
を実行します。
同じように、describe2
で実行された状態は無視されて、describe3
が実行されます。
そしてこの部分のdescribe
では、wallet1とwallet2、Bankコントラクトが想定通りのCARDトークン
を保有しているかを確認しています。
CARDトークンを預ける
describe('depositTokens', function () {
it('should deposit CARD token', async function () {
// wallet1から100CARDトークンを預け入れる。
await bank.connect(wallet1).depositTokens(
500,
CARD_TOKEN,
);
// wallet2から50CARDトークンを預け入れる。
await bank.connect(wallet2).depositTokens(
300,
CARD_TOKEN,
);
// wallet1とwallet2のトークン残高を確認。
expect(await token.balanceOf(wallet1.address)).to.equal(1500);
expect(await token.balanceOf(wallet2.address)).to.equal(700);
// Bankコントラクトにしっかり預け入れているか確認。
expect(
await bank.accountBalances(wallet1.address, CARD_TOKEN)
).to.equal(500);
expect(
await bank.accountBalances(wallet2.address, CARD_TOKEN)
).to.equal(300);
});
})
この部分ではCARDトークン
を預けることができるかテストしています。
wallet1から500CARDトークン
、wallet2から300CARDトークン
を預けて残高をチェックしています。
その後Bankコントラクトに預けたCARDトークン
が存在するかチェックしています。
CARDトークンを引き出す
describe('withdraw', function () {
it('should withdraw CARD token from the contract', async function () {
// wallet1から追加で600CARDトークンを預け入れる。
await bank.connect(wallet1).depositTokens(
1000,
CARD_TOKEN,
);
// 100CARDトークンを引き出す。
await bank.connect(wallet1).withdrawTokens(
500,
CARD_TOKEN,
);
// 600CARDトークンを預け入れ、100CARDトークンを引き出しので、
// wallet1は4500CARDトークンを保持していはずなので確認。
expect(await token.balanceOf(wallet1.address)).to.equal(1500);
// wallet1がBankコントラクトにどのくらいCARDトークンを預けているか確認。
expect(
await bank.accountBalances(wallet1.address, CARD_TOKEN)
).to.equal(500);
})
// 預け入れているよりも多くのCARDトークンを引き出してエラーが起きるかチェック。
it('should not allow withdrawing more than has been deposited', async function () {
await expect(
bank.connect(wallet1).withdrawTokens(1000, CARD_TOKEN)
).to.be.revertedWith("Insufficent funds")
}
)
})
})
最後にこの部分で行なっていることを解説していきます。
ここではざっくり、wallet1からBankコントラクトに1000CARDトークン
を預けて、500CARDトークン
を引き出しています。
その後、wallet1とBankコントラクトが所有しているCARDトークン
の量を確認しています。
最後の部分では、預けているCARDトークン
よりも多くの量のCARDトークン
を引き出そうとしてエラーが出るか確認しています。
実行
ではテストを実行していきましょう。
Terminalで以下を実行してください。
$ npx hardhat test
実行後以下のように出力されていればテストは成功です!
Bank
deployment
✔ should mint tokens to wallet 1
✔ should transfer tokens to wallet 2
✔ should whitelist CARD token on the contract
depositTokens
✔ should deposit CARD token (75ms)
withdraw
✔ should withdraw CARD token from the contract (47ms)
✔ should not allow withdrawing more than has been deposited
Lock
Deployment
✔ Should set the right unlockTime (59ms)
✔ Should set the right owner
✔ Should receive and store the funds to lock
✔ Should fail if the unlockTime is not in the future
Withdrawals
Validations
✔ Should revert with the right error if called too soon
✔ Should revert with the right error if called from another account
✔ Shouldn't fail if the unlockTime has arrived and the owner calls it
Events
✔ Should emit an event on withdrawals
Transfers
✔ Should transfer the funds to the owner
15 passing (2s)
コード

コードはイカを参考にしてください。
https://github.com/cardene777/smart-contract-test/tree/main/ERC20
各ファイルは以下に置いています。
Bank.sol
https://github.com/cardene777/smart-contract-test/blob/main/ERC20/contracts/Bank.sol
Token.sol
https://github.com/cardene777/smart-contract-test/blob/main/ERC20/contracts/Token.sol
Bank.js
https://github.com/cardene777/smart-contract-test/blob/main/ERC20/test/Bank.js
最後に

今回は「ERC20の預け入れと引き出し」について解説してきました。
いかがだったでしょうか?
ERC20についてある程度理解しているエンジニアの方であればついてこれたのではないでしょうか?
難しかった方はぜひSolidityの基礎を復習してみてください!
特にコントラクトの継承あたりは難しかったと思うので、その部分を調べてみてください。
また、ERC20が怪しい人はこの記事の最初でも紹介した以下の記事が参考になります。
何か質問があれば以下のTwitterなどから質問してください!
もし何か質問などがあれば以下のTwitterなどから連絡ください!
普段はSolidityやブロックチェーン、Web3についての情報発信をしています。
Twiiterでは気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!