You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

ERC-721同步铸造ERC-20代币失败及转账余额不足问题

问题:ERC20转账余额不足错误排查与修复

合约代码

ERC20 合约(FNFToken)

contract FNFToken is ERC20, ERC20Burnable, Ownable, IERC721Receiver
 {
    constructor() ERC20("FNFToken", "FNT") {}

    function mint(address to, uint256 amount) public onlyOwner {
        _mint(to, amount);
    }


    function onERC721Received(
        address,
        address,
        uint256,
        bytes memory
    ) public virtual override returns (bytes4) {
        return this.onERC721Received.selector;
    }
}

ERC721 合约(FractionalNFT)

contract FractionalNFT is ERC721, ERC721Enumerable, ERC721URIStorage, Pausable, Ownable, ERC721Burnable,IERC721Receiver {
    using Counters for Counters.Counter;

    Counters.Counter private _tokenIdCounter;

    struct _fnft{
        uint256 tokenId;
        address fractionalToken;
    }

    mapping(uint256 => _fnft) public FNFT;

    constructor() ERC721("FractionalNFT", "FNFT") {}

    function safeMint(address to) public onlyOwner {
        _safeMint(to, _tokenIdCounter.current());
        _tokenIdCounter.increment();
    }

    function pause() public onlyOwner {
        _pause();
    }

    function unpause() public onlyOwner {
        _unpause();
    }

    function _baseURI() internal pure override returns (string memory) {
        return "";
    }

    function _beforeTokenTransfer(address from, address to, uint256 tokenId)
        internal
        whenNotPaused
        override(ERC721, ERC721Enumerable)
    {
        super._beforeTokenTransfer(from, to, tokenId);
    }

    function _burn(uint256 tokenId) internal override(ERC721, ERC721URIStorage) {
        super._burn(tokenId);
    }

    function tokenURI(uint256 tokenId)
        public
        view
        override(ERC721, ERC721URIStorage)
        returns (string memory)
    {
        return super.tokenURI(tokenId);
    }

    function supportsInterface(bytes4 interfaceId)
        public
        view
        override(ERC721, ERC721Enumerable)
        returns (bool)
    {
        return super.supportsInterface(interfaceId);
    }

    // 校验调用者是否为NFT所有者
    modifier isNFTOwner(uint256 _tokenId) {
        require(msg.sender == ownerOf(_tokenId));
        _;
    }

    function transferFNFToken(
        address _to, 
        uint256 _tokenId, 
        uint256 _amount) 
        onlyOwner()
        public
      //  private
        //isNFTOwner(_tokenId)
    {
        FNFToken _fnftoken = FNFToken(FNFT[_tokenId].fractionalToken);
        _fnftoken.transfer(_to, _amount);

    }

    function mint(
        address _to,
        string memory tokenURI_, 
        uint256 _totalFractionalTokens
    ) external  {
        _safeMint(_to, _tokenIdCounter.current());
        _setTokenURI(_tokenIdCounter.current(), tokenURI_);

        // 为新铸造的NFT创建对应ERC20合约
        FNFToken _fnftoken = new FNFToken();                                      // 初始化
        _fnftoken.mint(_to, _totalFractionalTokens * 1000000000000000000);   // 铸造ERC20并发送给NFT所有者           
        console.log("To", _to);
        _fnft memory fnft;                                                          // 结构体实例化
        fnft.tokenId = _tokenIdCounter.current();                           
        fnft.fractionalToken = address(_fnftoken);
        FNFT[_tokenIdCounter.current()]  = fnft;                                    // 将ERC20与NFT绑定
        _tokenIdCounter.increment();
    }

        function onERC721Received(
        address,
        address,
        uint256,
        bytes memory
    ) public virtual override returns (bytes4) {
        return this.onERC721Received.selector;
    }
}

问题现象

FractionalNFT 的 mint 函数逻辑为:创建并铸造ERC721代币,生成对应ERC20合约并将代币 mint 到ERC721接收地址,最后绑定ERC20与该ERC721。

调用 mint 后执行 transferFNFToken 时触发报错:

Error: VM Exception while processing transaction: reverted with reason string 'ERC20: transfer amount exceeds balance'

额外排查情况:

  • 查询铸造账户余额,仅显示ERC721余额为1,ERC20余额显示为0
  • 单独铸造ERC20后,账户余额仍显示0;MetaMask中显示极小数值(0.000000000..12)
  • 怀疑小数位处理代码 _fnftoken.mint(_to, _totalFractionalTokens * 1000000000000000000); 存在问题

测试代码

describe("transferFNFToken", () => {
    it("should transfer tokens correctly", async function () {
    await contract1.deployed();
    await contract2.deployed();

    const [owner, addr1, addr2] = await ethers.getSigners();
    console.log("Owner Address",owner.address);
    await contract1.connect(owner).mint(owner.address, 'IPFSHASH/0',4);
    await contract1.connect(owner).transferFNFToken(addr2.address, 0,1);

    const balance = await contract1.connect(owner).balanceOf(owner.address);
    console.log("Balanace Owner Account: ",balance);
  }); 
});

问题分析与修复

核心问题1:转账调用主体错误

transferFNFToken 中直接调用 _fnftoken.transfer(_to, _amount) 时,调用者是 FractionalNFT合约,但ERC20代币是 mint 给了NFT所有者地址,合约本身没有该ERC20的余额,因此触发余额不足错误。

核心问题2:小数位硬编码风险

硬编码 1000000000000000000 虽符合ERC20默认18位小数,但后续若修改小数位会直接导致逻辑失效,且易出现输入错误。

修复方案

  1. 修改 transferFNFToken 函数,使用 transferFrom 并恢复权限校验:
function transferFNFToken(
    address _to, 
    uint256 _tokenId, 
    uint256 _amount) 
    public
    isNFTOwner(_tokenId) // 恢复权限校验,确保只有NFT所有者能操作
{
    FNFToken _fnftoken = FNFToken(FNFT[_tokenId].fractionalToken);
    // 使用transferFrom,需用户先调用ERC20的approve授权合约
    _fnftoken.transferFrom(msg.sender, _to, _amount);
}
  1. 优化小数位计算,使用常量替代硬编码:
    在FNFToken合约中添加常量定义:
uint256 public constant DECIMALS = 18;
uint256 public constant DECIMAL_MULTIPLIER = 10 ** DECIMALS;

修改mint中的计算逻辑:

_fnftoken.mint(_to, _totalFractionalTokens * FNFToken.DECIMAL_MULTIPLIER);
  1. 测试代码补充授权步骤:
await contract1.connect(owner).mint(owner.address, 'IPFSHASH/0',4);
// 获取对应ERC20合约实例
const fnftInfo = await contract1.FNFT(0);
const erc20Contract = await ethers.getContractAt("FNFToken", fnftInfo.fractionalToken);
// 授权合约转移ERC20
await erc20Contract.connect(owner).approve(contract1.address, 1);
// 执行转账
await contract1.connect(owner).transferFNFToken(addr2.address, 0,1);

额外注意点

  • 原函数参数 _tokenURI 命名错误,应改为 _tokenId,避免与tokenURI方法混淆
  • 确保 isNFTOwner 修饰符启用,防止未授权操作
  • 单独铸造ERC20时,需确认调用的是对应NFT绑定的ERC20合约地址,而非FNFToken工厂合约

内容的提问来源于stack exchange,提问作者Maliha

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.10 11:01:08