Decode ERC404

Decode ERC404

Tags
Web3
NFT
ERC20
Published
February 7, 2024
Author
Senn
最近的Pandora项目引起了ERC404的一波热潮. Pandora既是NFT又是ERC20, 可以同时在NFT交易所和DEX上交易. 截止本文撰写前, 1个Pandora的价格是3.95 ETH.
notion image
notion image
 
为什么ERC404会具有所谓的“图币二象性”? 简单来说是ERC404实现了ERC20和ERC721的标准接口, 并且对于转移的数量参数, 根据值的大小来区别转移意图. 如果转移数量小于所有mint的NFT数量, 则被合约认为转移的是NFT, 并且1个NFT等于1个token. 反之则认为转移的是ERC20, 转移参数大小的token数量.
 
本文无关投资, 主要是分析其实现和可能的创新价值.
我们来看一下它的源代码:
ERC404
0xacmeUpdated May 28, 2024
为了支持ERC20标准, ERC404中实现了ERC20的transfer(address,uint)函数, 根据amount参数来更新balanceOf这个mapping,也就是记录用户ERC20余额的storage.
 
在ERC404中, NFT的数量为token数量的向下取整数量.
举一个例子, 用户刚开始有1.5个Pandora, 那么其NFT的余额为1. 如果用户转移了0.3个Pandora, 还剩下1.2个Pandora, 那么其NFT余额还是1. 但是用户再转了0.3个Pandora, 余额变成0.9后, 其NFT数量就为0.
_transfer函数实际上就是将这个规则进行了实现. 所以和ERC20的实现不同的是, 在_transfer的实现中, 加入了对NFT余额的更新. 合约会根据from和to地址在转移ERC20之前和之后的余额, 计算并更新from和to的NFT余额.
/// @notice Function for fractional transfers function transfer( address to, uint256 amount ) public virtual returns (bool) { return _transfer(msg.sender, to, amount); } /// @notice Internal function for fractional transfers function _transfer( address from, address to, uint256 amount ) internal returns (bool) { uint256 unit = _getUnit(); uint256 balanceBeforeSender = balanceOf[from]; uint256 balanceBeforeReceiver = balanceOf[to]; balanceOf[from] -= amount; unchecked { balanceOf[to] += amount; } // Skip burn for certain addresses to save gas if (!whitelist[from]) { uint256 tokens_to_burn = (balanceBeforeSender / unit) - (balanceOf[from] / unit); for (uint256 i = 0; i < tokens_to_burn; i++) { _burn(from); } } // Skip minting for certain addresses to save gas if (!whitelist[to]) { uint256 tokens_to_mint = (balanceOf[to] / unit) - (balanceBeforeReceiver / unit); for (uint256 i = 0; i < tokens_to_mint; i++) { _mint(to); } } emit ERC20Transfer(from, to, amount); return true; } /// @dev Decimals for fractional representation uint8 public immutable decimals; // Internal utility logic function _getUnit() internal view returns (uint256) { return 10 ** decimals; }
 
_mint_burn 是按照NFT的方式实现的, 会对以下数据进行更新:
  • _ownerOf : NFT的所有者
  • _owned : 账户的所有NFT的Id
  • _ownedIndex: NFT在账户拥有的NFT的数组里的index
/// @dev Owner of id in native representation mapping(uint256 => address) internal _ownerOf; /// @dev Array of owned ids in native representation mapping(address => uint256[]) internal _owned; /// @dev Tracks indices for the _owned mapping mapping(uint256 => uint256) internal _ownedIndex; function _mint(address to) internal virtual { if (to == address(0)) { revert InvalidRecipient(); } unchecked { minted++; } uint256 id = minted; if (_ownerOf[id] != address(0)) { revert AlreadyExists(); } _ownerOf[id] = to; _owned[to].push(id); _ownedIndex[id] = _owned[to].length - 1; emit Transfer(address(0), to, id); } function _burn(address from) internal virtual { if (from == address(0)) { revert InvalidSender(); } uint256 id = _owned[from][_owned[from].length - 1]; _owned[from].pop(); delete _ownedIndex[id]; delete _ownerOf[id]; delete getApproved[id]; emit Transfer(from, address(0), id); }
 
有意思的是transferFrom(address,address,uint)的实现. 因为ERC20和ERC721都有这个接口, 如何来同时支持ERC20和ERC721标准?
ERC404使用数值大小来对amount进行区分, 也就是官网介绍中所说的:
In it's current implementation, ERC404 effectively isolates ERC20 / ERC721 standard logic or introduces pathing where possible. Pathing could best be described as a lossy encoding scheme in which token amount data and ids occupy shared space under the assumption that negligible token transfers occupying id space do not or do not need to occur.
 
在代码中, 我们可以看到, 如果amountOrId < minted, 这个时候amountOrId被视为转移的NFT数量, 反之被视作转移ERC20的数量. 其中minted参数表示的是合约中NFT的供应量.
通过这个trick来实现对NFT和ERC20的同时支持. 这里的一个潜在问题在于, 用户发起的交易可能不符合用户的意图, 比如当前NFT的供应量为10000, 用户如果想要转移 1个ERC20 token, 这是做不到的, 因为合约会将其判定为对 tokenId 为1的NFT的转移. 当然, 这个trick的合理之处是, 用户几乎没有转移如此小的ERC20数量的需求. (Pandora的ERC20精度为18).
/// @notice Function for mixed transfers /// @dev This function assumes id / native if amount less than or equal to current max id function transferFrom( address from, address to, uint256 amountOrId ) public virtual { if (amountOrId <= minted) { if (from != _ownerOf[amountOrId]) { revert InvalidSender(); } if (to == address(0)) { revert InvalidRecipient(); } if ( msg.sender != from && !isApprovedForAll[from][msg.sender] && msg.sender != getApproved[amountOrId] ) { revert Unauthorized(); } balanceOf[from] -= _getUnit(); unchecked { balanceOf[to] += _getUnit(); } _ownerOf[amountOrId] = to; delete getApproved[amountOrId]; // update _owned for sender uint256 updatedId = _owned[from][_owned[from].length - 1]; _owned[from][_ownedIndex[amountOrId]] = updatedId; // pop _owned[from].pop(); // update index for the moved id _ownedIndex[updatedId] = _ownedIndex[amountOrId]; // push token to to owned _owned[to].push(amountOrId); // update index for to owned _ownedIndex[amountOrId] = _owned[to].length - 1; emit Transfer(from, to, amountOrId); emit ERC20Transfer(from, to, _getUnit()); } else { uint256 allowed = allowance[from][msg.sender]; if (allowed != type(uint256).max) allowance[from][msg.sender] = allowed - amountOrId; _transfer(from, to, amountOrId); } }
 
ERC404实现了ERC721的相关接口, 来支持NFT的交易
  • approve
  • setApprovalForAll
  • safeTransferFrom
  • tokenURI
 
总结, 通过实现ERC20和ERC721的标准接口, 并且在transferFrom里按照转移的数量大小来区分NFT和ERC20, ERC404同时支持了ERC20和ERC721, 让其能够无缝地在DEX和NFT交易所上交易. 并且资产同时具有Fungible和Non-fungible的特性, 可能会在一些需要资产拆分但又同时需要资产特性的领域发挥作用.