はじめに
『DApps開発入門』という本や色々記事を書いているかるでねです。
今回は、従来のICOのような中央集権的な配布ではなく、トークンの初期配布をPoWによって行う『Mineable Token』の仕組みを提案しているERC918についてまとめていきます!
以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。
他にも様々なEIPについてまとめています。
概要
ERC918は、「マイナブルトークン(Mineable Token)」の標準化に関する提案です。 マイナブルトークンとは、Proof of Workアルゴリズムを使ってトークンを分配する仕組みを持つトークンのことです。
トークンは最初にコントラクト内にロックされ、ユーザーが mint() 関数を実行することで徐々に得られます。
mint() 関数は、いわゆるfaucetのような役割を果たし、Proof of Work によってガス代を最小限に抑えつつ、トークンの供給スピードを制御します。
ERC918を標準化することで、CPUやGPUを用いたマイニングソフトウェアやマイニングプールなど、マイニング関連の外部ツールの整備が進み、トークンマイニングのエコシステムが広がることが期待されます。
動機
従来のICO(Initial Coin Offering)モデルは、人為的な不正行為のリスクを抱えており、またプロジェクトの初期段階で資金やトークンの管理が一部の中央集権的な関係者に集中してしまう問題があります。
ERC918では、トークン配布を「Initial Mining Offering(IMO)」という形で実現することにより、コントラクトのデプロイ者も単なるユーザーの1人となり、トークンの所有権が特定の管理者に偏らなくなります。
このようなモデルにより、投資家が抱えるリスクを大幅に軽減でき、さらにERC20やERC721といった他の標準とも高い互換性を持つよう設計されています。
仕様
ERC918に準拠するコントラクトは、以下の関数とイベントを実装する必要があります。
mint(uint256 nonce)- トークンをマイニング(発行)するためのメイン関数。
- 難易度や報酬、現在のチャレンジ情報などを取得する取得関数群。
Mintイベント- マイニング成功時に発行。
Abstract コントラクト(オプション)
Abstractコントラクトは、ERC918インターフェースを拡張し、マイニング処理を4つの内部フェーズに分割します。
_hash- ハッシュ値の検証。
_reward- 報酬の発行。
_epoch- ブロックのカウント更新。
_adjustDifficulty- マイニング難易度の調整。
これにより、実装の自由度を保ちつつ標準的な構造を提供し、外部ツールや他コントラクトとの互換性を高めます。
インターフェース
interface ERC918 {
function mint(uint256 nonce) public returns (bool success);
function getAdjustmentInterval() public view returns (uint);
function getChallengeNumber() public view returns (bytes32);
function getMiningDifficulty() public view returns (uint);
function getMiningTarget() public view returns (uint);
function getMiningReward() public view returns (uint);
function decimals() public view returns (uint8);
event Mint(address indexed from, uint rewardAmount, uint epochCount, bytes32 newChallengeNumber);
}変数
adjustmentInterval
bytes32 public adjustmentInterval;難易度の調整間隔を表す変数。 マイニング難易度の再調整が行われるまでの時間(秒単位)を定義します。
challengeNumber
bytes32 public challengeNumber;現在のマイニングチャレンジの番号を表す変数。 新たなトークンが発行されるごとに更新され、次のマイニング作業の対象となる値です。
difficulty
uint public difficulty;現在のマイニング難易度を表す変数。
マイニング成功までの計算負荷を表し、ネットワークの状態に応じて _adjustDifficulty により変動します。
tokensMinted
uint public tokensMinted;これまでにマイニングされたトークンの合計数。
_reward フェーズで更新され、発行済トークンの追跡に使用されます。
epochCount
uint public epochCount;マイニングされたブロック(またはマイニング回数)のカウント。
mint() が成功するたびに加算され、報酬や難易度調整の条件として使われます。
イベント
Mint
event Mint(address indexed from, uint rewardAmount, uint epochCount, bytes32 newChallengeNumber);マイニングが成功し、新たにトークンが発行された時に発行されるイベント。 新たなチャレンジ番号とともに、誰がどれだけの報酬を得たかを示す記録となります。
パラメータ
from- トークンをマイニングしたアドレス。
rewardAmount- 発行されたトークンの量。
epochCount- 現在のマイニング回数。
newChallengeNumber- 新しいチャレンジ番号。
関数
mint
function mint(uint256 nonce) public returns (bool success);Proof of Workを使ってトークンを発行する関数。
ユーザーが自ら計算した nonce を渡し、コントラクトがPoW条件に一致するかを検証し、一致すれば報酬が発行されます。
引数
nonce- ハッシュ計算時に使うランダムな数値。
戻り値
success- マイニングに成功し、トークンが発行されたかどうか。
getAdjustmentInterval
function getAdjustmentInterval() public view returns (uint);難易度調整の間隔を取得する関数。 マイニング難易度を何秒ごとに再計算するかの設定値を返します。
戻り値
uint- 調整間隔(秒)。
getChallengeNumber
function getChallengeNumber() public view returns (bytes32);現在のチャレンジ番号を取得する関数。 次のマイニングターゲットとなるチャレンジ番号を返します。
戻り値
bytes32- 現在のチャレンジ番号。
getMiningDifficulty
function getMiningDifficulty() public view returns (uint);現在のマイニング難易度を取得する関数。 PoWがどれだけ困難かを表す数値を返します。
戻り値
uint- 難易度値。
getMiningTarget
function getMiningTarget() public view returns (uint);現在のハッシュターゲット値を取得する関数。 マイニングに成功するために計算されるべきターゲット値を返します。
戻り値
uint- ハッシュターゲットの値。
getMiningReward
function getMiningReward() public view returns (uint);現在のマイニング報酬量を取得する関数。 成功したマイニング1回あたりに発行されるトークン数を返します。
戻り値
uint- マイニング報酬量。
decimals
function decimals() public view returns (uint8);トークンの小数点以下の桁数を取得する関数。 トークンの表示単位(例えば18など)を返します。
戻り値
uint8- 小数点以下の桁数。
Mining Operations
ERC918におけるマイニングは、主に mint 関数を通じて行われ、4つのフェーズに分かれて内部処理されます。
hash- 提出された
nonceがチャレンジ条件を満たすか検証。
- 提出された
_reward- 現在の報酬量を計算し、マイナーへ割り当て。
_newEpoch- マイニングの状態(エポック)を更新。
_adjustDifficulty- 一定の間隔ごとに難易度を調整。
マイナーは、コントラクトから取得した challengeNumber と自身のアドレスに nonce を組み合わせてPoWを解き、mint を実行して報酬を受け取ります。
マイニングイベントの流れ
- マイナーが
nonceを見つけてmint関数を呼び出す。 - コントラクトが
hashによってPoW条件を検証。 - 検証成功時に
_rewardでトークンを割り当て、tokensMintedを加算。 _newEpochによりepochCountを更新。epochCountに応じて_adjustDifficultyを呼び出す。- 成功すれば
Mintイベントが発行される。
インターフェース
contract AbstractERC918 is EIP918Interface {
// the amount of time between difficulty adjustments
uint public adjustmentInterval;
// generate a new challenge number after a new reward is minted
bytes32 public challengeNumber;
// the current mining target
uint public miningTarget;
// cumulative counter of the total minted tokens
uint public tokensMinted;
// number of blocks per difficulty readjustment
uint public blocksPerReadjustment;
//number of 'blocks' mined
uint public epochCount;
/*
* Externally facing mint function that is called by miners to validate challenge digests, calculate reward,
* populate statistics, mutate epoch variables and adjust the solution difficulty as required. Once complete,
* a Mint event is emitted before returning a success indicator.
**/
function mint(uint256 nonce) public returns (bool success) {
require(msg.sender != address(0));
// perform the hash function validation
hash(nonce);
// calculate the current reward
uint rewardAmount = _reward();
// increment the minted tokens amount
tokensMinted += rewardAmount;
epochCount = _epoch();
//every so often, readjust difficulty. Don't readjust when deploying
if(epochCount % blocksPerReadjustment == 0){
_adjustDifficulty();
}
// send Mint event indicating a successful implementation
emit Mint(msg.sender, rewardAmount, epochCount, challengeNumber);
return true;
}
}変数
adjustmentInterval
uint public adjustmentInterval;マイニング難易度の調整間隔(秒)を表す変数。
epochCount の進行に応じて、難易度調整のトリガーとなるタイミングを設定します。
パラメータ
adjustmentInterval- 難易度調整の間隔(秒単位)。
challengeNumber
bytes32 public challengeNumber;現在のマイニングチャレンジを識別する値。 各エポックごとに新しい値が生成され、マイナーがPoWの対象とするデータとなります。
miningTarget
uint public miningTarget;マイニング成功と判定されるためのターゲット値。
この値より小さいハッシュを生成することで、mint に成功できます。
tokensMinted
uint public tokensMinted;これまでに発行されたトークンの総量。 成功したマイニングで発行されたトークンを累計します。
blocksPerReadjustment
uint public blocksPerReadjustment;難易度の再調整が行われるブロックの間隔。
epochCount がこの値の倍数になると _adjustDifficulty が呼ばれます。
epochCount
uint public epochCount;成功したマイニングの回数。 トークンが発行されるたびに加算され、難易度調整のトリガーにもなります。
イベント
Mint
event Mint(address indexed from, uint reward_amount, uint epochCount, bytes32 newChallengeNumber);トークンがマイニングされた時に発行されるイベント。 マイニングに成功したユーザー、報酬額、新しいチャレンジ番号などを記録します。
パラメータ
from- マイニングに成功したアドレス。
reward_amount- 発行されたトークン数。
epochCount- エポックのカウント。
newChallengeNumber- 次のチャレンジ番号。
関数
mint
function mint(uint256 nonce) public returns (bool success);PoWを検証し、報酬を発行する関数。
提出された nonce がチャレンジ条件を満たすか検証し、満たす場合に報酬を発行し、エポックや難易度などの状態を更新します。
引数
nonce- マイナーが計算したハッシュ用の乱数。
戻り値
success- マイニング成功時は
trueを返す。
- マイニング成功時は
hash
function hash(uint256 nonce) public returns (bytes32 digest);ハッシュを計算・検証する関数。
challengeNumber、msg.sender、nonce を組み合わせたハッシュが miningTarget 未満であるかを確認します。オーバーライド前提の関数です。
引数
nonce- PoWに使用される値。
戻り値
digest- 計算されたハッシュ値。
_reward
function _reward() internal returns (uint);マイナーへの報酬を計算・決定する内部関数。 実装によって報酬額を決定し、トークンを割り当てる。オーバーライド前提の関数。
戻り値
uint- 報酬額。
_newEpoch
function _newEpoch(uint256 nonce) internal returns (uint);次のマイニングフェーズの準備を行う関数。
epochCount を更新する他、必要に応じて変数を初期化・調整します。
引数
nonce- 現在のマイニングに使用された値。
戻り値
uint- 更新後のエポックカウント。
_adjustDifficulty
function _adjustDifficulty() internal returns (uint);マイニングの難易度を調整する関数。
epochCount の値に応じて difficulty や miningTarget を調整します。
戻り値
uint- 調整後の難易度。
getAdjustmentInterval
function getAdjustmentInterval() public view returns (uint);難易度調整のインターバルを取得する関数。 秒単位で定義されたインターバル値を返します。
戻り値
uint- 調整間隔(秒)。
getChallengeNumber
function getChallengeNumber() public view returns (bytes32);現在のチャレンジ番号を取得する関数。 新しいPoW対象となるチャレンジ値を返します。
戻り値
bytes32- 現在のチャレンジ番号。
getMiningDifficulty
function getMiningDifficulty() public view returns (uint);現在の難易度を取得する関数。 PoWに必要なハッシュの難易度(桁数)を返します。
戻り値
uint- 難易度値。
getMiningReward
function getMiningReward() public view returns (uint);マイニング報酬の現在値を取得する関数。 報酬アルゴリズムに応じて、マイニング1回あたりのトークン量を返します。
戻り値
uint- 報酬トークンの量。
マージマイニング拡張機能(オプション)
ERC918は、複数のトークンを同時にマイニング(マージマイニング)する拡張機能をサポートしています。 この機能により、マイナーは1つのProof of Work(PoW)によって複数のトークンを同時に取得できるようになります。
この拡張機能はオプションであり、基底コントラクト側で msg.sender ではなく tx.origin を用いて報酬を割り当てる実装である必要があります。
そうでない場合、報酬が呼び出し元コントラクトに送られ、最終的なマイナーに届かない可能性があります。
インターフェース
/**
* @title ERC-918 Mineable Token Standard, optional merged mining functionality
* @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-918.md
*
*/
contract ERC918Merged is AbstractERC918 {
/*
* @notice Externally facing merge function that is called by miners to validate challenge digests, calculate reward,
* populate statistics, mutate state variables and adjust the solution difficulty as required. Additionally, the
* merge function takes an array of target token addresses to be used in merged rewards. Once complete,
* a Mint event is emitted before returning a success indicator.
*
* @param _nonce the solution nonce
**/
function merge(uint256 _nonce, address[] _mineTokens) public returns (bool) {
for (uint i = 0; i < _mineTokens.length; i++) {
address tokenAddress = _mineTokens[i];
ERC918Interface(tokenAddress).mint(_nonce);
}
}
/*
* @notice Externally facing merge function kept for backwards compatibility with previous definition
*
* @param _nonce the solution nonce
* @param _challenge_digest the keccak256 encoded challenge number + message sender + solution nonce
**/
function merge(uint256 _nonce, bytes32 _challenge_digest, address[] _mineTokens) public returns (bool) {
//the challenge digest must match the expected
bytes32 digest = keccak256( abi.encodePacked(challengeNumber, msg.sender, _nonce) );
require(digest == _challenge_digest, "Challenge digest does not match expected digest on token contract [ ERC918Merged.mint() ]");
return merge(_nonce, _mineTokens);
}
}関数
merge
function merge(uint256 _nonce, address[] _mineTokens) public returns (bool)複数のトークンに対して同時に mint を呼び出す関数。
1つの nonce を使って、指定された複数のトークンコントラクトに mint を呼び出すことで、複数のトークンをマイニングできます。各トークンコントラクトが ERC918 に準拠している必要があります。
引数
_nonce- マイニングに使用される乱数値。
_mineTokens- マージマイニング対象となるトークンのコントラクトアドレスの配列。
戻り値
bool- 実行に成功したかどうか。
merge
function merge(uint256 _nonce, bytes32 _challenge_digest, address[] _mineTokens) public returns (bool)チャレンジダイジェストの検証を行うマージマイニング関数。
この関数は、呼び出し前に作成された challenge_digest が正しいかを検証します。
検証が成功すると、上記の merge(_nonce, _mineTokens) を実行します。
セキュリティ強化のために導入されています。
引数
_nonce- マイニングに使用される乱数値。
_challenge_digestkeccak256(challengeNumber + msg.sender + nonce)によって生成された検証用ハッシュ。
_mineTokens- マージマイニング対象となるトークンのコントラクトアドレスの配列。
戻り値
bool- 実行に成功したかどうか。
デリゲートマイニング拡張機能(オプション)
マイナーが直接トランザクションガスを支払わずにマイニング報酬を得る方法を提供します。
この仕組みでは、マイナーがローカル環境で nonce を計算し、delegatedMintHashing(uint256,address) の関数シグネチャと共に署名したパケットを、マイニングプールや第三者に送信します。
その第三者が代理で delegatedMint() を呼び出すことで、マイナーに代わってマイニング処理が実行されます。
コントラクト側では署名検証を行い、署名元が origin と一致するか確認された上で、内部的に mintInternal 関数が実行され、報酬が割り当てられます。
インターフェース
import 'openzeppelin-solidity/contracts/contracts/cryptography/ECDSA.sol';
contract ERC918DelegatedMint is AbstractERC918, ECDSA {
/**
* @notice Hash (keccak256) of the payload used by delegatedMint
* @param _nonce the golden nonce
* @param _origin the original minter
* @param _signature the original minter's elliptical curve signature
*/
function delegatedMint(uint256 _nonce, address _origin, bytes _signature) public returns (bool success) {
bytes32 hashedTx = delegatedMintHashing(_nonce, _origin);
address minter = recover(hashedTx, _signature);
require(minter == _origin, "Origin minter address does not match recovered signature address [ AbstractERC918.delegatedMint() ]");
require(minter != address(0), "Invalid minter address recovered from signature [ ERC918DelegatedMint.delegatedMint() ]");
success = mintInternal(_nonce, minter);
}
/**
* @notice Hash (keccak256) of the payload used by delegatedMint
* @param _nonce the golden nonce
* @param _origin the original minter
*/
function delegatedMintHashing(uint256 _nonce, address _origin) public pure returns (bytes32) {
/* "0x7b36737a": delegatedMintHashing(uint256,address) */
return toEthSignedMessageHash(keccak256(abi.encodePacked( bytes4(0x7b36737a), _nonce, _origin)));
}
}関数
delegatedMint
function delegatedMint(uint256 _nonce, address _origin, bytes _signature) public returns (bool success)署名付きマイニングデータを使って第三者が代理でマイニング処理を行う関数。
オフチェーンでマイナーが生成した nonce と署名を使って、第三者(プールなど)がこの関数を実行します。
コントラクトは署名を検証し、正当であれば報酬を割り当てます。
引数
_nonce- PoWで発見されたソリューションとなる値。
_origin- 実際にマイニングを行ったユーザーアドレス。
_signature_nonceと_originを組み合わせて署名されたECDSA署名。
戻り値
success- マイニングが成功したかどうか。
delegatedMintHashing
function delegatedMintHashing(uint256 _nonce, address _origin) public pure returns (bytes32)デリゲートマイニング時の署名用ハッシュを生成する関数。
nonce と origin を組み合わせ、関数シグネチャ 0x7b36737a(delegatedMintHashing(uint256,address))をプレフィックスとして付加し、keccak256 と toEthSignedMessageHash を通じてEthereumの署名対象ハッシュを生成します。
引数
_nonce- マイニングのソリューション。
_origin- 本来のマイナーアドレス。
戻り値
bytes32- Ethereum署名用のメッセージハッシュ。
パケット仕様(Mineable Mint Packet Metadata)
{
"nonce": "string",
"origin": "string",
"signature": "string"
}オフチェーンで生成されるマイニングパケットの標準フォーマット。
マイナーが自分の nonce とアドレスに基づいて署名を生成し、それをパケットとして第三者に送信します。
この形式に従うことで、プールや中継者が delegatedMint() を呼び出すことができます。
パラメータ
nonce- 発見されたマイニングソリューション値。
origin- 実際にPoWを解いたマイナーのアドレス。
signaturedelegatedMintHashing(nonce, origin)に対する署名(eth_sign形式)。
この拡張により、ERC918 トークンはマイニングプールや他の第三者による代理マイニング送信が可能となり、マイナーがガス代を負担しない設計が実現できます。 ただし、報酬分配はオフチェーンで別途管理・分配される必要があります。
マイナブルトークンメタデータ拡張(オプション)
この拡張は、ERC918 におけるマイナブルトークンに対して、より豊かな情報(名前、説明、画像URL、ハッシュアルゴリズムなど)を提供するための仕組みです。
オンチェーンでの全データ保存はガスコスト的に現実的ではないため、トークンに関連する情報を外部URIで提供する形式を採用します。そのために metadataURI() 関数を実装し、マイナブルトークンのメタデータを含むJSON文字列を返すようにします。
この仕組みにより、フロントエンドやマーケットプレイスは各トークンの特徴を動的に表示することができます。
インターフェース
/**
* @title ERC-918 Mineable Token Standard, optional metadata extension
* @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-918.md
*
*/
interface ERC918Metadata is AbstractERC918 {
/**
* @notice A distinct Uniform Resource Identifier (URI) for a mineable asset.
*/
function metadataURI() external view returns (string);
}関数
metadataURI
function metadataURI() external view returns (string);マイナブルトークンのメタデータURIを返す関数。 トークンの詳細情報(名前・記号・説明・画像・Webサイト・ハッシュアルゴリズムなど)が記述されたJSONデータのURIを返します。 このURIはIPFSやHTTPサーバーにホストされることが想定されています。
戻り値
string- JSONメタデータが配置されたURI。
メタデータスキーマ(Mineable Token Metadata)
{
"title": "Mineable Token Metadata",
"type": "object",
"properties": {
"symbol": {
"type": "string",
"description": "Identifies the Mineable Token's symbol",
},
"name": {
"type": "string",
"description": "Identifies the Mineable Token's name",
},
"description": {
"type": "string",
"description": "Identifies the Mineable Token's long description",
},
"website": {
"type": "string",
"description": "Identifies the Mineable Token's homepage URI",
},
"image": {
"type": "string",
"description": "Identifies the Mineable Token's image URI",
},
"type": {
"type": "string",
"description": "Identifies the Mineable Token's hash algorithm ( ie.keccak256 ) used to encode the solution",
}
}
}マイナブルトークンの情報を格納するJSON形式の構造体。 このメタデータスキーマは、トークンの識別やUI表示に必要な情報を含みます。 type フィールドには使用しているハッシュアルゴリズム(例: keccak256)が含まれ、トークンのPoW方式を明示します。
パラメータ
symbol- トークンの記号(例: BTC、ETHなど)。
name- トークンの正式名称。
description- トークンの説明文(長文可)。
website- トークンの公式WebサイトのURL。
image- トークンのロゴ画像URL(IPFSやHTTP対応)。
type- 使用しているハッシュアルゴリズムの種類(例: keccak256)。
補足
ERC918では keccak256 アルゴリズムの使用が推奨されています。
これは、Solidity内で効率よく実行できる一方向ハッシュ関数であり、EVM上での計算コストも低いためです。
ハッシュの構成要素には以下の3つがあります。
nonce- マイナーが発見すべきソリューション値。
challengeNumber- 次のマイニングが開始されるまで不明なランダムなシードとなり、事前にマイニングされるのを防ぐ役割を持ちます。
msg.sender- マイニング解がそのアドレスにのみ有効であるようにすることで、中間者攻撃(MiTM)を防止します。
- また、マイニングプールが自身のアドレスを強制的に使わせることにより、不正な報酬取得を防ぐことができます。
この仕組みにより、電力やハードウェアという現実資源をEthereumトークンに変換する経済的動機が生まれ、中央集権的なICOに代わる形で初期の投資家とマイナーの間にインセンティブの橋渡しが可能となります。
エネルギー消費に対する批判もありますが、ERC918のような「Initial Mining Offering(IMO)」は数時間〜数日などの短期間で完了する設計も可能であり、無駄な長期消費を避けられます。 また、ネットワークセキュリティではなく、「配布の公平性を担保する仕組み」としての価値があるとされています。
互換性
ERC918 の初期バージョンでは、mint() 関数に challenge_digest を渡す形式が採用されていました。
これは challengeNumber, msg.sender, nonce を keccak256 でハッシュ化したものです。
/**
* @title ERC-918 Mineable Token Standard, optional backwards compatibility function
* @dev See https://github.com/ethereum/EIPs/blob/master/EIPS/eip-918.md
*
*/
contract ERC918BackwardsCompatible is AbstractERC918 {
/*
* @notice Externally facing mint function kept for backwards compatibility with previous mint() definition
* @param _nonce the solution nonce
* @param _challenge_digest the keccak256 encoded challenge number + message sender + solution nonce
**/
function mint(uint256 _nonce, bytes32 _challenge_digest) public returns (bool success) {
//the challenge digest must match the expected
bytes32 digest = keccak256( abi.encodePacked(challengeNumber, msg.sender, _nonce) );
require(digest == _challenge_digest, "Challenge digest does not match expected digest on token contract [ AbstractERC918.mint() ]");
success = mint(_nonce);
}
}しかし、これはガスコストを最適化するために標準仕様から削除されました。
その代わり、以前のマイニングソフトウェアやプールとの互換性を保つため、以下のような互換用の関数を持つ ERC918BackwardsCompatible コントラクトを継承ツリーに加えることが推奨されています。
この関数は、提出された challenge_digest が正しいかを検証した上で、通常の mint(nonce) を実行します。
この方式により、過去のインフラとの互換性を保ちつつ、効率的なガス使用が可能となります。
引用
Jay Logelin jlogelin@alumni.harvard.edu, Infernal_toast admin@0xbitcoin.org, Michael Seiler mgs33@cornell.edu, Brandon Grill bg2655@columbia.edu, "ERC-918: Mineable Token Standard [DRAFT]," Ethereum Improvement Proposals, no. 918, March 2018. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-918.
最後に
今回は「従来のICOのような中央集権的な配布ではなく、トークンの初期配布をPoWによって行う『Mineable Token』の仕組みを提案しているERC918」についてまとめてきました! いかがだったでしょうか?
質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!
他の媒体でも情報発信しているのでぜひ他も見ていってください!

