bitbank

Smart Contract Solidity ブロックチェーン

ERC20の預け入れと引き出しについて実装しながら1から丁寧にわかりやすく解説

かるでね

こんにちは!実務でPython、Djangoを使って、機械学習やWebアプリケーション開発などをしているかるでねです!
最近はSolidityを使ってスマートコントラクト開発やバグを見つけたりしています。

今回の記事では「ERC20の預け入れと引き出し」についてまとめていきます。

本記事は、「ERC20がどんなものかわかったけど、実際にどう使うの?」という方向けの記事になっています。

ERC20を理解したエンジニアの次のステップとしては、実際にERC20を用いて何か作ることです。

ERC20トークンの預け入れと引き出しを知ることで、UniswapなどのDeFiへの理解も深まるのでぜひ学んだいってください!

前置きは早々に早速中身を確認していきましょう!

今回の記事は以下のコードをめちゃくちゃ参考にしています。

ERC20とは?

そもそもERC20って何?」という方もいると思います。

ERC20とは、「Ethereumブロックチェーンで動作する統一されたトークン規格の1つ」です。

統一されたトークンの規格」というのは、「共通のルールを持っているトークン」という事です。

共通のルールを持っていないと、トークンごとにウォレットが必要になります。

新しいトークンを触るごとにウォレットを作っていては、だいぶめんどくさいですよね。

そこでERC20というトークン規格に沿って設計することで1つのウォレットで管理できるようになります。

また、ERC20は「代替性トークン」とも呼ばれています。

代替性トークン」とは、あるトークンと同じ価値を持つ他のトークンと交換できるということです。

反対に交換できないのでNFTを代表する「非代替性トークン」です。

もっと詳しく知りたい方は以下の記事を読んでください。

非エンジニアでも分かるように丁寧にまとめています。

環境構築

ERC20の預け入れと引き出しを行う前に環境の構築をしていきます。

Node.jsnpmはインストールしてある前提で進めていきます。

インストールしていない方は以下の記事などを参考にインストールしてください。

まずは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 @nomiclabs/hardhat-ethers
$ npm i chai
$ npm i ethers
$ 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コントラクトのコードがみたい方は以下を見てください。

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コントラクトのデプロイ。
  • wallet1wallet2がトークンを所有。
  • 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では、wallet1wallet2Bankコントラクトが想定通りの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トークンを引き出しています。

その後、wallet1Bankコントラクトが所有している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)

コード

コードはイカを参考にしてください。

各ファイルは以下に置いています。

Bank.sol

Token.sol

Bank.js

最後に

今回は「ERC20の預け入れと引き出し」について解説してきました。

いかがだったでしょうか?

ERC20についてある程度理解しているエンジニアの方であればついてこれたのではないでしょうか?

難しかった方はぜひSolidityの基礎を復習してみてください!

特にコントラクトの継承あたりは難しかったと思うので、その部分を調べてみてください。

また、ERC20が怪しい人はこの記事の最初でも紹介した以下の記事が参考になります。

何か質問があれば以下のTwitterなどから質問してください!

もし何か質問などがあれば以下のTwitterなどから連絡ください!

普段はSolidityやブロックチェーン、Web3についての情報発信をしています。

Twiiterでは気になった記事などを共有しているので、ぜひフォローしてくれると嬉しいです!

-Smart Contract, Solidity, ブロックチェーン
-,