はじめに

『DApps開発入門』という本や色々記事を書いているかるでねです。

今回は、ステーキングされているERC721形式のNFTのトークン量をよりセキュアに取得する仕組みを提案しているERC4353についてまとめていきます!

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

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

概要

ERC4353は、NFT(ERC721)にステーキングされたトークンの数量を取得するインターフェースを提案しています。 現在、多くのユースケース(例:エスクロー、報酬、特典など)でNFTに他のトークンを預けたり結びつけられていますが、「ステーキングされたトークンの量」を取得する共通の方法が存在しません。 ERC4353により、ウォレットやマーケットプレイスがステーキングされたトークン情報を正しく取得・表示できるようになります。

動機

現状、NFTに紐づいたステーキング情報がオンチェーン上に存在していたとしても、それを標準的に取得する手段がないため、他のユーザーにその情報を伝えることができません。 その結果、ウォレットやマーケットプレイス、ブロックエクスプローラーなどでステーキング状況を可視化できず、NFTの本来の価値が適切に評価されない可能性があります。

NFT保有者にとって、ステーキングによって得られる外部的な価値は非常に重要な要素であり、その情報の可視化・検証が可能になることはNFTの信頼性や市場価値を高める上で重要です。

仕様

IERC721Staked

IERC721Staked.sol
// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.0;

/**
 * @dev Interface of the ERC4353 standard, as defined in the
 * https://eips.ethereum.org/EIPS/eip-4353.
 *
 * Implementers can declare support of contract interfaces, which can then be
 * queried by others.
 *
 * Note: The ERC-165 identifier for this interface is 0x3a3d855f.
 *
 */
interface IERC721Staked {
    
     /**
     * @dev Returns uint256 amount of on-chain tokens staked to the NFT.
     * 
     * @dev Wallets and marketplaces would need to call this for displaying
     *      the amount of tokens staked and/or bound to the NFT.
     */
    function stakedAmount(uint256 tokenId) external view returns (uint256);
    
}

stakedAmount

特定の tokenId に紐づいたステーキング済みのトークン量(uint256)を返す関数。 ウォレットやマーケットプレイスはこの関数を呼び出すことで、NFTに現在どれだけのトークンがステークされているかを表示できます。 ERC165を使用して、インターフェースをサポートしているかを確認可能です(インターフェースIDは 0x3a3d855f)。

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

実装のフロー

ERC4353で推奨されている実装方法は、NFTのミント時にトークンをステークし、Burn時のみステークされたトークンを引き出せるようにする方法です。

ステーク時のフロー

NFTの作成者(Creator)が、Mint時か任意のタイミングでNFTに対してERC20トークンを預け入れます。 一度ステークされたトークンは、基本的にNFTがBurnされるまで外部へ引き出すことはできません(transfer不可)。 これにより、ステークされたトークンとNFTの結びつきが強化され、信頼性や資産価値の保証につながります。

引き出しの仕組み

トークンの引き出しは、NFTのBurn処理に付随して行うことを想定しています。 トークンの残高情報は、ステーキングやBurnのタイミングで適切に更新する必要があります。

ウォレットやマーケットプレイスでの表示処理

ウォレットやマーケットプレイスは、NFTがステーキングトークンを保持しているかどうかを確認し、視覚的に表示するために stakedAmount(tokenId) を呼び出します。

参考実装

ERC721Staked.sol
// contracts/Token.sol
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
import "@openzeppelin/contracts/access/Ownable.sol";

/**
 * @title Token
 * @dev Very simple ERC721 example with stake interface example.
 * Note this implementation enforces recommended procedure:
 *  1) stake at mint
 *  2) withdraw at burn
 */
contract ERC721Staked is ERC721URIStorage, Ownable {
    /// @dev track original minter of tokenId
    mapping (uint256 => address payable) private payees;
    /// @dev map tokens to stored staked token value
    mapping (uint256 => uint256) private tokenValue;

    /// @dev metadata
    constructor() ERC721 (
        "Staked NFT", 
        "SNFT"
    ){}

    /// @dev mints a new NFT
    /// @param _to address that will own the minted NFT
    /// @param _tokenId id the NFT
    /// @param _uri metadata
    function mint(
        address payable _to,
        uint256 _tokenId,
        string calldata _uri
    )
        external 
        payable
        onlyOwner
    {
        _mint(_to, _tokenId);
        _setTokenURI(_tokenId, _uri);
        payees[_tokenId] = _to;
        tokenValue[_tokenId] = msg.value;
    }

    /// @dev staked interface
    /// @param _tokenId id of the NFT
    /// @return _value staked value
    function stakedAmount(
        uint256 _tokenId
    ) external view returns (uint256 _value) {
        _value = tokenValue[_tokenId];
        return _value;
    }

    /// @dev removes NFT & transfers crypto to minter
    /// @param _tokenId the NFT we want to remove
    function burn(
        uint256 _tokenId
    )
        external
        onlyOwner
    {
        super._burn(_tokenId);
        payees[_tokenId].transfer(tokenValue[_tokenId]);
        tokenValue[_tokenId] = 0;
    }

}

補足

ERC4353は、トークンがどのようにNFTへ預け入れられ、管理されるかには一切関与しません。 つまり、NFTにおけるトークンエコノミクスの設計や実装は、あくまでコントラクト作成者(発行者)の責任となります。 発行者は、購入者に対してどのような仕組みでステーキングが行われるかを適切に伝える必要があります。

ERC4353で推奨される運用は「Mint時にステーキングし、Burn時に引き出し可能とする」という方法ですが、DeFi用途などでは動的なステーキングと引き出しが求められる場合もあるため、状況に応じてインターフェースを拡張する余地もあります。

テスト

js
const { expect } = require("chai");
const { ethers, waffle } = require("hardhat");
const provider = waffle.provider;

describe("StakedNFT", function () {
    let _id = 1234567890;
    let value = '1.5';
    let Token;
    let Interface;
    let owner;
    let addr1;
    let addr2;

    beforeEach(async function () {
        Token = await ethers.getContractFactory("ERC721Staked");
        [owner, addr1, ...addr2] = await ethers.getSigners();
        Interface = await Token.deploy();
    });

    describe("Staked NFT", function () {
        it("Should set the right owner", async function () {
            let mint = await Interface.mint(
                addr1.address, _id, 'http://foobar')
            expect(await Interface.ownerOf(_id)).to.equal(addr1.address);
        });

        it("Should not have staked balance without value", async function () {
            let mint = await Interface.mint(
                addr1.address, _id, 'http://foobar')
            expect(await Interface.stakedAmount(_id)).to.equal(
                ethers.utils.parseEther('0'));
        });

        it("Should set and return the staked amount", async function () {
            let mint = await Interface.mint(
                addr1.address, _id, 'http://foobar',
                {value: ethers.utils.parseEther(value)})
            expect(await Interface.stakedAmount(_id)).to.equal(
                ethers.utils.parseEther(value));
        });

        it("Should decrease owner eth balance on mint (deposit)", async function () {
            let balance1 = await provider.getBalance(owner.address);
            let mint = await Interface.mint(
                addr1.address, _id, 'http://foobar',
                {value: ethers.utils.parseEther(value)})
            let balance2 = await provider.getBalance(owner.address);
            let diff = parseFloat(ethers.utils.formatEther(
                balance1.sub(balance2))).toFixed(1);
            expect(diff === value);
        });

        it("Should add to payee's eth balance on burn (withdraw)", async function () {
            let balance1 = await provider.getBalance(addr1.address);
            let mint = await Interface.mint(
                addr1.address, _id, 'http://foobar',
                {value: ethers.utils.parseEther(value)})
            await Interface.burn(_id);
            let balance2 = await provider.getBalance(addr1.address);
            let diff = parseFloat(ethers.utils.formatEther(
                balance2.sub(balance1))).toFixed(1);
            expect(diff === value);
        });

        it("Should update balance after transfer", async function () {
            let mint = await Interface.mint(
                addr1.address, _id, 'http://foobar',
                {value: ethers.utils.parseEther(value)})
            await Interface.burn(_id);
            expect(await Interface.stakedAmount(_id)).to.equal(
                ethers.utils.parseEther('0'));
        });
    });
});

セキュリティ

ロック機構の導入が不可欠

例えば、「Burn時のみトークン引き出しが可能」というように、預け入れたトークンをロックしないと任意のタイミングで引き出しが可能になってしまい、表示されたステーキング量が実際とは異なることにつながります。

トークン残高の更新処理に注意

コントラクトの設計によっては、トークンを自由にtransferできるにも関わらず stakedAmount の値が更新されないと、誤った情報を表示することになります。

そのため、ルールベースの厳格な実装が求められます。 加えて、ブロックチェーン上のトランザクションを分析してNFTのステーク情報を検証する専用の検証サービスの導入も考えられます。

引用

Rex Creed (@aug2uag), Dane Scarborough dane@nftapps.us, "ERC-4353: Interface for Staked Tokens in NFTs [DRAFT]," Ethereum Improvement Proposals, no. 4353, October 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4353.

最後に

今回は「ステーキングされているERC721形式のNFTのトークン量をよりセキュアに取得する仕組みを提案しているERC4353」についてまとめてきました! いかがだったでしょうか?

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

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