本文详细介绍了ERC-404标准,该标准结合了ERC-20和ERC-721的特点,允许创建既可作为同质化代币又可作为非同质化代币的资产。文章涵盖了ERC-404的功能、合约部署、测试以及如何在Uniswap V3上创建流动性池等内容。
本指南详细介绍了 ERC-404 标准,它代表了已广泛使用的 ERC-20 和 ERC-721 标准的新颖整合。它面向那些有兴趣扩展其在以太坊区块链上复杂智能合约开发和部署策略专业知识的开发者。
随着你逐步学习本指南,你将了解更多关于 ERC-404 标准的内容,从功能细节到合约部署。
ERC-404 使用注意事项
请注意,ERC-404 是一个实验性且目前未经审计的标准,可能包含尚未发现的安全漏洞。ERC-404 尚未正式成为 EIP,并且尚未通过严格的 EIP 和 ERC 验证流程。由于未知的安全缺陷,增加了资金损失的风险。在这些初步阶段,请谨慎行事,避免在 ERC-404 项目中投入超过你承受能力的资金。
ERC-404 是由 Pandora 团队 设计的一种新的非官方智能合约标准。它旨在将 ERC-20(可替代代币)和 ERC-721(不可替代代币或 NFT)的特性合并到一个标准中。该标准允许创建既可以作为可替代代币用于质押或交易等用例,又可以作为唯一的不可替代代币来表示独特所有权的数字资产。
当用户购买一定数量的 ERC-404 代币或从另一个钱包接收它们时,合约不仅会更新可替代代币的余额,还可以向接收者铸造一个唯一的 ERC-721 NFT。这个 NFT 可以代表一种特殊权利、会员资格或与可替代代币相关的独特资产的一部分所有权。相反,出售或转移可替代代币可能会触发相关 NFT 的转移,确保所有权权利与可替代代币余额一起正确维护和转移。
ERC-404 的关键特性包括:
混合性质:虽然 ERC-20 专注于可替代代币(相同且可互换)而 ERC-721 专注于不可替代代币(唯一且不可互换),但 ERC-404 利用了这两种代币标准,允许在同一智能合约中同时具备可替代和不可替代功能。这种功能类似于已经存在的 ERC-1155 标准,它也允许从单一合约中进行相同类型的代币操作。
NFT 的本地分片化:与标准的 ERC-721 不同,在 ERC-721 中,NFT 代表一个完整的、不可分割的资产,而 ERC-404 引入了对 NFT 分片所有权的本地支持。这意味着用户可以拥有 NFT 的一部分,从而增强了高价值资产的流动性和可访问性。
增强的流动性:通过允许分片所有权,ERC-404 克服了传统 NFT 的主要限制之一——缺乏流动性。它使小型投资者能够参与高价值资产的所有权,并促进在交易所中更轻松的交易。
动态功能:ERC-404 代币可以根据交易上下文充当可替代或不可替代资产。例如,当从另一个用户购买或接收代币时,合约可以自动分配 ERC-721 NFT 以表示特定的所有权权利或成就,同时还可以无缝处理可替代代币交易。
ERC-404 引入了一组函数,允许在一个合约内精细处理可替代和不可替代代币的各个方面。让我们解释 ERC-404 合约中的每个函数和组件:
所有者、支出者和操作者
在讨论 ERC-404 合约的函数之前,让我们定义一些关键术语和角色以避免混淆:
所有者:持有代币所有权的实体或地址。在 NFT(ERC-721 代币)的上下文中,所有者拥有一个唯一的代币。对于可替代代币(ERC-20),所有者持有一定数量的代币。
支出者:已被所有者授予权限的地址,可以代表所有者转移指定数量的可替代代币(ERC-20)或特定的 NFT(ERC-721)。
操作者:被所有者批准管理其所有代币(包括可替代和不可替代代币)的实体或地址。这个角色比支出者更广泛,因为它可以涵盖合约内所有者所有资产的管理。
使用 name、symbol、decimals、可替代代币的总供应量 和合约 owner 初始化合约。
在本指南中,我们将使用 以太坊 Sepolia 测试网。然而,本指南中的代码适用于所有 EVM 兼容的主网和测试网,如以太坊、Polygon 和 Arbitrum。
要在以太坊 Sepolia 测试网上构建,你需要一个 API 端点来连接网络。你可以使用公共节点或部署和管理自己的基础设施;但是,如果你希望获得更快的响应时间,可以将繁重的工作交给我们。在此处注册一个免费账户 here。
登录后,点击 创建端点,然后选择 以太坊 Sepolia 区块链。
创建端点后,复制 HTTP Provider 链接并妥善保存,因为你接下来会需要它。

我们需要获取一些测试 ETH 以支付我们智能合约的部署和交互费用。
导航到 QuickNode 多链水龙头 并连接你的钱包或粘贴你的钱包地址。你需要选择 以太坊 链和 Sepolia 网络,然后请求资金。
首先初始化一个新的 Node.js 项目并安装 Hardhat,这是一个专为以太坊和其他基于 EVM 的区块链上的智能合约开发而设计的开发环境。
打开你的终端并导航到你想要的目录。然后,运行以下命令以在新创建的 erc404-project 目录中创建一个新的 Node.js 项目。
mkdir erc404-project && cd erc404-project
npm init -y
在你的项目目录中,运行以下命令。
npm install --save-dev hardhat
在终端中运行以下命令。按照提示创建一个新的 Hardhat 项目。在提示时选择创建一个带有所有默认选项的基本 TypeScript 项目。
npx hardhat init
npm install --save-dev @openzeppelin/contracts dotenv
dotenv 库对于存储敏感数据(如你的私钥和 QuickNode 端点 URL)至关重要。创建一个 .env 文件。
echo > .env
然后,打开 .env 文件并粘贴以下内容。将 YOUR_QUICKNODE_ENDPOINT_HTTP_URL 和 YOUR_WALLET_PRIVATE_KEY 替换为你的 QuickNode 端点 URL 和你的钱包的私钥(用于签署交易)。
.env
HTTP_PROVIDER_URL="YOUR_QUICKNODE_ENDPOINT_HTTP_URL"
PRIVATE_KEY="YOUR_WALLET_PRIVATE_KEY"
hardhat.config.ts 文件包括所有与 Hardhat 相关的设置,如 Solidity 编译器版本、网络等。
hardhat.config.ts
import { HardhatUserConfig } from "hardhat/config";
import "@nomicfoundation/hardhat-toolbox";
import dotenv from "dotenv";
dotenv.config();
const config: HardhatUserConfig = {
  solidity: "0.8.20",
  networks: {
    sepolia: {
      url: process.env.HTTP_PROVIDER_URL,
      accounts: [process.env.PRIVATE_KEY as string],
    },
  },
  gasReporter: { enabled: true },
};
export default config;
确保你的 tsconfig.json 文件除了 compilerOptions 属性外,还包括 exclude、include 和 files 属性。
tsconfig.json
{
    "compilerOptions": {
        "target": "es2020",
        "module": "commonjs",
        "esModuleInterop": true,
        "forceConsistentCasingInFileNames": true,
        "strict": true,
        "skipLibCheck": true,
        "resolveJsonModule": true
    },
    "exclude": ["dist", "node_modules"],
    "include": ["./test", "./src", "./scripts", "./typechain-types"],
    "files": ["./hardhat.config.ts"]
}
为了构建我们的 ERC-404 代币合约,我们将开发两个独立的 Solidity 合约:
在你的项目目录中运行以下命令。
echo > contracts/ERC404.sol
echo > contracts/My404.sol
此外,你可以删除 contracts 目录下的任何其他合约文件。
打开 ERC404.sol 文件,并将以下代码粘贴到文件中。
信息
ERC-404 合约中的 transferFrom 函数基于 amountOrId 参数将可替代和不可替代代币转移混为一谈,没有明确区分,可能导致意外行为或错误。如果 amountOrId 旨在表示可替代代币数量,但偶然与现有代币 ID 匹配,该函数可能会错误地将转移视为 NFT 操作,更改唯一资产的所有权,而不是转移可替代数量。这种模糊性可能导致混淆和意外的资产损失。
花时间检查代码和相关注释,以充分理解函数的功能。要查看 ERC404 标准代码的来源,请查看 这里。
contracts/ERC404.sol
//SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.0;
abstract contract Ownable {
    event OwnershipTransferred(address indexed user, address indexed newOwner);
    error Unauthorized();
    error InvalidOwner();
    address public owner;
    modifier onlyOwner() virtual {
        if (msg.sender != owner) revert Unauthorized();
        _;
    }
    constructor(address _owner) {
        if (_owner == address(0)) revert InvalidOwner();
        owner = _owner;
        emit OwnershipTransferred(address(0), _owner);
    }
    function transferOwnership(address _owner) public virtual onlyOwner {
        if (_owner == address(0)) revert InvalidOwner();
        owner = _owner;
        emit OwnershipTransferred(msg.sender, _owner);
    }
    function revokeOwnership() public virtual onlyOwner {
        owner = address(0);
        emit OwnershipTransferred(msg.sender, address(0));
    }
}
abstract contract ERC721Receiver {
    function onERC721Received(
        address,
        address,
        uint256,
        bytes calldata
    ) external virtual returns (bytes4) {
        return ERC721Receiver.onERC721Received.selector;
    }
}
/// @notice ERC404
///         A gas-efficient, mixed ERC20 / ERC721 implementation
///         with native liquidity and fractionalization.
///
///         This is an experimental standard designed to integrate
///         with pre-existing ERC20 / ERC721 support as smoothly as
///         possible.
///
/// @dev    In order to support full functionality of ERC20 and ERC721
///         supply assumptions are made that slightly constraint usage.
///         Ensure decimals are sufficiently large (standard 18 recommended)
///         as ids are effectively encoded in the lowest range of amounts.
///
///         NFTs are spent on ERC20 functions in a FILO queue, this is by
///         design.
///
abstract contract ERC404 is Ownable {
    // Events
    event ERC20Transfer(
        address indexed from,
        address indexed to,
        uint256 amount
    );
    event Approval(
        address indexed owner,
        address indexed spender,
        uint256 amount
    );
    event Transfer(
        address indexed from,
        address indexed to,
        uint256 indexed id
    );
    event ERC721Approval(
        address indexed owner,
        address indexed spender,
        uint256 indexed id
    );
    event ApprovalForAll(
        address indexed owner,
        address indexed operator,
        bool approved
    );
    // Errors
    error NotFound();
    error AlreadyExists();
    error InvalidRecipient();
    error InvalidSender();
    error UnsafeRecipient();
    // Metadata
    /// @dev Token name
    string public name;
    /// @dev Token symbol
    string public symbol;
    /// @dev Decimals for fractional representation
    uint8 public immutable decimals;
    /// @dev Total supply in fractionalized representation
    uint256 public immutable totalSupply;
    /// @dev Current mint counter, monotonically increasing to ensure accurate ownership
    uint256 public minted;
    // Mappings
    /// @dev Balance of user in fractional representation
    mapping(address => uint256) public balanceOf;
    /// @dev Allowance of user in fractional representation
    mapping(address => mapping(address => uint256)) public allowance;
    /// @dev Approval in native representaion
    mapping(uint256 => address) public getApproved;
    /// @dev Approval for all in native representation
    mapping·------------------------------|----------------------------|-------------|-----------------------------·
|     Solc 版本: 0.8.20     ·  优化器启用: false  ·  运行次数: 200  ·  区块限制: 30000000 gas  │
·······························|····························|·············|······························
|  方法                                                                                              │
·············|·················|··············|·············|·············|···············|··············
|  合约  ·  方法         ·  最小值         ·  最大值        ·  平均值        ·  调用次数      ·  usd (平均值)  │
·············|·················|··············|·············|·············|···············|··············
|  My404     ·  setDataURI     ·           -  ·          -  ·      47631  ·            1  ·          -  │
·············|·················|··············|·············|·············|···············|··············
|  My404     ·  setNameSymbol  ·           -  ·          -  ·      37145  ·            1  ·          -  │
·············|·················|··············|·············|·············|···············|··············
|  My404     ·  setTokenURI    ·       47563  ·      47587  ·      47575  ·            2  ·          -  │
·············|·················|··············|·············|·············|···············|··············
|  部署                       ·                                          ·  限制百分比   ·             │
·······························|··············|·············|·············|···············|··············
|  My404                       ·           -  ·          -  ·    3081303  ·       10.3 %  ·          -  │
·------------------------------|--------------|-------------|-------------|---------------|-------------·
根据 gas 报告,部署 My404 合约需要 3081303 gas。假设 gas 价格为 10 gwei/gas。因此,部署 My404 合约至少需要 0.03081303 ether。请注意,实际 gas 费用可能会根据部署时的网络流量而有所不同。
(0.00000001 ether/gas) * (3081303 gas) = 0.03081303 ether
现在,是时候将合约部署到区块链上了!
在继续部署之前,请确保你有足够的 ETH 来部署合约,因为我们在上一节中计算了所需的 ETH 数量。
在项目目录中运行以下命令。
echo > scripts/deploy.ts
此外,你可以删除 scripts 目录下的任何其他脚本文件。
此脚本旨在将 My404 智能合约部署到区块链,然后将部署者的地址加入白名单。
关于 setWhitelist 功能的额外说明
setWhitelist 函数在 My404 合约的操作灵活性中起着至关重要的作用。通过允许特定地址在不铸造或销毁相应 NFT 的情况下转移代币,合约所有者可以更有效地管理代币供应。此功能在初始分发阶段尤为重要,因为所有者拥有所有初始代币供应,可能需要分发代币而不受销毁 NFT 的要求限制。
以下是添加了注释的详细说明:
scripts/deploy.ts
// 从 Hardhat 包中导入必要的功能。
import { ethers } from 'hardhat'
async function main() {
    // 检索第一个签名者,通常是 Hardhat 中的默认账户,用作部署者。
    const [deployer] = await ethers.getSigners()
    console.log('合约正在部署...')
    // 部署 My404 合约,将部署者的地址作为构造函数参数传递。
    const my404 = await ethers.deployContract('My404', [deployer.address])
    // 等待合约部署在区块链上得到确认。
    await my404.waitForDeployment()
    // 记录部署的 My404 合约的地址。
    console.log(`My404 合约已部署。代币地址:${my404.target}`)
    console.log('部署者地址正在加入白名单...')
    // 调用已部署合约的 setWhitelist 函数,将部署者的地址加入白名单。
    // 这使得部署者可以在没有相应 NFT 的情况下转移代币,
    // 这对于初始设置或特定的操作例外至关重要。
    const tx = await my404.setWhitelist(deployer.address, true)
    await tx.wait() // 等待交易被挖出。
    console.log(`将部署者地址加入白名单的交易哈希:${tx.hash}`)
}
// 这种模式允许在整个过程中使用 async/await,并确保错误被捕获并正确处理。
main().catch(error => {
    console.error(error)
    process.exitCode = 1
})
执行以下命令来部署你的 ERC-404 合约。
npx hardhat run scripts/deploy.ts --network sepolia
输出应如下所示。
合约正在部署...
My404 合约已部署。代币地址:0x18E7e3b02286bB6A405909552cfFbD61Da0d09A4
部署者地址正在加入白名单...
将部署者地址加入白名单的交易哈希:0xb2fdbea51ed123c92c781673c72ffce9f26741cb7b6a5dbc43ea25363f585e86
恭喜,你刚刚部署了一个 ERC-404 合约!
现在,让我们尝试将一些 My404 代币发送到另一个地址。目前,部署者(所有者)拥有所有代币供应。
你可以通过以太坊钱包(例如 MetaMask、Rabby 等)或借助脚本来发送代币。我们将向你展示如何使用脚本来完成此操作。
在项目目录中运行以下命令。
echo > scripts/transferToken.ts
此脚本旨在将一定数量的 ERC-404 代币转移到另一个地址。将 ANOTHER_ADDRESS 和 MY404_CONTRACT_ADDRESS 占位符替换为你想要发送 20 个 My404 代币的地址和 My404 代币地址。
scripts/transferToken.ts
import { ethers } from 'hardhat'
async function main() {
    const toAddress = 'ANOTHER_ADDRESS'
    const contractAddress = 'MY404_CONTRACT_ADDRESS'
    console.log('正在发送 My404 代币...')
    const my404 = await ethers.getContractAt('My404', contractAddress)
    const tx = await my404.transfer(toAddress, ethers.parseEther('20'))
    tx.wait()
    console.log(`发送 My404 代币的交易哈希:${tx.hash}`)
}
// 我们推荐这种模式,以便能够随处使用 async/await
// 并正确处理错误。
main().catch(error => {
    console.error(error)
    process.exitCode = 1
})
运行以下命令。
npx hardhat run scripts/transferToken.ts --network sepolia
如果你在区块浏览器(如 Etherscan)上检查交易哈希,你会看到 20 个 NFT 被转移到同一个地址,同时 20 个 My404 代币也被转移。

拥有一个经过验证的合约是在社区中建立信任的好方法,因为未经验证的合约可能包含一些可疑的功能。因此,你可能希望在部署后添加额外的步骤,例如在 Etherscan 上验证合约源代码。如果是这样,请查看我们的 验证指南。
如果你希望为你的 ERC-404 代币增强流动性和交易机会,在 Uniswap V3 上创建流动性池是一个战略步骤。本节概述了如何在 Uniswap V3 平台上为你的 ERC-404 代币和 WETH(Wrapped Ether)建立流动性池。
你需要将 Uniswap 的 NonfungiblePositionManager 和 UniswapV3Factory 合约的 ABI 文件保存在项目的 abis 目录中,以便能够与这些合约进行交互。
在项目目录中运行以下命令。
mkdir abis
echo > abis/NonfungiblePositionManager.json
echo > abis/UniswapV3Factory.json
然后,前往 Etherscan 的每个合约页面,复制它们的 ABI,并修改你的 ABI 文件。
0x1238536071E1c677A632429e3655c799b22cDA520x0227628f3F023bb0B980b67D528571c95c6DaC1c代币地址(token0 和 token1):这些地址定义了流动性池中的交易对。你的 ERC-404 代币和 WETH 用于在此创建一个市场,以便与广泛使用且稳定的价值参考 WETH 进行交易。
费用等级(fee):费用等级影响交易者支付的费用以及流动性提供者的潜在收益。选择 1% 的费用等级作为示例,在潜在收益和交易者吸引力之间取得平衡。
初始价格(sqrtPriceX96):初始价格设置了池中交易的起点。它以 Uniswap V3 使用的特定格式编码,反映了两种代币之间的期望价格比率。
运行以下命令以创建将用于在 Uniswap V3 上初始化流动性池的脚本文件。
echo > scripts/poolInitializer.ts
然后,将其修改为如下内容。
将 YOUR_ERC404_TOKEN_ADDRESS 占位符替换为你的 ERC404 代币地址。
如果你使用除以太坊 Sepolia 之外的任何其他区块链,请查看 Uniswap 文档 以获取合约地址,并检查 WETH 代币地址。
scripts/poolInitializer.ts
import { ethers } from 'hardhat'
import NonfungiblePositionManagerABI from '../abis/NonfungiblePositionManager.json'
import UniswapV3Factory from '../abis/UniswapV3Factory.json'
async function main() {
    const [deployer] = await ethers.getSigners()
    console.log('My404 - WETH 池正在 Uniswap V3 上初始化...')
    const token0 = 'YOUR_ERC404_TOKEN_ADDRESS' // MY404 代币在 Sepolia 上的地址
    const token1 = '0xfFf9976782d46CC05630D1f6eBAb18b2324d6B14' // WETH 代币在 Sepolia 上的地址
    const fee = 10000n // 1% 费用
    const sqrtPriceX96 = 792281625000000000000000000n // 1/10000 价格比率
    // 定义代币和流动性池参数:
    // - token0 和 token1 表示池中的代币对,使用你的 ERC-404 代币和 WETH。
    // - fee 表示池的费用等级,影响交易费用和流动性提供者的潜在收益。
    // - sqrtPriceX96 是一个编码值,表示池的初始价格,基于期望的价格比率设置。
    const contractAddress = {
        uniswapV3NonfungiblePositionManager:
            '0x1238536071E1c677A632429e3655c799b22cDA52',
        uniswapV3Factory: '0x0227628f3F023bb0B980b67D528571c95c6DaC1c',
    }
    // 用于与 Uniswap V3 的 NonfungiblePositionManager 和 Factory 交互的合约实例。
    const nonfungiblePositionManagerContract = new ethers.Contract(
        contractAddress.uniswapV3NonfungiblePositionManager,
        NonfungiblePositionManagerABI,
        deployer
    )
    const uniswapV3FactoryContract = new ethers.Contract(
        contractAddress.uniswapV3Factory,
        UniswapV3Factory,
        deployer
    )
    const my404Contract = await ethers.getContractAt('My404', token0, deployer)
    // 通过指定代币、费用和初始价格在 Uniswap V3 上创建流动性池。
    let tx =
        await nonfungiblePositionManagerContract.createAndInitializePoolIfNecessary(
            token0,
            token1,
            fee,
            sqrtPriceX96
        )
    await tx.wait()
    console.log(`在 Uniswap V3 上初始化流动性池的交易哈希:${tx.hash}`)
    // 检索新创建的流动性池的地址,以便进一步与之交互。
    const pool = await uniswapV3FactoryContract.getPool(token0, token1, fee)
    console.log(`流动性池地址:${pool}`)
    // 将 Uniswap V3 流动性池地址加入你的 ERC-404 代币合约的白名单。
    // 此步骤对于绕过代币的内置保护或要求至关重要,这些保护或要求可能在 Uniswap 上的流动性提供或交易期间触发。
    console.log('Uniswap V3 流动性池地址正在加入白名单...')
    tx = await my404Contract.setWhitelist(pool, true)
    tx.wait()
    console.log(`将 Uniswap V3 流动性池加入白名单的交易哈希:${tx.hash}`)
}
// 我们推荐这种模式,以便能够随处使用 async/await
// 并正确处理错误。
main().catch(error => {
    console.error(error)
    process.exitCode = 1
})
执行以下命令以初始化你的 Uniswap V3 流动性池。
npx hardhat run scripts/poolInitializer.ts --network sepolia
初始化流动性池后,你可以通过 Uniswap 的前端或脚本文件向池中添加流动性。

要使用 IPFS 为你的 ERC-404 代币配置 NFT 元数据,你主要需要与智能合约中的 setTokenURI 函数进行交互。此函数允许你设置代币元数据的基本 URI,该 URI 可以是一个指向存储元数据位置的 IPFS 链接。通过将元数据存储在 IPFS 上,你可以确保其去中心化且不可篡改,符合区块链技术的精神。
要了解更多关于在 NFT 项目中使用 IPFS 的信息,请查看这些 QuickNode 指南的 IPFS 相关部分:
总之,本指南展示了开发、部署和管理 ERC-404 代币的过程,从使用 Hardhat 进行初始设置到在 Uniswap V3 上创建流动性。通过遵循提供的详细步骤,你将能够启动一个利用 ERC-404 混合功能独特优势的代币。请记住,成功的区块链项目关键在于彻底的测试、安全审计和持续学习。
订阅我们的 新闻通讯 以获取更多关于 Web3 和区块链的文章和指南。如果你有任何问题或需要进一步的帮助,请随时加入我们的 Discord 服务器或使用下面的表单提供反馈。通过关注我们的 Twitter (@QuickNode) 和我们的 Telegram 公告频道 保持最新动态。
让我们知道 如果你有任何反馈或新主题的请求。我们很乐意听取你的意见。
- 原文链接: quicknode.com/guides/eth...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
 
                如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!