Alert Source Discuss
🚧 Stagnant Standards Track: ERC

ERC-4353: NFT 中质押代币的接口

此接口能够访问 NFT 的公开可见的质押数据。

Authors Rex Creed (@aug2uag), Dane Scarborough <dane@nftapps.us>
Created 2021-10-08
Discussion Link https://ethereum-magicians.org/t/eip-4353-viewing-staked-tokens-in-nft/7234
Requires EIP-165

摘要

EIP-721 代币可以出于各种原因(包括托管、奖励、福利等)存入或质押在 NFT 中。目前没有检索质押和/或绑定到 NFT 的代币数量的方法。本提案概述了一个标准,所有钱包和市场都可以轻松地实施该标准,以正确检索 NFT 的质押代币数量。

动机

如果没有质押代币数据,质押代币的实际数量无法从代币所有者传递给其他用户,也无法在钱包、市场或区块浏览器中显示。识别和验证来自质押过程的外生价值的能力对于 NFT 持有者的目标可能至关重要。

规范

// SPDX-License-Identifier: CC0-1.0

pragma solidity ^0.8.0;

/**
 * @dev ERC4353 标准的接口,如
 * https://eips.ethereum.org/EIPS/eip-4353 中定义。
 *
 * 实现者可以声明对合约接口的支持,然后其他人可以
 * 查询这些接口。
 *
 * 注意:此接口的 ERC-165 标识符为 0x3a3d855f。
 *
 */
interface IERC721Staked {
    
     /**
     * @dev 返回质押给 NFT 的链上代币的 uint256 数量。
     * 
     * @dev 钱包和市场需要调用此函数来显示
     *      质押和/或绑定到 NFT 的代币数量。
     */
    function stakedAmount(uint256 tokenId) external view returns (uint256);
    
}

建议流程:

构造函数/部署

  • 创建者 - NFT 的所有者,对于在代币铸造时和/或之后存入代币有自己的规则。
  • 代币数量 - 从一个或多个存款绑定到 NFT 的当前链上 EIP-20 或派生代币的数量。
  • 提取机制 - 基于规则的方法,用于提取质押代币并确保更新质押代币的余额。

在铸造时质押并将代币锁定在 NFT 中

此标准的建议和预期实现是在铸造 NFT 时质押代币,而不实现任何 burn 之外的代币出站转移。因此,仅在铸造时质押,仅在销毁时提取。

在钱包或市场中显示的 NFT

钱包或市场检查 NFT 是否有可公开显示的质押代币 - 如果是,则调用 stakedAmount(tokenId) 以获取质押和/或绑定到 NFT 的当前代币数量。

逻辑代码如下所示,并受到 William Entriken 的启发:

// 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 非常简单的 ERC721 示例,带有质押接口示例。
 * 请注意,此实现强制执行推荐的程序:
 *  1) 在铸造时质押
 *  2) 在销毁时提取
 */
contract ERC721Staked is ERC721URIStorage, Ownable {
    /// @dev 追踪 tokenId 的原始铸造者
    mapping (uint256 => address payable) private payees;
    /// @dev 将代币映射到存储的质押代币值
    mapping (uint256 => uint256) private tokenValue;

    /// @dev 元数据
    constructor() ERC721 (
        "Staked NFT", 
        "SNFT"
    ){}

    /// @dev 铸造新的 NFT
    /// @param _to 将拥有铸造的 NFT 的地址
    /// @param _tokenId NFT 的 ID
    /// @param _uri 元数据
    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 质押接口
    /// @param _tokenId NFT 的 ID
    /// @return _value 质押值
    function stakedAmount(
        uint256 _tokenId
    ) external view returns (uint256 _value) {
        _value = tokenValue[_tokenId];
        return _value;
    }

    /// @dev 移除 NFT 并将加密货币转移给铸造者
    /// @param _tokenId 我们要移除的 NFT
    function burn(
        uint256 _tokenId
    )
        external
        onlyOwner
    {
        super._burn(_tokenId);
        payees[_tokenId].transfer(tokenValue[_tokenId]);
        tokenValue[_tokenId] = 0;
    }

}

原理

此标准完全不考虑代币如何被 NFT 存入或处理。因此,作者有责任对他们的代币经济学进行编码并传达给代币的购买者,和/或使他们的合约可供购买者查看。

尽管此标准的目的是在铸造时质押代币,并且仅在销毁时可提取,但该接口可以修改为动态提取和存入代币,尤其是在 DeFi 应用程序设置下。以目前的形式,合约逻辑可能是确定是否存在偏离标准的决定因素。

向后兼容性

待定

测试用例

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'));
        });
    });
});

安全考虑

此标准的目的是简单且公开地识别 NFT 是否声明拥有质押代币。

如果没有强制执行锁定机制,质押声明将不可靠,例如,如果质押代币只能在销毁时转移。否则,可以随时通过任意方法存入和/或提取代币。此外,可能允许任意转移而不更新正确余额的合约将导致潜在问题。应考虑到这些极端情况,采取严格的基于规则的方法。

可能存在一个专用服务,通过分析浏览器上的交易来验证代币的声明。通过这种方式,可以自动验证以确保代币的声明有效。此方法的逻辑扩展可能是扩展接口并支持标记错误声明,同时保持验证和验证质押金额存在的简单目标,以使运营商受益。

版权

版权及相关权利通过 CC0 放弃。

Citation

Please cite this document as:

Rex Creed (@aug2uag), Dane Scarborough <dane@nftapps.us>, "ERC-4353: NFT 中质押代币的接口 [DRAFT]," Ethereum Improvement Proposals, no. 4353, October 2021. [Online serial]. Available: https://eips.ethereum.org/EIPS/eip-4353.