ERC404 想要解决的问题
ERC-404 是一种图币结合的协议试验
- ERC-20 可以通过 Uniswap 等平台,很容易解决流动性问题。
- 基于 ERC-721 的 NFT 虽然有 Opensea 等平台,但因为唯一性的特征,一直都没有很好的解决流动性问题,使用类似拍卖的形式进行交易。
ERC-404 是尝试解决 NFT 流动性的实验协议。
解决方案的总结
把 NFT 的集合建一个对应的资金池,交易 NFT 就是交易币,同时交易币也就是交易 NFT。
ERC-404 的实际流转
假设 1 个 NFT 对应发行 1 个 ERC20 代币:这时候拥有 1 枚代币就会铸造生成 1 个 NFT
- 当用户地址少于 1 个代币的时候: 只有币,没有图
- 当用户地址大于 1 个代币的时候: 既有币,也有图
- 当用户买入代币,每凑满 1 个整数,就会铸造 1 个 NFT
- 当用户在 Uni 上购买 1 枚代币,用户除了得到代币外,还会收到一个生成的 NFT(ERC-721 标准),可以直接在 NFT 市场上交易
- 当用户地址恰好有一个代币:当卖出一部分的时候,会随机烧毁一个 NFT。(不能指定)
- 当用户在 Uni 上出售 1 枚代币,所拥有的 NFT 也随即被自动销毁
- 当用户将持有的 1 枚代币转账到新地址,原地址的 NFT 被销毁,新地址中会随即生成一个全新的随机 NFT
- 同理当从市场上购买一个 NFT 后,也会收到 1 个代币,出售这个 NFT 后,代币也会随即消失
注意: 只有拥有整数枚代币才会相对应的生成整数个 NFT,拥有 1.5 个代币,只会生成 1 个 NFT,当拥有 2 个代币,出售了 1.5 个代币后,持有的 2 个 NFT 也会随即被销毁。
ERC-404 售卖中的问题
- 费用问题
- 用户在 Uni 上购买代币或转账时,需要承担 NFT 的铸造费用
- 会新生成 NFT,会有稀有度的变化。可能会对已经持有稀有 NFT 的用户造成损失。或许会导致大家无限的转账来刷稀有度,类似卡 BUG 一样的抽奖玩法。
- NFT 问题
- 交易无法指定 NFT,如果用户持有 2 个 NFT 2 个代币,当在 Uni 上出售 1 枚代币时,用户没法选择出售其持有的哪一个 NFT(合约也没有办法判定出售的代币是属于哪 1 个,因为代币是同质化的,而规则是卖掉 1 个代币,需要强制销毁一个 NFT) 。
- 目前只能通过打散的方式,将一个代币对应一个地址的方式来绕开这个问题。
- 如果判断授权及对应转账的是 NFT ID 还是 Token 金额。
- 这里是通过数值大小判断的。通过业务层解决,比如授权,不授权 1e18 以下的数字,(同理授权转账的大小最小为 1e18)
if (amountOrId <= minted && amountOrId > 0) { // NFT ID address owner = _ownerOf[amountOrId]; if (msg.sender != owner && !isApprovedForAll[owner][msg.sender]) { revert Unauthorized(); } getApproved[amountOrId] = spender; emit Approval(owner, spender, amountOrId); } else { // Token 金额 allowance[msg.sender][spender] = amountOrId; emit Approval(msg.sender, spender, amountOrId); }
代码分析
abstract contract ERC404 is Ownable {
// Events ...省略
// Errors ...省略
// ERC20
string public name;
string public symbol;
uint8 public immutable decimals;
uint256 public immutable totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
/// @dev Current mint counter, monotonically increasing to ensure accurate ownership
uint256 public minted;
// ERC721
mapping(uint256 => address) public getApproved;
mapping(address => mapping(address => bool)) public isApprovedForAll;
mapping(uint256 => address) internal _ownerOf; // ID 对应的拥有者
mapping(address => uint256[]) internal _owned; // 地址拥有的ID
mapping(uint256 => uint256) internal _ownedIndex; // 跟踪 _owned 映射的索引
mapping(address => bool) public whitelist; // 白名单 (用来设置交易对、路由器等地址,避免不必要的 mint 和 burn)
// Constructor
constructor(string memory _name, string memory _symbol, uint8 _decimals, uint256 _totalNativeSupply, address _owner) Ownable(_owner) {
name = _name;
symbol = _symbol;
decimals = _decimals;
totalSupply = _totalNativeSupply * (10 ** decimals);
}
// 设置白名单,白名单的地址不会收到NFT和销毁NFT
function setWhitelist(address target, bool state) public onlyOwner {
whitelist[target] = state;
}
// ======================== Functions: View ========================
// 获取指定ID的owner
function ownerOf(uint256 id) public view virtual returns (address owner) {
owner = _ownerOf[id];
if (owner == address(0)) {
revert NotFound();
}
}
// 获取指定ID的URL
function tokenURI(uint256 id) public view virtual returns (string memory);
/// @notice token approvals
/// @dev 如果小于等于当前ID,则认为是NFT的ID
function approve(address spender, uint256 amountOrId) public virtual returns (bool) {
if (amountOrId <= minted && amountOrId > 0) {
address owner = _ownerOf[amountOrId];
if (msg.sender != owner && !isApprovedForAll[owner][msg.sender]) {
revert Unauthorized();
}
getApproved[amountOrId] = spender;
emit Approval(owner, spender, amountOrId);
} else {
allowance[msg.sender][spender] = amountOrId;
emit Approval(msg.sender, spender, amountOrId);
}
return true;
}
/// @notice Function native approvals
function setApprovalForAll(address operator, bool approved) public virtual {
isApprovedForAll[msg.sender][operator] = approved;
emit ApprovalForAll(msg.sender, operator, approved);
}
/// @notice 混合转移
/// @dev 如果小于等于当前ID,则认为是NFT的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);
}
}
/// @notice Function for fractional transfers
function transfer(address to, uint256 amount) public virtual returns (bool) {
return _transfer(msg.sender, to, amount);
}
/// @notice Function for native transfers with contract support
function safeTransferFrom(address from, address to, uint256 id) public virtual {
transferFrom(from, to, id);
if (to.code.length != 0 && ERC721Receiver(to).onERC721Received(msg.sender, from, id, '') != ERC721Receiver.onERC721Received.selector) {
revert UnsafeRecipient();
}
}
/// @notice Function for native transfers with contract support and callback data
function safeTransferFrom(address from, address to, uint256 id, bytes calldata data) public virtual {
transferFrom(from, to, id);
if (to.code.length != 0 && ERC721Receiver(to).onERC721Received(msg.sender, from, id, data) != ERC721Receiver.onERC721Received.selector) {
revert UnsafeRecipient();
}
}
/// @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;
}
// Internal utility logic
function _getUnit() internal view returns (uint256) {
return 10 ** decimals;
}
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);
}
function _setNameSymbol(string memory _name, string memory _symbol) internal {
name = _name;
symbol = _symbol;
}
}
参考资料
- https://github.com/Pandora-Labs-Org/erc404
- https://github.com/Pandora-Labs-Org/erc404-legacy
- https://www.okx.com/cn/learn/what-is-erc-404
- https://zhuanlan.zhihu.com/p/682126308
- https://www.theblockbeats.info/news/50659
- https://www.coingecko.com/en/coins/pandora
- https://etherscan.io/token/0x9E9FbDE7C7a83c43913BddC8779158F1368F0413#code