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位小数,但后续若修改小数位会直接导致逻辑失效,且易出现输入错误。
修复方案
- 修改
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); }
- 优化小数位计算,使用常量替代硬编码:
在FNFToken合约中添加常量定义:
uint256 public constant DECIMALS = 18; uint256 public constant DECIMAL_MULTIPLIER = 10 ** DECIMALS;
修改mint中的计算逻辑:
_fnftoken.mint(_to, _totalFractionalTokens * FNFToken.DECIMAL_MULTIPLIER);
- 测试代码补充授权步骤:
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
相关产品推荐
相关产品推荐

