概要
ERC20準拠のトークンをmintできる機能を作ったので紹介します。
まだまだブロックチェーン界隈の記事は少ないので参考になれば幸いです。
- 目次
- 作ったものの機能紹介
- ソースコード解説
作ったもの
イーサリアムメインネットではなくGoerliテストネットのみで利用が可能です。
Goerliテストネットへmetamaskを接続するには以下記事を参考にするとよいです。
https://qiita.com/ItodaiCrypto/items/563214a28239c6f9cf0e
mintする機能を含め、一部の機能はガス代が必要です。以下でgoerli-ETHを入手しておいてください。
https://goerlifaucet.com/
画面実装が面倒だったので、残高確認などの出力項目はconsole.logで出しています。
get
get!!!ボタンを押すことでERC20準拠の「OKT」をmintし、ボタンを押した人のwalletアドレスで確認できます。
Metamaskを利用している場合は、カスタムトークンのインポート作業が必要になるので、最初にその作業をしておくとよいです。
OKTトークンのコントラクトアドレスは、こちらに記載しています。
balance
現在のMetamaskアクティブアカウントのOKTトークン残高をconsole.logします。
approve30
フォームに入力したアドレスに対して、OKTを30まで送金されることを許可します。
第三者が、自分のOKTを特定のアドレスに対して送付することができます。
サブスクリプションサービスなどに使われるような機能のようです。
transferFrom
AからBにOKTを送金します。approveを実行していれば、第三者がこの操作をすることができます。
checkAllowance
AからBに対していくらOKTを送金できるかを表示します。
approve30を押したぶんだけ値が増加します。
ソースコード
コントラクトまわり
inportしているモジュールがあるので、お手元のコンソールで以下コマンドを実行してモジュールをダウンロードしておいてください。
npm install --save-dev @nomiclabs/hardhat-ethers dotenv ethers hardhat
npm install --save-dev @nomiclabs/hardhat-etherscan
ほとんどOpenZeppelin Wizardのテンプレと一緒ですが、いくつかカスタムした部分があります。
- OkayuToken.sol(トークン名は適当です。)
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.4;
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Burnable.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/draft-ERC20Permit.sol";
import "@openzeppelin/contracts/token/ERC20/extensions/ERC20Votes.sol";
/// @custom:security-contact teniguru.jimukyoku@gmail.com
contract OkayuToken is ERC20, ERC20Burnable, Pausable, Ownable, ERC20Permit, ERC20Votes {
constructor() ERC20("OkayuToken", "OKT") ERC20Permit("OkayuToken") {
_mint(msg.sender, 1000 * 10 ** decimals());
}
event TokenNotification(uint token);
function getToken() external {
_mint(msg.sender, 10 ether);
emit TokenNotification(balanceOf(msg.sender));
}
function pause() public onlyOwner {
_pause();
}
function unpause() public onlyOwner {
_unpause();
}
function mint(address to, uint256 amount) public onlyOwner {
_mint(to, amount);
}
function _beforeTokenTransfer(address from, address to, uint256 amount)
internal
whenNotPaused
override
{
super._beforeTokenTransfer(from, to, amount);
}
// The following functions are overrides required by Solidity.
function _afterTokenTransfer(address from, address to, uint256 amount)
internal
override(ERC20, ERC20Votes)
{
super._afterTokenTransfer(from, to, amount);
}
function _mint(address to, uint256 amount)
internal
override(ERC20, ERC20Votes)
{
super._mint(to, amount);
}
function _burn(address account, uint256 amount)
internal
override(ERC20, ERC20Votes)
{
super._burn(account, amount);
}
}
コントラクトをデプロイした人にトークンを発行する処理
ここも他のサイトを参照したテンプレのようなものですが、ERC20トークンの名前を定義してデプロイアドレスに1000OKTを付与する記述になります。
constructor() ERC20("OkayuToken", "OKMT") ERC20Permit("OkayuToken") {
_mint(msg.sender, 1000 * 10 ** decimals());
}
getボタンを押下したときにトークンをmintする処理
ERC20トークンにガス代以上の価値を持たせるのならあまりおススメはできない記述だとは思います。
10OKTを誰でも発行できるようにする関数です。まあ実験的なアプリを作ろうとしているなら問題ないです。
function getToken() external {
_mint(msg.sender, 10 ether);
emit TokenNotification(balanceOf(msg.sender));
}
フロントエンド周り
javascript弱者すぎてcontractより苦労しました。VueとかReactとか知らんし。用語が誤っているかもしれないのでご指摘ください。
html
OKTToken.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script language="javascript" type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<script src="https://unpkg.com/@metamask/legacy-web3@latest/dist/metamask.web3.min.js"></script>
<script language="javascript" type="text/javascript" src="./js/web3.min.js"></script>
<script type="module" src="./js/OkayuToken.js"></script>
</head>
<body>
<h1>ERC20 OKT Token Test(Goerli Test Network Only)</h1>
<ul>
<li>Deploying contracts with the account: 0xa9eA5a7125bB715EBFcd6361F0d577B383C2996e</li>
<li>OKT Token address: 0xa4F4e451B27a56C183407C0d3181D1be856219E0</li>
</ul>
<h2>Token Test</h2>
<button id = "getbutton">get!!!</button>
</br>
<button id = "balance">balance</button>
</br>
<input type="text" value="" id="spendToAddress" />
<button id = "approve30">approve30</button>
</br>
from:<input type="text" value="" id="transferFromAddress" />
to:<input type="text" value="" id="transferToAddress" />
<button id = "transferFrom">transferFrom</button>
</br>
from:<input type="text" value="" id="allowanceFrom" />
to:<input type="text" value="" id="allowanceTo" />
<input id = "checkAllowance" type="button" value="checkAllownance" />
</br>
<input type="text" value="" id="inputaddr" />
<input id = "balancebutton2" type="button" value="getBalance" />
</br>
<button class="enableEthereumButton">Enable Ethereum</button>
</body>
</html>
javascript
Metamaskの機能を呼び出すときに以下のようなエラーが出ていました。Reactなどを良く知っていて非同期処理を書き慣れているならそこまで引っかからなかったかも?コントラクトよりもjavascriptでコントラクト関数を呼ぶ処理が鬼門だった。
The MetaMask Web3 object does not support synchronous methods like **** without a callback parameter
OkayuToken.js
import OkayuTokenContract from '../contracts/erc20_mintable_burnable_getToken.abi.json' assert {type:'json'};
console.log(OkayuTokenContract.contractName);
const Web3 = require("web3");
console.log(web3.version);
var OkayuToken;
function startApp() {
var OkayuTokenAddress = "0xa4F4e451B27a56C183407C0d3181D1be856219E0";
OkayuToken = web3.eth.contract(OkayuTokenContract).at(OkayuTokenAddress);
console.log(OkayuToken);
if (typeof window.ethereum !== 'undefined') {
console.log('MetaMask is installed!');
}
web3.eth.getAccounts(function(error, accounts) {
if (error) return;
let user_account = accounts[0];
if(typeof user_account !== 'undefined'){
console.log(user_account);
getBalance(user_account);
}else{
console.log("ログインして下さい");
}
});
}
window.addEventListener('load', function() {
if (typeof web3 !== 'undefined') {
web3 = new Web3(web3.currentProvider);
} else {
}
startApp();
})
function getBalance(_address){
web3.eth.getBalance(_address, (error, balance) => {
if (error) return;
console.log(JSON.stringify(balance, null, 2));
});
}
const ethereumButton = document.querySelector('.enableEthereumButton');
ethereumButton.addEventListener('click', () => {
ethereum.request({ method: 'eth_requestAccounts' });
});
document.getElementById("balancebutton2").onclick = function(){
const walletAddress = document.getElementById("inputaddr").value;
console.log(walletAddress);
try {
OkayuToken.balanceOf(walletAddress, (error, balance) => {
if (error) return;
balance = web3.fromWei(balance, 'ether');
console.log(balance + "OKT");
});
} catch (error) {
console.error(error);
}
console.log("get!");
}
function allowanceOKT() {
console.log("clicked!");
try {
var allowanceTo = document.getElementById("allowanceTo").value;
var allowanceFrom = document.getElementById("allowanceFrom").value;
OkayuToken.allowance(allowanceFrom, allowanceTo, (error, remaining) => {
OkayuToken.decimals((error, decimals) => {
remaining = remaining.div(10**decimals);
console.log(remaining.toString() + 'OKT');
});
});
alert("good!");
} catch (error) {
console.error(error);
alert("notgood!");
}
}
$("#checkAllowance").click(async () => {
allowanceOKT();
});
function approve30OKT() {
web3.eth.defaultAccount = web3.eth.accounts[0];
console.log("clicked!");
try {
var approvalSpendTo = document.getElementById("spendToAddress").value;
OkayuToken.approve(approvalSpendTo, 30000000000000000000, (error) => {
});
alert("good!");
} catch (error) {
console.error(error);
alert("notgood!");
}
}
$("#approve30").click(async () => {
approve30OKT();
});
function transferFromOKT() {
web3.eth.defaultAccount = web3.eth.accounts[0];
console.log("clicked!");
try {
var transferFromAddress = document.getElementById("transferFromAddress").value;
var transferToAddress = document.getElementById("transferToAddress").value;
OkayuToken.transferFrom(transferFromAddress, transferToAddress, 10000000000000000000, (error) => {
});
alert("good!");
} catch (error) {
console.error(error);
alert("notgood!");
}
}
$("#transferFrom").click(async () => {
transferFromOKT();
});
function getOKTbalance() {
web3.eth.defaultAccount = web3.eth.accounts[0];
try {
var walletAddress = web3.eth.defaultAccount;
OkayuToken.balanceOf(walletAddress, (error, balance) => {
OkayuToken.decimals((error, decimals) => {
balance = balance.div(10**decimals);
console.log(balance.toString() + 'OKT');
});
});
alert("good!");
} catch (error) {
console.error(error);
alert("notgood!");
}
}
$("#balance").click(async () => {
getOKTbalance();
});
function getOKTtoken() {
web3.eth.defaultAccount = web3.eth.accounts[0];
try {
OkayuToken.getToken((error) => {
});
} catch (error) {
console.error(error);
}
}
$("#getbutton").click(async () => {
getOKTtoken();
});
初期設定
画面をロードした際にweb3 = new Web3(web3.currentProvider);
でweb3APIをインスタンス化し、OkayuToken = web3.eth.contract(OkayuTokenContract).at(OkayuTokenAddress);
にてコントラクトへのアクセスを可能にしています。
OkayuTokenContract
はコントラクトへアクセスするためのABIを格納します。後述します。
OkayuTokenAddress
にはコントラクトをデプロイした際に振られたコントラクトアドレスを格納しています。
function startApp() {
var OkayuTokenAddress = "0xa4F4e451B27a56C183407C0d3181D1be856219E0";
OkayuToken = web3.eth.contract(OkayuTokenContract).at(OkayuTokenAddress);
console.log(OkayuToken);
if (typeof window.ethereum !== 'undefined') {
console.log('MetaMask is installed!');
}
web3.eth.getAccounts(function(error, accounts) {
if (error) return;
let user_account = accounts[0];
if(typeof user_account !== 'undefined'){
console.log(user_account);
getBalance(user_account);
}else{
console.log("ログインして下さい");
}
});
}
window.addEventListener('load', function() {
if (typeof web3 !== 'undefined') {
web3 = new Web3(web3.currentProvider);
} else {
}
startApp();
})
自身が持っているOKT残高を取得
web3.eth.defaultAccount = web3.eth.accounts[0];
で現在アクティブになっているウォレットアドレスを取得しています。
OkayuToken.balanceOf
を非同期で呼び出し、console.logで残高を出力します。途中のbalance.divはbalanceOf関数のメソッドで、そのままだとgweiの単位で出力され読みづらいので10乗しています。
function getOKTbalance() {
web3.eth.defaultAccount = web3.eth.accounts[0];
try {
var walletAddress = web3.eth.defaultAccount;
OkayuToken.balanceOf(walletAddress, (error, balance) => {
OkayuToken.decimals((error, decimals) => {
balance = balance.div(10**decimals);
console.log(balance.toString() + 'OKT');
});
});
alert("good!");
} catch (error) {
console.error(error);
alert("notgood!");
}
}
$("#balance").click(async () => {
getOKTbalance();
});
OKTをmintする
現在アクティブになっているウォレットアドレスを取得したあと、そこに対してOKTをmintするコントラクト関数getToken
を呼んでいます。
function getOKTtoken() {
web3.eth.defaultAccount = web3.eth.accounts[0];
try {
OkayuToken.getToken((error) => {
});
} catch (error) {
console.error(error);
}
}
$("#getbutton").click(async () => {
getOKTtoken();
});
ABIについて
コントラクトをコンパイルする際にABIが自動生成されると思っていたのですが探してもそれらしいものがない?
仕方ないので手作りしましたが、いい方法あれば教えてほしいです。
コンパイルしたあとのartifacts/contracts/OkayuToken.json
かなと思ったら違う。。。
erc20_mintable_burnable_getToken.abi.json
[
{
"constant": true,
"inputs": [],
"name": "name",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function",
"signature": "0x06fdde03"
},
{
"constant": false,
"inputs": [
{
"name": "spender",
"type": "address"
},
{
"name": "value",
"type": "uint256"
}
],
"name": "approve",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function",
"signature": "0x095ea7b3"
},
{
"constant": true,
"inputs": [],
"name": "totalSupply",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function",
"signature": "0x18160ddd"
},
{
"constant": false,
"inputs": [
{
"name": "from",
"type": "address"
},
{
"name": "to",
"type": "address"
},
{
"name": "value",
"type": "uint256"
}
],
"name": "transferFrom",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function",
"signature": "0x23b872dd"
},
{
"constant": true,
"inputs": [],
"name": "decimals",
"outputs": [
{
"name": "",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "view",
"type": "function",
"signature": "0x313ce567"
},
{
"constant": false,
"inputs": [
{
"name": "spender",
"type": "address"
},
{
"name": "addedValue",
"type": "uint256"
}
],
"name": "increaseAllowance",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function",
"signature": "0x39509351"
},
{
"constant": false,
"inputs": [
{
"name": "to",
"type": "address"
},
{
"name": "value",
"type": "uint256"
}
],
"name": "mint",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function",
"signature": "0x40c10f19"
},
{
"constant": false,
"inputs": [
{
"name": "value",
"type": "uint256"
}
],
"name": "burn",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function",
"signature": "0x42966c68"
},
{
"constant": true,
"inputs": [
{
"name": "owner",
"type": "address"
}
],
"name": "balanceOf",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function",
"signature": "0x70a08231"
},
{
"constant": false,
"inputs": [
{
"name": "from",
"type": "address"
},
{
"name": "value",
"type": "uint256"
}
],
"name": "burnFrom",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function",
"signature": "0x79cc6790"
},
{
"constant": true,
"inputs": [],
"name": "symbol",
"outputs": [
{
"name": "",
"type": "string"
}
],
"payable": false,
"stateMutability": "view",
"type": "function",
"signature": "0x95d89b41"
},
{
"constant": false,
"inputs": [
{
"name": "account",
"type": "address"
}
],
"name": "addMinter",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function",
"signature": "0x983b2d56"
},
{
"constant": false,
"inputs": [],
"name": "renounceMinter",
"outputs": [],
"payable": false,
"stateMutability": "nonpayable",
"type": "function",
"signature": "0x98650275"
},
{
"constant": false,
"inputs": [
{
"name": "spender",
"type": "address"
},
{
"name": "subtractedValue",
"type": "uint256"
}
],
"name": "decreaseAllowance",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function",
"signature": "0xa457c2d7"
},
{
"inputs": [],
"name": "getToken",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"constant": false,
"inputs": [
{
"name": "to",
"type": "address"
},
{
"name": "value",
"type": "uint256"
}
],
"name": "transfer",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "function",
"signature": "0xa9059cbb"
},
{
"constant": true,
"inputs": [
{
"name": "account",
"type": "address"
}
],
"name": "isMinter",
"outputs": [
{
"name": "",
"type": "bool"
}
],
"payable": false,
"stateMutability": "view",
"type": "function",
"signature": "0xaa271e1a"
},
{
"constant": true,
"inputs": [
{
"name": "owner",
"type": "address"
},
{
"name": "spender",
"type": "address"
}
],
"name": "allowance",
"outputs": [
{
"name": "",
"type": "uint256"
}
],
"payable": false,
"stateMutability": "view",
"type": "function",
"signature": "0xdd62ed3e"
},
{
"inputs": [
{
"name": "_name",
"type": "string"
},
{
"name": "_symbol",
"type": "string"
},
{
"name": "_decimals",
"type": "uint8"
}
],
"payable": false,
"stateMutability": "nonpayable",
"type": "constructor",
"signature": "constructor"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "account",
"type": "address"
}
],
"name": "MinterAdded",
"type": "event",
"signature": "0x6ae172837ea30b801fbfcdd4108aa1d5bf8ff775444fd70256b44e6bf3dfc3f6"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "account",
"type": "address"
}
],
"name": "MinterRemoved",
"type": "event",
"signature": "0xe94479a9f7e1952cc78f2d6baab678adc1b772d936c6583def489e524cb66692"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "from",
"type": "address"
},
{
"indexed": true,
"name": "to",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Transfer",
"type": "event",
"signature": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"
},
{
"anonymous": false,
"inputs": [
{
"indexed": true,
"name": "owner",
"type": "address"
},
{
"indexed": true,
"name": "spender",
"type": "address"
},
{
"indexed": false,
"name": "value",
"type": "uint256"
}
],
"name": "Approval",
"type": "event",
"signature": "0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925"
}
]
感想
これらの材料があればERC20トークンを新規デプロイしてmintするアプリを作れるはず。
何か気になったことがあればお知らせください。
今後は、approveの利用用途をもう少し調べるのと、
今回は誰でもmintできる機能になってはいますが、フロントのアプリのロジックと連携して「こういった条件の場合にトークンをmintできる/mintする権利をもらえる」というような仕組みを実装するにはどういうスキームが考えられるのか、調べてみたいと思います。
(通常のwebアプリに保存されたデータを前提にしてブロックチェーンアプリを動かすのはナンセンスでは?という突っ込み歓迎です)
以上