<100 subscribers

Uniswap V3交易预计算技巧
Uniswap上做swap交易时,比如用usdt购买btc,会根据界面上输入的usdt数量,实时计算出可以swap到多少个btc,v2版本因为是应用了xy=k的公式,可以方便的计算出来。代码里通过getAmountOut和getAmountIn得到,这两个都是view函数,不需要消耗gas。而到了...
Meebits mint随机算法
先上代码function randomIndex() internal returns (uint) { uint totalSize = TOKEN_LIMIT - numTokens; uint index = uint(keccak256(abi.encodePacked(nonce, ms...

nftx闪电贷领取apecoin空投
已经是两个多月前的事件了,这几天才对这个tx进行了分析和fork重现,记录下来加深一下理解。apecoin的空投并没有限制调用方不能是合约地址,也不是用链下签名再到合约里验签的方式来领取空投,而是直接在合约里校验调用方的地址里有没有bayc/mayc,再加上apecoin当时的价格在8u左右,总体...

Uniswap V3交易预计算技巧
Uniswap上做swap交易时,比如用usdt购买btc,会根据界面上输入的usdt数量,实时计算出可以swap到多少个btc,v2版本因为是应用了xy=k的公式,可以方便的计算出来。代码里通过getAmountOut和getAmountIn得到,这两个都是view函数,不需要消耗gas。而到了...
Meebits mint随机算法
先上代码function randomIndex() internal returns (uint) { uint totalSize = TOKEN_LIMIT - numTokens; uint index = uint(keccak256(abi.encodePacked(nonce, ms...

nftx闪电贷领取apecoin空投
已经是两个多月前的事件了,这几天才对这个tx进行了分析和fork重现,记录下来加深一下理解。apecoin的空投并没有限制调用方不能是合约地址,也不是用链下签名再到合约里验签的方式来领取空投,而是直接在合约里校验调用方的地址里有没有bayc/mayc,再加上apecoin当时的价格在8u左右,总体...
Share Dialog
Share Dialog
NFT白单和公售mint时,有些项目允许一个tx交易mint多个NFT,以太坊标准的ERC721在mint多个时gas成本成倍增加,对用户来说不太友好。Azuki对此进行了优化,发布了ERC721A合约,这个合约也在跟随着最新市场变化而不断优化,现对当前最新版本v3.0.0进行解读。
合约源码见
https://github.com/chiru-labs/ERC721A
核心优化是不再每个tokenId都存储对应的所有者地址,对于mint多个的地址只在第一个tokenId上存储所有者信息,但这样依赖于tokenId的连续性。也就是说mint多个时,tokenId是连续的,对于某些项目的tokenId不是连续的来说就不太适用。比如meebits的tokenId是随机去除获取的。
下面对关键代码进行了中文的补充注释
// Compiler will pack this into a single 256bit word. struct TokenOwnership { // The address of the owner. address addr;//所有者地址 160位 // Keeps track of the start time of ownership with minimal overhead for tokenomics. uint64 startTimestamp;//连续片段的第一个token归属给用户的时间戳 // Whether the token has been burned. bool burned;//是否燃烧 8位 }//160+64+8=212,对齐到264位 // Compiler will pack this into a single 256bit word. struct AddressData { // Realistically, 2**64-1 is more than enough. uint64 balance;//token余额 // Keeps track of mint count with minimal overhead for tokenomics. uint64 numberMinted;//mint数量 // Keeps track of burn count with minimal overhead for tokenomics. uint64 numberBurned;//燃烧数量 // For miscellaneous variable(s) pertaining to the address // (e.g. number of whitelist mint slots used). // If there are multiple variables, please pack them into a uint64. uint64 aux;//附加参数 } // The tokenId of the next token to be minted. uint256 internal _currentIndex; // The number of tokens burned. uint256 internal _burnCounter; // Token name string private _name; // Token symbol string private _symbol; // Mapping from token ID to ownership details // An empty struct value does not necessarily mean the token is unowned. See ownershipOf implementation for details. mapping(uint256 => TokenOwnership) internal _ownerships;//tokenId与所有者地址的映射关系 // Mapping owner address to address data mapping(address => AddressData) private _addressData;//地址与地址token的映射关系
主要用了TokenOwnership来存储所有者信息,AddressData存储所有者对应的token信息
/** * Gas spent here starts off proportional to the maximum mint batch size. * It gradually moves to O(1) as tokens get transferred around in the collection over time. */ function ownershipOf(uint256 tokenId) internal view returns (TokenOwnership memory) { uint256 curr = tokenId; unchecked { if (_startTokenId() <= curr && curr < _currentIndex) {//tokenId在当前mint的数量范围内 TokenOwnership memory ownership = _ownerships[curr]; if (!ownership.burned) {//未燃烧 if (ownership.addr != address(0)) { return ownership; } // Invariant: // There will always be an ownership that has an address and is not burned // before an ownership that does not have an address and is not burned. // Hence, curr will not underflow. while (true) {//tokenId对应的所有者地址是空,表示此tokenId在存储时不是连续块的第一个token,因此要向前遍历直接找到非0地址 curr--; ownership = _ownerships[curr]; if (ownership.addr != address(0)) { return ownership; } } } } } revert OwnerQueryForNonexistentToken(); }
因为不再每个tokenId都存储所有者信息,所以在根据tokenId查找所有者时,有可能要遍历查找,但读交易是不需要gas的
/** * @dev Mints `quantity` tokens and transfers them to `to`. * * Requirements: * * - `to` cannot be the zero address. * - `quantity` must be greater than 0. * * Emits a {Transfer} event. */ function _mint( address to, uint256 quantity, bytes memory _data, bool safe ) internal { uint256 startTokenId = _currentIndex;//当前tokenId索引 if (to == address(0)) revert MintToZeroAddress();//不能mint到0地址 if (quantity == 0) revert MintZeroQuantity();//数量不能=0 _beforeTokenTransfers(address(0), to, startTokenId, quantity); // Overflows are incredibly unrealistic. // balance or numberMinted overflow if current value of either + quantity > 1.8e19 (2**64) - 1 // updatedIndex overflows if _currentIndex + quantity > 1.2e77 (2**256) - 1 unchecked { _addressData[to].balance += uint64(quantity);//目标地址token余额+1 _addressData[to].numberMinted += uint64(quantity);//目标地址mint数量+1 _ownerships[startTokenId].addr = to;//设置tokenId所有者 _ownerships[startTokenId].startTimestamp = uint64(block.timestamp);//设置连续片段的第一个token归属给用户的时间戳 uint256 updatedIndex = startTokenId; uint256 end = updatedIndex + quantity;//最后一个tokenId if (safe && to.isContract()) {//目标地址是合约 do { emit Transfer(address(0), to, updatedIndex);//事件日志 if (!_checkContractOnERC721Received(address(0), to, updatedIndex++, _data)) {//校验是否符合IERC721Receiversf标准 revert TransferToNonERC721ReceiverImplementer(); } } while (updatedIndex != end); // Reentrancy protection if (_currentIndex != startTokenId) revert(); } else { do { emit Transfer(address(0), to, updatedIndex++);//事件日志 } while (updatedIndex != end); } _currentIndex = updatedIndex;//更新当前tokenId索引,供下次mint使用 } _afterTokenTransfers(address(0), to, startTokenId, quantity); } /** * @dev Transfers `tokenId` from `from` to `to`. * * Requirements: * * - `to` cannot be the zero address. * - `tokenId` token must be owned by `from`. * * Emits a {Transfer} event. */ function _transfer( address from, address to, uint256 tokenId ) private { TokenOwnership memory prevOwnership = ownershipOf(tokenId);//获取tokenId所有者地址 bool isApprovedOrOwner = (_msgSender() == prevOwnership.addr || isApprovedForAll(prevOwnership.addr, _msgSender()) || getApproved(tokenId) == _msgSender());//tokenId是函数调用方自己的 或者 已授权给函数调用方 if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved(); if (prevOwnership.addr != from) revert TransferFromIncorrectOwner();//tokenId所有者与from参数要一样 if (to == address(0)) revert TransferToZeroAddress();//不能转到0地址 _beforeTokenTransfers(from, to, tokenId, 1); // Clear approvals from the previous owner _approve(address(0), tokenId, prevOwnership.addr);//转出地址的tokenId授权记录要清空 // Underflow of the sender's balance is impossible because we check for // ownership above and the recipient's balance can't realistically overflow. // Counter overflow is incredibly unrealistic as tokenId would have to be 2**256. unchecked { _addressData[from].balance -= 1;//转出地址余额-1 _addressData[to].balance += 1;//转入地址余额+1 _ownerships[tokenId].addr = to;//更新tokenId所有者地址为目标地址 _ownerships[tokenId].startTimestamp = uint64(block.timestamp);//更新连续片段的第一个token归属给用户的时间戳 // If the ownership slot of tokenId+1 is not explicitly set, that means the transfer initiator owns it. // Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls. uint256 nextTokenId = tokenId + 1; if (_ownerships[nextTokenId].addr == address(0)) {//下一个tokenId的所有者信息是空时,表示下一个tokenId也是当前tokenId拥有的,因此要更新所有者信息 // This will suffice for checking _exists(nextTokenId), // as a burned slot cannot contain the zero address. if (nextTokenId < _currentIndex) { _ownerships[nextTokenId].addr = prevOwnership.addr;//更新下一个tokenId所有者地址为转出地址 _ownerships[nextTokenId].startTimestamp = prevOwnership.startTimestamp;//更新连续片段的第一个token归属给用户的时间戳 } } } emit Transfer(from, to, tokenId); _afterTokenTransfers(from, to, tokenId, 1); } /** * @dev Destroys `tokenId`. * The approval is cleared when the token is burned. * * Requirements: * * - `tokenId` must exist. * * Emits a {Transfer} event. */ function _burn(uint256 tokenId) internal virtual { TokenOwnership memory prevOwnership = ownershipOf(tokenId);//获取tokenId所有者地址 _beforeTokenTransfers(prevOwnership.addr, address(0), tokenId, 1); // Clear approvals from the previous owner _approve(address(0), tokenId, prevOwnership.addr);//地址的tokenId授权记录要清空 // Underflow of the sender's balance is impossible because we check for // ownership above and the recipient's balance can't realistically overflow. // Counter overflow is incredibly unrealistic as tokenId would have to be 2**256. unchecked { _addressData[prevOwnership.addr].balance -= 1;//地址余额-1 _addressData[prevOwnership.addr].numberBurned += 1;//地址煅烧数量+1 // Keep track of who burned the token, and the timestamp of burning. _ownerships[tokenId].addr = prevOwnership.addr;//更新tokenid的所有者地址,因为可能tokenId是属于连续块中的非第一个,之前mint时没有做所有者记录 _ownerships[tokenId].startTimestamp = uint64(block.timestamp);//更新连续片段的第一个token归属给用户的时间戳 _ownerships[tokenId].burned = true;//更新tokenId状态为已燃烧 // If the ownership slot of tokenId+1 is not explicitly set, that means the burn initiator owns it. // Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls. uint256 nextTokenId = tokenId + 1; if (_ownerships[nextTokenId].addr == address(0)) {//下一个tokenId的所有者信息是空时,表示下一个tokenId也是当前tokenId拥有的,因此要更新所有者信息 // This will suffice for checking _exists(nextTokenId), // as a burned slot cannot contain the zero address. if (nextTokenId < _currentIndex) { _ownerships[nextTokenId].addr = prevOwnership.addr;//更新下一个tokenId所有者地址 _ownerships[nextTokenId].startTimestamp = prevOwnership.startTimestamp;//更新连续片段的第一个token归属给用户的时间戳 } } } emit Transfer(prevOwnership.addr, address(0), tokenId); _afterTokenTransfers(prevOwnership.addr, address(0), tokenId, 1); // Overflow not possible, as _burnCounter cannot be exceed _currentIndex times. unchecked { _burnCounter++; } }
多个mint时,只存储了第一个tokenId对应的_addressData和_ownerships,这样可以省去重复的gas消耗;transfer时如果不是第一个tokenId的transfer,需要更新tokenId+1的_ownerships;burn时与transfer类似。
项目官方有对mint的gas消耗与ERC721进行了对比,可以说在gas消耗上节省了许多,往后应该会有越来越多的NFT项目采用此合约标准。

NFT白单和公售mint时,有些项目允许一个tx交易mint多个NFT,以太坊标准的ERC721在mint多个时gas成本成倍增加,对用户来说不太友好。Azuki对此进行了优化,发布了ERC721A合约,这个合约也在跟随着最新市场变化而不断优化,现对当前最新版本v3.0.0进行解读。
合约源码见
https://github.com/chiru-labs/ERC721A
核心优化是不再每个tokenId都存储对应的所有者地址,对于mint多个的地址只在第一个tokenId上存储所有者信息,但这样依赖于tokenId的连续性。也就是说mint多个时,tokenId是连续的,对于某些项目的tokenId不是连续的来说就不太适用。比如meebits的tokenId是随机去除获取的。
下面对关键代码进行了中文的补充注释
// Compiler will pack this into a single 256bit word. struct TokenOwnership { // The address of the owner. address addr;//所有者地址 160位 // Keeps track of the start time of ownership with minimal overhead for tokenomics. uint64 startTimestamp;//连续片段的第一个token归属给用户的时间戳 // Whether the token has been burned. bool burned;//是否燃烧 8位 }//160+64+8=212,对齐到264位 // Compiler will pack this into a single 256bit word. struct AddressData { // Realistically, 2**64-1 is more than enough. uint64 balance;//token余额 // Keeps track of mint count with minimal overhead for tokenomics. uint64 numberMinted;//mint数量 // Keeps track of burn count with minimal overhead for tokenomics. uint64 numberBurned;//燃烧数量 // For miscellaneous variable(s) pertaining to the address // (e.g. number of whitelist mint slots used). // If there are multiple variables, please pack them into a uint64. uint64 aux;//附加参数 } // The tokenId of the next token to be minted. uint256 internal _currentIndex; // The number of tokens burned. uint256 internal _burnCounter; // Token name string private _name; // Token symbol string private _symbol; // Mapping from token ID to ownership details // An empty struct value does not necessarily mean the token is unowned. See ownershipOf implementation for details. mapping(uint256 => TokenOwnership) internal _ownerships;//tokenId与所有者地址的映射关系 // Mapping owner address to address data mapping(address => AddressData) private _addressData;//地址与地址token的映射关系
主要用了TokenOwnership来存储所有者信息,AddressData存储所有者对应的token信息
/** * Gas spent here starts off proportional to the maximum mint batch size. * It gradually moves to O(1) as tokens get transferred around in the collection over time. */ function ownershipOf(uint256 tokenId) internal view returns (TokenOwnership memory) { uint256 curr = tokenId; unchecked { if (_startTokenId() <= curr && curr < _currentIndex) {//tokenId在当前mint的数量范围内 TokenOwnership memory ownership = _ownerships[curr]; if (!ownership.burned) {//未燃烧 if (ownership.addr != address(0)) { return ownership; } // Invariant: // There will always be an ownership that has an address and is not burned // before an ownership that does not have an address and is not burned. // Hence, curr will not underflow. while (true) {//tokenId对应的所有者地址是空,表示此tokenId在存储时不是连续块的第一个token,因此要向前遍历直接找到非0地址 curr--; ownership = _ownerships[curr]; if (ownership.addr != address(0)) { return ownership; } } } } } revert OwnerQueryForNonexistentToken(); }
因为不再每个tokenId都存储所有者信息,所以在根据tokenId查找所有者时,有可能要遍历查找,但读交易是不需要gas的
/** * @dev Mints `quantity` tokens and transfers them to `to`. * * Requirements: * * - `to` cannot be the zero address. * - `quantity` must be greater than 0. * * Emits a {Transfer} event. */ function _mint( address to, uint256 quantity, bytes memory _data, bool safe ) internal { uint256 startTokenId = _currentIndex;//当前tokenId索引 if (to == address(0)) revert MintToZeroAddress();//不能mint到0地址 if (quantity == 0) revert MintZeroQuantity();//数量不能=0 _beforeTokenTransfers(address(0), to, startTokenId, quantity); // Overflows are incredibly unrealistic. // balance or numberMinted overflow if current value of either + quantity > 1.8e19 (2**64) - 1 // updatedIndex overflows if _currentIndex + quantity > 1.2e77 (2**256) - 1 unchecked { _addressData[to].balance += uint64(quantity);//目标地址token余额+1 _addressData[to].numberMinted += uint64(quantity);//目标地址mint数量+1 _ownerships[startTokenId].addr = to;//设置tokenId所有者 _ownerships[startTokenId].startTimestamp = uint64(block.timestamp);//设置连续片段的第一个token归属给用户的时间戳 uint256 updatedIndex = startTokenId; uint256 end = updatedIndex + quantity;//最后一个tokenId if (safe && to.isContract()) {//目标地址是合约 do { emit Transfer(address(0), to, updatedIndex);//事件日志 if (!_checkContractOnERC721Received(address(0), to, updatedIndex++, _data)) {//校验是否符合IERC721Receiversf标准 revert TransferToNonERC721ReceiverImplementer(); } } while (updatedIndex != end); // Reentrancy protection if (_currentIndex != startTokenId) revert(); } else { do { emit Transfer(address(0), to, updatedIndex++);//事件日志 } while (updatedIndex != end); } _currentIndex = updatedIndex;//更新当前tokenId索引,供下次mint使用 } _afterTokenTransfers(address(0), to, startTokenId, quantity); } /** * @dev Transfers `tokenId` from `from` to `to`. * * Requirements: * * - `to` cannot be the zero address. * - `tokenId` token must be owned by `from`. * * Emits a {Transfer} event. */ function _transfer( address from, address to, uint256 tokenId ) private { TokenOwnership memory prevOwnership = ownershipOf(tokenId);//获取tokenId所有者地址 bool isApprovedOrOwner = (_msgSender() == prevOwnership.addr || isApprovedForAll(prevOwnership.addr, _msgSender()) || getApproved(tokenId) == _msgSender());//tokenId是函数调用方自己的 或者 已授权给函数调用方 if (!isApprovedOrOwner) revert TransferCallerNotOwnerNorApproved(); if (prevOwnership.addr != from) revert TransferFromIncorrectOwner();//tokenId所有者与from参数要一样 if (to == address(0)) revert TransferToZeroAddress();//不能转到0地址 _beforeTokenTransfers(from, to, tokenId, 1); // Clear approvals from the previous owner _approve(address(0), tokenId, prevOwnership.addr);//转出地址的tokenId授权记录要清空 // Underflow of the sender's balance is impossible because we check for // ownership above and the recipient's balance can't realistically overflow. // Counter overflow is incredibly unrealistic as tokenId would have to be 2**256. unchecked { _addressData[from].balance -= 1;//转出地址余额-1 _addressData[to].balance += 1;//转入地址余额+1 _ownerships[tokenId].addr = to;//更新tokenId所有者地址为目标地址 _ownerships[tokenId].startTimestamp = uint64(block.timestamp);//更新连续片段的第一个token归属给用户的时间戳 // If the ownership slot of tokenId+1 is not explicitly set, that means the transfer initiator owns it. // Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls. uint256 nextTokenId = tokenId + 1; if (_ownerships[nextTokenId].addr == address(0)) {//下一个tokenId的所有者信息是空时,表示下一个tokenId也是当前tokenId拥有的,因此要更新所有者信息 // This will suffice for checking _exists(nextTokenId), // as a burned slot cannot contain the zero address. if (nextTokenId < _currentIndex) { _ownerships[nextTokenId].addr = prevOwnership.addr;//更新下一个tokenId所有者地址为转出地址 _ownerships[nextTokenId].startTimestamp = prevOwnership.startTimestamp;//更新连续片段的第一个token归属给用户的时间戳 } } } emit Transfer(from, to, tokenId); _afterTokenTransfers(from, to, tokenId, 1); } /** * @dev Destroys `tokenId`. * The approval is cleared when the token is burned. * * Requirements: * * - `tokenId` must exist. * * Emits a {Transfer} event. */ function _burn(uint256 tokenId) internal virtual { TokenOwnership memory prevOwnership = ownershipOf(tokenId);//获取tokenId所有者地址 _beforeTokenTransfers(prevOwnership.addr, address(0), tokenId, 1); // Clear approvals from the previous owner _approve(address(0), tokenId, prevOwnership.addr);//地址的tokenId授权记录要清空 // Underflow of the sender's balance is impossible because we check for // ownership above and the recipient's balance can't realistically overflow. // Counter overflow is incredibly unrealistic as tokenId would have to be 2**256. unchecked { _addressData[prevOwnership.addr].balance -= 1;//地址余额-1 _addressData[prevOwnership.addr].numberBurned += 1;//地址煅烧数量+1 // Keep track of who burned the token, and the timestamp of burning. _ownerships[tokenId].addr = prevOwnership.addr;//更新tokenid的所有者地址,因为可能tokenId是属于连续块中的非第一个,之前mint时没有做所有者记录 _ownerships[tokenId].startTimestamp = uint64(block.timestamp);//更新连续片段的第一个token归属给用户的时间戳 _ownerships[tokenId].burned = true;//更新tokenId状态为已燃烧 // If the ownership slot of tokenId+1 is not explicitly set, that means the burn initiator owns it. // Set the slot of tokenId+1 explicitly in storage to maintain correctness for ownerOf(tokenId+1) calls. uint256 nextTokenId = tokenId + 1; if (_ownerships[nextTokenId].addr == address(0)) {//下一个tokenId的所有者信息是空时,表示下一个tokenId也是当前tokenId拥有的,因此要更新所有者信息 // This will suffice for checking _exists(nextTokenId), // as a burned slot cannot contain the zero address. if (nextTokenId < _currentIndex) { _ownerships[nextTokenId].addr = prevOwnership.addr;//更新下一个tokenId所有者地址 _ownerships[nextTokenId].startTimestamp = prevOwnership.startTimestamp;//更新连续片段的第一个token归属给用户的时间戳 } } } emit Transfer(prevOwnership.addr, address(0), tokenId); _afterTokenTransfers(prevOwnership.addr, address(0), tokenId, 1); // Overflow not possible, as _burnCounter cannot be exceed _currentIndex times. unchecked { _burnCounter++; } }
多个mint时,只存储了第一个tokenId对应的_addressData和_ownerships,这样可以省去重复的gas消耗;transfer时如果不是第一个tokenId的transfer,需要更新tokenId+1的_ownerships;burn时与transfer类似。
项目官方有对mint的gas消耗与ERC721进行了对比,可以说在gas消耗上节省了许多,往后应该会有越来越多的NFT项目采用此合约标准。

No comments yet