LoginSignup
1
0

[ERC7565] 資産をNFTとしてロックしてDeFiなどで担保にできる仕組みを理解しよう!

Posted at

はじめに

初めまして。
CryptoGamesというブロックチェーンゲーム企業でエンジニアをしている cardene(かるでね) です!
スマートコントラクトを書いたり、フロントエンド・バックエンド・インフラと幅広く触れています。

代表的なゲームはクリプトスペルズというブロックチェーンゲームです。

今回は、流動性の確保と資産管理の効率化に焦点を当て、NFTを活用した新たな担保ローンシステムの仕組みを提案しているERC7565についてまとめていきます!

以下にまとめられているものを翻訳・要約・補足しながらまとめていきます。

他にも様々なEIPについてまとめています。

概要

このERC(イーサリアムリクエストコメント)の提案は、特定の資産を担保にして資金を借りるためのメカニズムに関するものです。
ここで言う「資産所有者」とは、ロックされた預金や資産を表すNFT(非代替トークン)を担保にして資金を借りる人のことを指します。
これらのNFTは、予め定義された満期後に、ベースとなる資産とそれに付随する利益を請求する権利を表しています。

動機

このERCの提案は、DeFi(分散型金融)における資産のロックと流動性確保の課題に対する解決策を提示しています。
DeFiの世界では、資産をロックすることで利息や投票権などのメリットを提供するさまざまなメカニズムが導入されていますが、これらの資産がロックされている間に流動性を維持することは大きな課題です。

この提案は、ロックされた資産から利益を生み出すための方法として、ERC721(NFTを表す標準)とERC4907(レンタルや一時的な使用権をNFTで表現するための拡張標準)を使用することを提案しています。

ERC721については以下の記事を参考にしてください。

ERC4907については以下の記事を参考にしてください。

具体的には、以下のようなプロセスになっています。

1. 資産のロックとNFT

DeFiサービス、特に自動マーケットメーカー(AMM)では、流動性提供者が資産をプールに貢献し、そのステークを表すNFTを受け取ります。
これらのNFTは、資産への権利と関連するメリットを示しますが、同時にプール内の資産をロックし、提供者にとっての流動性の課題を引き起こします。

2. 担保としてのNFT使用

現行の実装では、緊急の流動性ニーズに応えるためには、提供者が自身の資産を引き出さなければならず、これがプールの流動性を悪化させる原因になっています。
提案されたメカニズムでは、流動性プール内のロックされた資産を表すNFTを担保として使用できるようにします。
これにより、流動性提供者は資産を引き出さずに一時的な流動性を得ることができ、プールの流動性レベルを維持できます。

DeFi(分散型金融)におけるAMM(Automated Market Maker、自動マーケットメーカー)は、従来の金融市場における市場作成者(マーケットメーカー)の概念を、ブロックチェーンとスマートコントラクトを利用して自動化したシステムです。
AMMは、資産の売買価格をアルゴリズムに基づいて自動的に決定し、取引所における流動性を提供する重要な役割を担っています。

AMMの基本構造

流動性プール

AMMの核となるのは、「流動性プール」と呼ばれるスマートコントラクトに保管されたトークンの集合です。
これらのプールは、異なる種類のトークン(例:ETHとDAI)を一定の比率で保持しています。

流動性提供者

ユーザーは、特定のトークンペアのプールにトークンを預けることで「流動性提供者」となります。
これにより、プールの流動性が増し、他のユーザーがそのトークンペアを簡単に交換できるようになります。

価格決定アルゴリズム

AMMでは、価格は供給と需要の市場原理によってではなく、スマートコントラクト内の数学的アルゴリズム(例:x*y=k)に基づいて自動的に決定されます。
このアルゴリズムは、トークン間の相対的価値を保ちながら、トークンの交換レートを計算します。

AMMのメリットと課題

メリット

  • 常時取引可能
    • 伝統的な取引所のように売り手と買い手がマッチする必要がないため、いつでも即座に取引が可能です。
  • 誰でも流動性提供者になれる
    • 誰でも容易に流動性プールに参加し、取引手数料の一部を収益として得ることができます。
  • 分散化
    • 中央集権的な管理者や仲介者が不要で、コードが全てを管理します。

課題

  • 希薄化リスク
    • 価格変動により、流動性提供者が元本を失うリスクがあります(不恒等価格変動リスク)。
  • スマートコントラクトのリスク
    • コードのバグや脆弱性がセキュリティ問題を引き起こす可能性があります。

AMMはDeFiエコシステムにおいて革新的な役割を果たしており、流動性の提供、新しいファイナンスの形態の促進、ユーザー参加の分散化など、多くの利点を提供しています。
しかし、その利用にはリスクも伴うため、関与する前にこれらを十分に理解することが重要です。

3. パーペチュアルコントラクトNFT

さらに、この提案では「パーペチュアルコントラクトNFT」という概念を導入しています。
これは、暗号通貨デリバティブ市場におけるパーペチュアルフューチャーコントラクトのアイデアを利用したもので、パーペチュアルコントラクトとその担保への権利を表します。
これにより、ロックされた資産の有効性を高め、DeFiの様々なアプリケーションで流動性を提供しながら資産ロックのメリットを保持する新しい形のNFTが提供されます。

この提案により、ロックされた資産を表すNFTの担保化を可能にし、貸し付けや取引など、資産ロックが一般的な幅広いDeFiサービスにおいて、多様なユーザーベースに利益をもたらす汎用的な流動性ソリューションを提供することを目指しています。
これにより、DeFiエコシステム全体での流動性と利用性が向上することが期待されます。

パーペチュアル(Perpetual)

パーペチュアル」という言葉は英語で「永久の」という意味を持ちます。
金融の文脈では、特定の期限や満期日が設定されていない契約や商品を指すのに使われることがあります。

パーペチュアルコントラクト(Perpetual Contract)

パーペチュアルコントラクトは、伝統的な先物契約と似ていますが、満期日や決済日が存在しない点が大きな違いです。
これにより、投資家は理論上無限にポジションを保持することが可能になり、ポジションを閉じるか、他の契約にロールオーバーする必要がなくなります。
暗号通貨市場においては、特にビットコインやイーサリアムなどの主要な通貨を対象としたパーペチュアルコントラクトが人気を博しています。

主な特徴とメカニズム

  • 満期日がない

    • パーペチュアルコントラクトは、名前の通り「永久に」続く契約であり、取引者は好きな時に契約を終了させることができます。
  • 資金調達率

    • パーペチュアルコントラクトでは、市場の流動性を維持し、現物価格と契約価格の差を調整するために「資金調達率」というメカニズムが用いられます。
    • 資金調達率が正の場合、ロングポジションを持つトレーダーはショートポジションを持つトレーダーに資金を支払います(逆もまた然り)。
  • レバレッジ: パーペチュアルコントラクトでは高いレバレッジ(借入れを使った取引)が可能で、これにより少ない資本で大きなポジションを取ることができますが、それに伴うリスクも大きくなります。

利用のメリット

  • 柔軟性

    • いつでも契約を終了させることができ、長期的な市場観測や短期的な価格変動に基づく戦略を立てやすくなります。
  • アクセス容易性

    • デリバティブ市場へのアクセスが容易になり、特に小規模な投資家でも大きな市場に参加できるようになります。

注意点

  • 高リスク

    • 高レバレッジによる取引は大きな利益をもたらす可能性がありますが、同時に大きな損失を招くリスクもあります。
  • 価格変動

    • 資金調達率の変動や市場の急激な価格変動により、ポジションの維持コストが変わる可能性があります。

パーペチュアルコントラクトは、暗号通貨市場における先進的な取引ツールの一つであり、適切に利用すれば市場の機会を捉えるための強力な手段となり得ます。
しかし、その仕組みとリスクを十分に理解した上で利用することが重要です。

より詳しくは以下の記事が参考になります。

仕様

コントラクトインターフェース

    interface IPerpetualContractNFT {

        // Emitted when an NFT is collateralized for obtaining a loan
        event Collateralized(uint256 indexed tokenId, address indexed owner, uint256 loanAmount, uint256 interestRate, uint256 loanDuration);

        // Emitted when a loan secured by an NFT is fully repaid, releasing the NFT from collateral
        event LoanRepaid(uint256 indexed tokenId, address indexed owner);

        // Emitted when a loan defaults, resulting in the transfer of the NFT to the lender
        event Defaulted(uint256 indexed tokenId, address indexed lender);

        // Enables an NFT owner to collateralize their NFT in exchange for a loan
        // @param tokenId The NFT to be used as collateral
        // @param loanAmount The amount of funds to be borrowed
        // @param interestRate The interest rate for the loan
        // @param loanDuration The duration of the loan
        function collateralize(uint256 tokenId, uint256 loanAmount, uint256 interestRate, uint64 loanDuration) external;

        // Enables a borrower to repay their loan and regain ownership of the collateralized NFT
        // @param tokenId The NFT that was used as collateral
        // @param repayAmount The amount of funds to be repaid
        function repayLoan(uint256 tokenId, uint256 repayAmount) external;

        // Allows querying the loan terms for a given NFT
        // @param tokenId The NFT used as collateral
        // @return loanAmount The amount of funds borrowed
        // @return interestRate The interest rate for the loan
        // @return loanDuration The duration of the loan
        // @return loanDueDate The due date for the loan repayment
        function getLoanTerms(uint256 tokenId) external view returns (uint256 loanAmount, uint256 interestRate, uint256 loanDuration, uint256 loanDueDate);

        // Allows querying the current owner of the NFT
        // @param tokenId The NFT in question
        // @return The address of the current owner
        function currentOwner(uint256 tokenId) external view returns (address);

        // View the total amount required to repay the loan for a given NFT
        // @param tokenId The NFT used as collateral
        // @return The total amount required to repay the loan, including interest
        function viewRepayAmount(uint256 tokenId) external view returns (uint256);
    }

Collateralized(担保化された)イベント

NFTがローンの担保として使用された事象をログに記録します。
ローン金額、利息率、ローン期間などの重要な詳細をキャプチャします。
collateralize 関数が成功した時に発行されます。

LoanRepaid(ローン返済)イベント

ローンが返済され、対応するNFTが担保から解放されたことをログに記録します。
repayLoan 関数が成功した時に発行されます。

Defaulted(デフォルト)イベント

ローンがデフォルトになり、NFTが貸し手に移転された事象をログに記録します。
ローンのデフォルトとNFTの貸し手への移転を通じて、貸し手が保護されるプロセスを示します。
ローンがデフォルト状態になったシナリオで発行されます。

collateralize 関数

NFT所有者が自分のNFTを担保にしてローンを受けることができる機能を提供します。
この関数はexternalとして実装されるべきで、これによりコントラクトの外部からのみアクセス可能になります。

repayLoan 関数

NFT所有者が自分のローンを返済し、担保に入れたNFTを取り戻すことができる機能を提供します。
この関数もexternalとして実装されるべきで、外部からのアクセスを可能にします。

getLoanTerms 関数

特定のNFTに対するローンの条件(例えば、ローン額、利息率、返済期間など)を照会する機能を提供します。
この関数はexternal viewとして実装されることができ、読み取り専用のアクセスを提供します。
viewは状態を変更せずにデータを読み取ることを示します。

currentOwner 関数

特定のNFTの現在の所有者を照会する機能を提供します。
この関数もexternal viewで実装でき、読み取り専用アクセスを提供します。

viewRepayAmount 関数

特定のNFTに対する現在の返済額(元本+利息など)を照会する機能を提供します。
これもexternal viewで実装されることができ、読み取り専用アクセスを提供します。

これらの関数は、NFTを担保にしたローンシステムにおける基本的な操作をカバーしており、スマートコントラクトを介して自動的に、かつ透明にローンのプロセスを管理することを可能にします。
各関数は特定の目的に沿って設計されており、NFTの担保提供、ローンの受け取り、返済、およびローン条件の確認を行うことができます。

補足

この提案は、DeFi(分散型金融)セクターにおける特定の課題に対処するために設計された新しい標準について説明しています。
特に、担保としてロックされた資産の流動性と管理に関する問題に焦点を当てています。
従来のDeFiメカニズムでは、資産保有者が貸付、ステーキング、イールドファーミングなどの活動に参加するためには、資産をロックアップする必要があります。
これは流動性の損失を意味します。
この新しい標準の目的は、より柔軟なアプローチを導入することで、資産がロックされている間も資産保有者がある程度の流動性を保持できるようにし、DeFi製品の有用性と魅力を高めることです。

設計の動機

流動性と資産管理の課題

DeFiにおいて資産を担保としてロックすることによる流動性の損失を解決する必要性に基づいています。

設計決定

デュアルロールシステム

NFTの所有者(資産保有者)とNFTを担保として利用するDeFiプラットフォームやコントラクトとの間に明確な区別を設けることで、権利と責任の管理を単純化し、明確性を向上させ、潜在的なコンフリクトを減少させます。

流動性の向上

資産所有者がロックされた資産を表すNFTを担保として利用してローンを確保できるようにすることで、プールやステーキングプログラムから資産を引き出す必要なく流動性にアクセスできるようにします。

自動化されたローンと担保管理

担保化されたNFTの条件を管理するための自動化された機能の統合は、取引コストと複雑さを最小限に抑えるための意図的な選択です。

DeFiの組み合わせ性

資産ロッキングサービスと担保化サービスの統合に特に焦点を当てることで、この標準がDeFiプラットフォームやサービス全体での採用を促進し、DeFiエコシステム内でのシームレスな接続を促進することを目指しています。

代替設計と関連する研究

ERC4907との比較

ERC4907もNFTに対するデュアルロールモデル(所有者と使用者)を導入していますが、ここで提案されている標準は、金融取引におけるNFTの担保化の使用に特に焦点を当てており、ERC4907の賃貸向けのアプローチからは逸脱しています。

従来の担保化方法の改善

完全な資産ロックアップを要求する従来のDeFi担保化と比較して、この標準は継続的な流動性アクセスを可能にするよりダイナミックで柔軟なモデルを提案しています。

この標準の設計は、DeFiにおける資産管理と流動性確保の課題に対処するためのものであり、資産保有者が資産をロックしている間も流動性を一部保持できるようにすることで、DeFiの有用性と魅力を高めることを目指しています。

互換性

ERC721と完全に互換性があり、ERC4907と統合してNFTをレンタルできます。

実装

// SPDX-License-Identifier: CC0-1.0 
pragma solidity ^0.8.0;

//import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "./IPerpetualContractNFT.sol";
import "./ERC4907/ERC4907.sol";

contract PerpetualContractNFT is ERC4907, IPerpetualContractNFT {
    struct LoanInfo {
        address borrower;   // Address that borrowed against the NFT
        uint256 loanAmount; // Amount of funds borrowed
        uint256 interestRate; // Interest rate for the loan
        uint64 loanDuration; // Duration of the loan
        uint256 loanStartTime; // Timestamp when the loan starts
    }

    mapping(uint256 => LoanInfo) internal _loans;

    //Constructor to initialize the Perpetual Contract NFT contract with the given name and symbo
    constructor(string memory name_, string memory symbol_)
        ERC4907(name_, symbol_)
    {}

    function collateralize(uint256 tokenId, uint256 loanAmount, uint256 interestRate, uint64 loanDuration) public override {
        require(ownerOf(tokenId) == msg.sender || isApprovedForAll(ownerOf(tokenId), msg.sender) || getApproved(tokenId) == msg.sender, "Not owner nor approved");

        LoanInfo storage info = _loans[tokenId];
        info.borrower = msg.sender;
        // The loan amount should reflect the asset's value as represented by the NFT, considering an appropriate loan-to-value (LTV) ratio.
        info.loanAmount = loanAmount;
        info.interestRate = interestRate;
        info.loanDuration = loanDuration;
        info.loanStartTime = block.timestamp;

        setUser(tokenId, address(this), loanDuration);
        emit Collateralized(tokenId, msg.sender, loanAmount, interestRate, loanDuration);

        // Further logic can be implemented here to manage the lending of assets
    }

    function repayLoan(uint256 tokenId, uint256 repayAmount) public override {
        require(_loans[tokenId].borrower == msg.sender, "Not the borrower.");

        // Calculate the total amount due for repayment
        uint256 totalDue = viewRepayAmount(tokenId);

        // Check if the repayAmount is sufficient to cover at least a part of the total due amount
        require(repayAmount <= totalDue, "Repay amount exceeds total due.");

        // Calculate the remaining loan amount after repayment
        _loans[tokenId].loanAmount = totalDue - repayAmount;

        // Resets the user of the NFT to the default state if the entire loan amount is fully repaid
        if(_loans[tokenId].loanAmount == 0) {
            setUser(tokenId, address(0), 0);
        }

        emit LoanRepaid(tokenId, msg.sender);
    }


    function getLoanTerms(uint256 tokenId) public view override returns (uint256, uint256, uint256, uint256) {
        LoanInfo storage info = _loans[tokenId];
        return (info.loanAmount, info.interestRate, info.loanDuration, info.loanStartTime);
    }

    function currentOwner(uint256 tokenId) public view override returns (address) {
        return ownerOf(tokenId);
    }

    function viewRepayAmount(uint256 tokenId) public view returns (uint256) {
        if (_loans[tokenId].loanAmount == 0) {
            // If the loan amount is zero, there is nothing to repay
            return 0;
        }

        // The interest is calculated on an hourly basis, prorated based on the actual duration for which the loan was held.
        // If the borrower repays before the loan duration ends, they are charged interest only for the time the loan was held.
        // For example, if the annual interest rate is 5% and the borrower repays in half the loan term, they pay only 2.5% interest.
        uint256 elapsed = block.timestamp > (_loans[tokenId].loanStartTime + _loans[tokenId].loanDuration) 
                        ? _loans[tokenId].loanDuration  / 1 hours
                        : (block.timestamp - _loans[tokenId].loanStartTime) / 1 hours;

        // Round up
        // Example: 15/4 = 3.75
        // round((15 + 4 - 1)/4) = 4, round((15/4) = 3)
        uint256 interest = ((_loans[tokenId].loanAmount * _loans[tokenId].interestRate / 100) * elapsed + (_loans[tokenId].loanDuration / 1 hours) - 1) / 
                    (_loans[tokenId].loanDuration / 1 hours);

        // Calculate the total amount due
        uint256 totalDue = _loans[tokenId].loanAmount + interest;

        return totalDue;
    }

    // Additional functions and logic to handle loan defaults, transfers, and other aspects of the NFT lifecycle
}

LoanInfo

struct LoanInfo {
    address borrower;   // Address that borrowed against the NFT
    uint256 loanAmount; // Amount of funds borrowed
    uint256 interestRate; // Interest rate for the loan
    uint64 loanDuration; // Duration of the loan
    uint256 loanStartTime; // Timestamp when the loan starts
}

概要

NFTに対するローンの詳細を格納するための構造体。

詳細

この構造体は、NFTを担保としてローンを受けた際の各種情報を管理するために使用されます。
ローンを受ける借り手のアドレス、借入額、利息率、ローン期間、ローン開始時刻といった重要な情報を含みます。

パラメータ

  • borrower
    • ローンを受けた借り手のアドレス。
  • loanAmount
    • 借り入れた資金の額。
  • interestRate
    • ローンの利息率。
  • loanDuration
    • ローンの期間(秒単位)。
  • loanStartTime
    • ローンが開始した時刻のタイムスタンプ。

_loans

mapping(uint256 => LoanInfo) internal _loans;

概要

_loansは、ローン情報を管理するためのマッピング。

詳細

このマッピングは、各NFTに対して一意のローン情報を関連付けるために使用されます。
キーはNFTのユニークな識別子(たとえばトークンID)で、値はLoanInfo構造体によって提供されるローンの詳細情報です。

パラメータ

  • uint256
    • NFTのユニークな識別子(トークンID)。
  • LoanInfo
    • 特定のNFTに対するローン情報を格納する構造体。

collateralize

function collateralize(uint256 tokenId, uint256 loanAmount, uint256 interestRate, uint64 loanDuration) public override {
    require(ownerOf(tokenId) == msg.sender || isApprovedForAll(ownerOf(tokenId), msg.sender) || getApproved(tokenId) == msg.sender, "Not owner nor approved");
    LoanInfo storage info = _loans[tokenId];
    info.borrower = msg.sender;
    info.loanAmount = loanAmount;
    info.interestRate = interestRate;
    info.loanDuration = loanDuration;
    info.loanStartTime = block.timestamp;
    setUser(tokenId, address(this), loanDuration);
    emit Collateralized(tokenId, msg.sender, loanAmount, interestRate, loanDuration);
}

概要

NFTを担保としてローンを受けるための関数。

詳細

この関数は、NFTの所有者がNFTを担保にしてローンを受ける際に使用します。
ローンの金額、利息率、期間を指定してローン契約を開始し、担保としてのNFTはコントラクトに一時的に譲渡されます。

引数

  • tokenId
    • 担保に使用するNFTのトークンID。
  • loanAmount
    • 借入れる資金の量。
  • interestRate
    • ローンの利息率。
  • loanDuration
    • ローンの期間(秒単位)。

repayLoan

function repayLoan(uint256 tokenId, uint256 repayAmount) public override {
    require(_loans[tokenId].borrower == msg.sender, "Not the borrower.");
    uint256 totalDue = viewRepayAmount(tokenId);
    require(repayAmount <= totalDue, "Repay amount exceeds total due.");
    _loans[tokenId].loanAmount = totalDue - repayAmount;
    if(_loans[tokenId].loanAmount == 0) {
        setUser(tokenId, address(0), 0);
    }
    emit LoanRepaid(tokenId, msg.sender);
}

概要

ローンを返済し、担保のNFTを取り戻すための関数。

詳細

この関数を使用すると、借り手はローンに対する返済を行うことができます。
完全な返済が行われると、担保として設定されたNFTは借り手に返却されます。

引数

  • tokenId
    • 返済するローンに関連付けられたNFTのトークンID。
  • repayAmount
    • 返済する金額。

getLoanTerms

function getLoanTerms(uint256 tokenId) public view override returns (uint256, uint256, uint256, uint256) {
    LoanInfo storage info = _loans[tokenId];
    return (info.loanAmount, info.interestRate, info.loanDuration, info.loanStartTime);
}

概要

特定のローン契約の条件を取得するための関数。

詳細

この関数は、指定されたNFTに対するローンの詳細な条件(ローン金額、利息率、ローン期間、ローン開始時刻)を返します。

引数

  • tokenId
    • ローン契約の条件を知りたいNFTのトークンID。

戻り値

  • ローンの金額、利息率、ローンの期間、ローン開始時刻。

currentOwner

function currentOwner(uint256 tokenId) public view override returns (address) {
    return ownerOf(tokenId);
}

概要

特定のNFTの現在の所有者を照会するための関数。

詳細

この関数は、指定されたトークンIDのNFTの現在の所有者のアドレスを返します。

引数

  • tokenId
    • 現在の所有者を知りたいNFTのトークンID。

戻り値

  • 指定されたNFTの現在の所有者のアドレス。

viewRepayAmount

function viewRepayAmount(uint256 tokenId) public view returns (uint256) {
    if (_loans[tokenId].loanAmount == 0) {
        return 0;
    }
    uint256 elapsed = block.timestamp > (_loans[tokenId].loanStartTime + _loans[tokenId].loanDuration) 
                    ? _loans[tokenId].loanDuration  / 1 hours
                    : (block.timestamp - _loans[tokenId].loanStartTime) / 1 hours;
    uint256 interest = ((_loans[tokenId].loanAmount * _loans[tokenId].interestRate / 100) * elapsed + (_loans[tokenId].loanDuration / 1 hours) - 1) / 
                (_loans[tokenId].loanDuration / 1 hours);
    uint256 totalDue = _loans[tokenId].loanAmount + interest;
    return totalDue;
}

概要

特定のNFTに関連するローンの現在の返済額を照会するための関数。

詳細

この関数は、返済が必要な総額(元本+利息)を計算して返します。
利息は、ローンが保持された実際の時間に基づいて時給ベースで計算されます。

引数

  • tokenId
    • 返済額を知りたいローンに関連付けられたNFTのトークンID。

戻り値

  • 指定されたローンの返済に必要な総額。

引用

Hyoungsung Kim (@HyoungsungKim) hyougnsung@keti.re.kr, Yong-Suk Park yspark@keti.re.kr, Hyun-Sik Kim hskim@keti.re.kr, "ERC-7565: Perpetual Contract NFTs as Collateral [DRAFT]," Ethereum Improvement Proposals, no. 7565, November 2023. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-7565.

最後に

今回は「流動性の確保と資産管理の効率化に焦点を当て、NFTを活用した新たな担保ローンシステムの仕組みを提案しているERC7565」についてまとめてきました!
いかがだったでしょうか?

質問などがある方は以下のTwitterのDMなどからお気軽に質問してください!

Twitter @cardene777

他の媒体でも情報発信しているのでぜひ他も見ていってください!

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0