diff --git a/contracts/contracts-registry/pools/proxy/ProxyBeacon.sol b/contracts/contracts-registry/pools/proxy/ProxyBeacon.sol index 48b894ac..f907d714 100644 --- a/contracts/contracts-registry/pools/proxy/ProxyBeacon.sol +++ b/contracts/contracts-registry/pools/proxy/ProxyBeacon.sol @@ -35,7 +35,7 @@ contract ProxyBeacon is IBeacon { emit Upgraded(newImplementation_); } - function implementation() external view override returns (address) { + function implementation() public view override returns (address) { return _implementation; } } diff --git a/contracts/diamond/tokens/ERC721/DiamondERC721.sol b/contracts/diamond/tokens/ERC721/DiamondERC721.sol new file mode 100644 index 00000000..42a60b9a --- /dev/null +++ b/contracts/diamond/tokens/ERC721/DiamondERC721.sol @@ -0,0 +1,361 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import {Address} from "@openzeppelin/contracts/utils/Address.sol"; + +import {DiamondERC721Storage} from "./DiamondERC721Storage.sol"; + +/** + * @notice This is modified version of OpenZeppelin's ERC721 contract to be used as a Storage contract + * by the Diamond Standard. + */ +contract DiamondERC721 is DiamondERC721Storage { + using Address for address; + + /** + * @notice Sets the values for {name} and {symbol}. + */ + function __DiamondERC721_init( + string memory name_, + string memory symbol_ + ) internal onlyInitializing(DIAMOND_ERC721_STORAGE_SLOT) { + DERC721Storage storage _erc721Storage = _getErc721Storage(); + + _erc721Storage.name = name_; + _erc721Storage.symbol = symbol_; + } + + /** + * @inheritdoc IERC721 + */ + function approve(address to_, uint256 tokenId_) public virtual override { + address owner_ = ownerOf(tokenId_); + require(to_ != owner_, "ERC721: approval to current owner"); + + require( + msg.sender == owner_ || isApprovedForAll(owner_, msg.sender), + "ERC721: approve caller is not token owner or approved for all" + ); + + _approve(to_, tokenId_); + } + + /** + * @inheritdoc IERC721 + */ + function setApprovalForAll(address operator_, bool approved_) public virtual override { + _setApprovalForAll(msg.sender, operator_, approved_); + } + + /** + * @inheritdoc IERC721 + */ + function transferFrom(address from_, address to_, uint256 tokenId_) public virtual override { + require( + _isApprovedOrOwner(msg.sender, tokenId_), + "ERC721: caller is not token owner or approved" + ); + + _transfer(from_, to_, tokenId_); + } + + /** + * @inheritdoc IERC721 + */ + function safeTransferFrom( + address from_, + address to_, + uint256 tokenId_ + ) public virtual override { + safeTransferFrom(from_, to_, tokenId_, ""); + } + + /** + * @inheritdoc IERC721 + */ + function safeTransferFrom( + address from_, + address to_, + uint256 tokenId_, + bytes memory data_ + ) public virtual override { + require( + _isApprovedOrOwner(msg.sender, tokenId_), + "ERC721: caller is not token owner or approved" + ); + + _safeTransfer(from_, to_, tokenId_, data_); + } + + /** + * @notice Safely transfers `tokenId` token from `from` to `to`, checking first that contract recipients + * are aware of the ERC721 protocol to prevent tokens from being forever locked. + */ + function _safeTransfer( + address from_, + address to_, + uint256 tokenId_, + bytes memory data_ + ) internal virtual { + _transfer(from_, to_, tokenId_); + + require( + _checkOnERC721Received(from_, to_, tokenId_, data_), + "ERC721: transfer to non ERC721Receiver implementer" + ); + } + + /** + * @notice Safely mints `tokenId` and transfers it to `to`. + */ + function _safeMint(address to_, uint256 tokenId_) internal virtual { + _safeMint(to_, tokenId_, ""); + } + + /** + * @notice Same as _safeMint, with an additional `data` parameter. + */ + function _safeMint(address to_, uint256 tokenId_, bytes memory data_) internal virtual { + _mint(to_, tokenId_); + + require( + _checkOnERC721Received(address(0), to_, tokenId_, data_), + "ERC721: transfer to non ERC721Receiver implementer" + ); + } + + /** + * @notice Mints `tokenId` and transfers it to `to`. + */ + function _mint(address to_, uint256 tokenId_) internal virtual { + require(to_ != address(0), "ERC721: mint to the zero address"); + require(!_exists(tokenId_), "ERC721: token already minted"); + + _beforeTokenTransfer(address(0), to_, tokenId_, 1); + + // Check that tokenId was not minted by `_beforeTokenTransfer` hook + require(!_exists(tokenId_), "ERC721: token already minted"); + + DERC721Storage storage _erc721Storage = _getErc721Storage(); + + unchecked { + _erc721Storage.balances[to_] += 1; + } + + _erc721Storage.owners[tokenId_] = to_; + + emit Transfer(address(0), to_, tokenId_); + + _afterTokenTransfer(address(0), to_, tokenId_, 1); + } + + /** + * @notice Destroys `tokenId`. + */ + function _burn(uint256 tokenId_) internal virtual { + address owner_ = ownerOf(tokenId_); + + _beforeTokenTransfer(owner_, address(0), tokenId_, 1); + + // Update ownership in case tokenId was transferred by `_beforeTokenTransfer` hook + owner_ = ownerOf(tokenId_); + + DERC721Storage storage _erc721Storage = _getErc721Storage(); + + // Clear approvals + delete _erc721Storage.tokenApprovals[tokenId_]; + + unchecked { + _erc721Storage.balances[owner_] -= 1; + } + + delete _erc721Storage.owners[tokenId_]; + + emit Transfer(owner_, address(0), tokenId_); + + _afterTokenTransfer(owner_, address(0), tokenId_, 1); + } + + /** + * @notice Transfers `tokenId` from `from` to `to`. + */ + function _transfer(address from_, address to_, uint256 tokenId_) internal virtual { + require(ownerOf(tokenId_) == from_, "ERC721: transfer from incorrect owner"); + require(to_ != address(0), "ERC721: transfer to the zero address"); + + _beforeTokenTransfer(from_, to_, tokenId_, 1); + + // Check that tokenId was not transferred by `_beforeTokenTransfer` hook + require(ownerOf(tokenId_) == from_, "ERC721: transfer from incorrect owner"); + + DERC721Storage storage _erc721Storage = _getErc721Storage(); + + // Clear approvals from the previous owner + delete _erc721Storage.tokenApprovals[tokenId_]; + + unchecked { + _erc721Storage.balances[from_] -= 1; + _erc721Storage.balances[to_] += 1; + } + + _getErc721Storage().owners[tokenId_] = to_; + + emit Transfer(from_, to_, tokenId_); + + _afterTokenTransfer(from_, to_, tokenId_, 1); + } + + /** + * @notice Approve `to` to operate on `tokenId`. + */ + function _approve(address to_, uint256 tokenId_) internal virtual { + _getErc721Storage().tokenApprovals[tokenId_] = to_; + + emit Approval(ownerOf(tokenId_), to_, tokenId_); + } + + /** + * @notice Approve `operator` to operate on all of `owner` tokens. + */ + function _setApprovalForAll( + address owner_, + address operator_, + bool approved_ + ) internal virtual { + require(owner_ != operator_, "ERC721: approve to caller"); + + _getErc721Storage().operatorApprovals[owner_][operator_] = approved_; + + emit ApprovalForAll(owner_, operator_, approved_); + } + + /** + * @notice Function to check if the 'to' can receive token. + * The call is not executed if the target address is not a contract. + */ + function _checkOnERC721Received( + address from_, + address to_, + uint256 tokenId_, + bytes memory data_ + ) private returns (bool) { + if (to_.isContract()) { + try IERC721Receiver(to_).onERC721Received(msg.sender, from_, tokenId_, data_) returns ( + bytes4 retval + ) { + return retval == IERC721Receiver.onERC721Received.selector; + } catch (bytes memory reason) { + if (reason.length == 0) { + revert("ERC721: transfer to non ERC721Receiver implementer"); + } else { + // @solidity memory-safe-assembly + assembly { + revert(add(32, reason), mload(reason)) + } + } + } + } else { + return true; + } + } + + /** + * @notice Hook that is called before any token transfer. This includes minting and burning. + */ + function _beforeTokenTransfer( + address from_, + address to_, + uint256 firstTokenId_, + uint256 batchSize_ + ) internal virtual { + if (batchSize_ > 1) { + // Will only trigger during construction. Batch transferring (minting) is not available afterwards. + revert("ERC721Enumerable: consecutive transfers not supported"); + } + + uint256 tokenId_ = firstTokenId_; + + if (from_ == address(0)) { + _addTokenToAllTokensEnumeration(tokenId_); + } else if (from_ != to_) { + _removeTokenFromOwnerEnumeration(from_, tokenId_); + } + + if (to_ == address(0)) { + _removeTokenFromAllTokensEnumeration(tokenId_); + } else if (to_ != from_) { + _addTokenToOwnerEnumeration(to_, tokenId_); + } + } + + /** + * @notice Hook that is called after any token transfer. This includes minting and burning. + */ + function _afterTokenTransfer( + address from_, + address to_, + uint256 firstTokenId_, + uint256 batchSize_ + ) internal virtual {} + + /** + * @notice Private function to add a token to ownership-tracking data structures. + */ + function _addTokenToOwnerEnumeration(address to, uint256 tokenId) private { + DERC721Storage storage _erc721Storage = _getErc721Storage(); + + uint256 length_ = balanceOf(to); + _erc721Storage.ownedTokens[to][length_] = tokenId; + _erc721Storage.ownedTokensIndex[tokenId] = length_; + } + + /** + * @notice Private function to add a token to token tracking data structures. + */ + function _addTokenToAllTokensEnumeration(uint256 tokenId) private { + DERC721Storage storage _erc721Storage = _getErc721Storage(); + + _erc721Storage.allTokensIndex[tokenId] = _erc721Storage.allTokens.length; + _erc721Storage.allTokens.push(tokenId); + } + + /** + * @dev Private function to remove a token from ownership-tracking data structures. + */ + function _removeTokenFromOwnerEnumeration(address from, uint256 tokenId) private { + DERC721Storage storage _erc721Storage = _getErc721Storage(); + + uint256 lastTokenIndex_ = balanceOf(from) - 1; + uint256 tokenIndex_ = _erc721Storage.ownedTokensIndex[tokenId]; + + if (tokenIndex_ != lastTokenIndex_) { + uint256 lastTokenId = _erc721Storage.ownedTokens[from][lastTokenIndex_]; + + _erc721Storage.ownedTokens[from][tokenIndex_] = lastTokenId; + _erc721Storage.ownedTokensIndex[lastTokenId] = tokenIndex_; + } + + delete _erc721Storage.ownedTokensIndex[tokenId]; + delete _erc721Storage.ownedTokens[from][lastTokenIndex_]; + } + + /** + * @dev Private function to remove a token from token tracking data structures. + */ + function _removeTokenFromAllTokensEnumeration(uint256 tokenId) private { + DERC721Storage storage _erc721Storage = _getErc721Storage(); + + // "swap and pop" pattern is used + uint256 lastTokenIndex_ = _erc721Storage.allTokens.length - 1; + uint256 tokenIndex_ = _erc721Storage.allTokensIndex[tokenId]; + uint256 lastTokenId_ = _erc721Storage.allTokens[lastTokenIndex_]; + + _erc721Storage.allTokens[tokenIndex_] = lastTokenId_; + _erc721Storage.allTokensIndex[lastTokenId_] = tokenIndex_; + + delete _erc721Storage.allTokensIndex[tokenId]; + _erc721Storage.allTokens.pop(); + } +} diff --git a/contracts/diamond/tokens/ERC721/DiamondERC721Storage.sol b/contracts/diamond/tokens/ERC721/DiamondERC721Storage.sol new file mode 100644 index 00000000..1e022cff --- /dev/null +++ b/contracts/diamond/tokens/ERC721/DiamondERC721Storage.sol @@ -0,0 +1,199 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {IERC165} from "@openzeppelin/contracts/utils/introspection/IERC165.sol"; +import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; +import {IERC721Enumerable} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Enumerable.sol"; +import {IERC721Metadata} from "@openzeppelin/contracts/token/ERC721/extensions/IERC721Metadata.sol"; +import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; + +import {DiamondERC165} from "../../introspection/DiamondERC165.sol"; +import {InitializableStorage} from "../../utils/InitializableStorage.sol"; + +/** + * @notice This is an ERC721 token Storage contract with Diamond Standard support + */ +abstract contract DiamondERC721Storage is + InitializableStorage, + DiamondERC165, + IERC721, + IERC721Metadata +{ + using Strings for uint256; + + bytes32 public constant DIAMOND_ERC721_STORAGE_SLOT = + keccak256("diamond.standard.diamond.erc721.storage"); + + struct DERC721Storage { + string name; + string symbol; + uint256[] allTokens; + mapping(uint256 => address) owners; + mapping(address => uint256) balances; + mapping(uint256 => address) tokenApprovals; + mapping(uint256 => uint256) allTokensIndex; + mapping(uint256 => uint256) ownedTokensIndex; + mapping(address => mapping(address => bool)) operatorApprovals; + mapping(address => mapping(uint256 => uint256)) ownedTokens; + } + + function _getErc721Storage() internal pure returns (DERC721Storage storage _erc721Storage) { + bytes32 slot_ = DIAMOND_ERC721_STORAGE_SLOT; + + assembly { + _erc721Storage.slot := slot_ + } + } + + /** + * @inheritdoc IERC165 + */ + function supportsInterface( + bytes4 interfaceId_ + ) public view virtual override(DiamondERC165, IERC165) returns (bool) { + return + interfaceId_ == type(IERC721).interfaceId || + interfaceId_ == type(IERC721Metadata).interfaceId || + interfaceId_ == type(IERC721Enumerable).interfaceId || + super.supportsInterface(interfaceId_); + } + + /** + * @notice The function to get the name of the token. + * @return The name of the token. + */ + function name() public view virtual override returns (string memory) { + return _getErc721Storage().name; + } + + /** + * @notice The function to get the symbol of the token. + * @return The symbol of the token. + */ + function symbol() public view virtual override returns (string memory) { + return _getErc721Storage().symbol; + } + + /** + * @notice The function to get the Uniform Resource Identifier (URI) for `tokenId` token. + * @return The URI of the token. + */ + function tokenURI(uint256 tokenId_) public view virtual override returns (string memory) { + _requireMinted(tokenId_); + + string memory baseURI_ = _baseURI(); + + return + bytes(baseURI_).length > 0 + ? string(abi.encodePacked(baseURI_, tokenId_.toString())) + : ""; + } + + /** + * @notice The function to get total amount of minted tokens. + * @return The amount of minted tokens. + */ + function totalSupply() public view virtual returns (uint256) { + return _getErc721Storage().allTokens.length; + } + + /** + * @inheritdoc IERC721 + */ + function balanceOf(address owner_) public view virtual override returns (uint256) { + return _getErc721Storage().balances[owner_]; + } + + /** + * @notice This function allows you to retrieve the NFT token ID for a specific owner at a specified index. + */ + function tokenOfOwnerByIndex( + address owner_, + uint256 index_ + ) public view virtual returns (uint256) { + require(index_ < balanceOf(owner_), "ERC721Enumerable: owner index out of bounds"); + + return _getErc721Storage().ownedTokens[owner_][index_]; + } + + /** + * @notice This function allows you to retrieve the NFT token ID at a given `index` of all the tokens stored by the contract. + */ + function tokenByIndex(uint256 index_) public view virtual returns (uint256) { + require(index_ < totalSupply(), "ERC721Enumerable: global index out of bounds"); + + return _getErc721Storage().allTokens[index_]; + } + + /** + * @inheritdoc IERC721 + */ + function ownerOf(uint256 tokenId_) public view virtual override returns (address) { + address owner = _ownerOf(tokenId_); + + require(owner != address(0), "ERC721: invalid token ID"); + + return owner; + } + + /** + * @inheritdoc IERC721 + */ + function getApproved(uint256 tokenId_) public view virtual override returns (address) { + _requireMinted(tokenId_); + + return _getErc721Storage().tokenApprovals[tokenId_]; + } + + /** + * @inheritdoc IERC721 + */ + function isApprovedForAll( + address owner_, + address operator_ + ) public view virtual override returns (bool) { + return _getErc721Storage().operatorApprovals[owner_][operator_]; + } + + /** + * @notice This function that returns the base URI that can be used to construct the URI for retrieving metadata related to the NFT collection. + */ + function _baseURI() internal view virtual returns (string memory) { + return ""; + } + + /** + * @notice The function that reverts if the `tokenId` has not been minted yet. + */ + function _requireMinted(uint256 tokenId_) internal view virtual { + require(_exists(tokenId_), "ERC721: invalid token ID"); + } + + /** + * @notice The function that returns whether `tokenId` exists. + */ + function _exists(uint256 tokenId_) internal view virtual returns (bool) { + return _ownerOf(tokenId_) != address(0); + } + + /** + * @notice The function that returns the owner of the `tokenId`. Does NOT revert if token doesn't exist. + */ + function _ownerOf(uint256 tokenId_) internal view virtual returns (address) { + return _getErc721Storage().owners[tokenId_]; + } + + /** + * @notice The function that returns whether `spender` is allowed to manage `tokenId`. + */ + function _isApprovedOrOwner( + address spender_, + uint256 tokenId_ + ) internal view virtual returns (bool) { + address owner = ownerOf(tokenId_); + + return (spender_ == owner || + isApprovedForAll(owner, spender_) || + getApproved(tokenId_) == spender_); + } +} diff --git a/contracts/interfaces/tokens/ISBT.sol b/contracts/interfaces/tokens/ISBT.sol index 8a29d8ec..251addab 100644 --- a/contracts/interfaces/tokens/ISBT.sol +++ b/contracts/interfaces/tokens/ISBT.sol @@ -5,8 +5,7 @@ pragma solidity ^0.8.4; * @notice The SBT module */ interface ISBT { - event Minted(address to, uint256 tokenId); - event Burned(address from, uint256 tokenId); + event Transfer(address indexed from, address indexed to, uint256 indexed tokenId); function name() external view returns (string memory); diff --git a/contracts/mock/diamond/tokens/DiamondERC721Mock.sol b/contracts/mock/diamond/tokens/DiamondERC721Mock.sol new file mode 100644 index 00000000..c454e955 --- /dev/null +++ b/contracts/mock/diamond/tokens/DiamondERC721Mock.sol @@ -0,0 +1,88 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.4; + +import {IERC721Receiver} from "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; +import {ERC721Holder} from "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; + +import {DiamondERC721} from "../../../diamond/tokens/ERC721/DiamondERC721.sol"; + +contract DiamondERC721Mock is DiamondERC721 { + string baseUri; + bool replaceOwner; + + constructor() { + _disableInitializers(DIAMOND_ERC721_STORAGE_SLOT); + } + + function __DiamondERC721Direct_init(string memory name_, string memory symbol_) external { + __DiamondERC721_init(name_, symbol_); + } + + function __DiamondERC721Mock_init( + string memory name_, + string memory symbol_ + ) external initializer(DIAMOND_ERC721_STORAGE_SLOT) { + __DiamondERC721_init(name_, symbol_); + } + + function toggleReplaceOwner() external { + replaceOwner = !replaceOwner; + } + + function setBaseURI(string memory baseUri_) external { + baseUri = baseUri_; + } + + function mint(address to_, uint256 tokenId_) external { + _safeMint(to_, tokenId_); + } + + function burn(uint256 tokenId_) external { + _burn(tokenId_); + } + + function transferFromMock(address from_, address to_, uint256 tokenId_) external { + _transfer(from_, to_, tokenId_); + } + + function safeTransferFromMock(address from_, address to_, uint256 tokenId_) external { + safeTransferFrom(from_, to_, tokenId_); + } + + function beforeTokenTransfer(uint256 batchSize) external { + _beforeTokenTransfer(address(this), address(this), 1, batchSize); + } + + function disableInitializers() external { + _disableInitializers(DIAMOND_ERC721_STORAGE_SLOT); + } + + function _baseURI() internal view override returns (string memory) { + super._baseURI(); + return baseUri; + } + + function _beforeTokenTransfer( + address from_, + address to_, + uint256 firstTokenId_, + uint256 batchSize_ + ) internal override { + if (replaceOwner) { + _getErc721Storage().owners[firstTokenId_] = address(this); + } else { + super._beforeTokenTransfer(from_, to_, firstTokenId_, batchSize_); + } + } +} + +contract NonERC721Receiver is IERC721Receiver { + function onERC721Received( + address, + address, + uint256, + bytes calldata + ) external pure override returns (bytes4) { + revert("ERC721Receiver: reverting onERC721Received"); + } +} diff --git a/contracts/oracles/UniswapV2Oracle.sol b/contracts/oracles/UniswapV2Oracle.sol index be56dcc9..9b41e46c 100644 --- a/contracts/oracles/UniswapV2Oracle.sol +++ b/contracts/oracles/UniswapV2Oracle.sol @@ -7,7 +7,6 @@ import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Ini import {IUniswapV2Factory} from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol"; import {IUniswapV2Pair} from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Pair.sol"; -import {UniswapV2OracleLibrary} from "@uniswap/v2-periphery/contracts/libraries/UniswapV2OracleLibrary.sol"; import {ArrayHelper} from "../libs/arrays/ArrayHelper.sol"; @@ -23,7 +22,6 @@ import {ArrayHelper} from "../libs/arrays/ArrayHelper.sol"; */ abstract contract UniswapV2Oracle is Initializable { using EnumerableSet for EnumerableSet.AddressSet; - using UniswapV2OracleLibrary for address; using ArrayHelper for uint256[]; using Math for uint256; @@ -70,8 +68,11 @@ abstract contract UniswapV2Oracle is Initializable { PairInfo storage pairInfo = _pairInfos[pair_]; uint256[] storage pairTimestamps = pairInfo.blockTimestamps; - (uint256 price0Cumulative_, uint256 price1Cumulative_, uint256 blockTimestamp_) = pair_ - .currentCumulativePrices(); + ( + uint256 price0Cumulative_, + uint256 price1Cumulative_, + uint256 blockTimestamp_ + ) = _currentCumulativePrices(pair_); if ( pairTimestamps.length == 0 || @@ -252,8 +253,11 @@ abstract contract UniswapV2Oracle is Initializable { uint256 price0_; uint256 price1_; - (uint256 price0Cumulative_, uint256 price1Cumulative_, uint256 blockTimestamp_) = pair_ - .currentCumulativePrices(); + ( + uint256 price0Cumulative_, + uint256 price1Cumulative_, + uint256 blockTimestamp_ + ) = _currentCumulativePrices(pair_); price0_ = (price0Cumulative_ - price0CumulativeOld_) / @@ -274,4 +278,35 @@ abstract contract UniswapV2Oracle is Initializable { return (pair_ != address(0), pair_); } + + /** + * @notice The private function to get the current cumulative prices of a pair. Adopted for Solidity 0.8.0 + */ + function _currentCumulativePrices( + address pair_ + ) + private + view + returns (uint256 price0Cumulative_, uint256 price1Cumulative_, uint32 blockTimestamp_) + { + unchecked { + blockTimestamp_ = uint32(block.timestamp); + price0Cumulative_ = IUniswapV2Pair(pair_).price0CumulativeLast(); + price1Cumulative_ = IUniswapV2Pair(pair_).price1CumulativeLast(); + + (uint112 reserve0_, uint112 reserve1_, uint32 timestampLast_) = IUniswapV2Pair(pair_) + .getReserves(); + + if (timestampLast_ != blockTimestamp_) { + uint32 timeElapsed_ = blockTimestamp_ - timestampLast_; + + price0Cumulative_ += + uint256((uint224(reserve1_) << 112) / reserve0_) * + timeElapsed_; + price1Cumulative_ += + uint256((uint224(reserve0_) << 112) / reserve1_) * + timeElapsed_; + } + } + } } diff --git a/contracts/package.json b/contracts/package.json index 460e151a..dc8dd4fc 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@solarity/solidity-lib", - "version": "2.6.0", + "version": "2.6.5", "license": "MIT", "author": "Distributed Lab", "readme": "README.md", diff --git a/contracts/tokens/SBT.sol b/contracts/tokens/SBT.sol index 3aa73dcd..586d8a34 100644 --- a/contracts/tokens/SBT.sol +++ b/contracts/tokens/SBT.sol @@ -1,7 +1,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.4; -import {Initializable} from "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import {ERC165Upgradeable} from "@openzeppelin/contracts-upgradeable/utils/introspection/ERC165Upgradeable.sol"; +import {IERC721Metadata} from "@openzeppelin/contracts/interfaces/IERC721Metadata.sol"; import {Strings} from "@openzeppelin/contracts/utils/Strings.sol"; import {EnumerableSet} from "@openzeppelin/contracts/utils/structs/EnumerableSet.sol"; @@ -15,7 +16,7 @@ import {ISBT} from "../interfaces/tokens/ISBT.sol"; * * Has to be inherited in order to be useful in the project */ -abstract contract SBT is ISBT, Initializable { +abstract contract SBT is ISBT, ERC165Upgradeable { using Strings for uint256; using EnumerableSet for EnumerableSet.UintSet; @@ -134,6 +135,18 @@ abstract contract SBT is ISBT, Initializable { return ""; } + /** + * @notice Returns true if this contract implements the interface defined by `interfaceId` + * @param interfaceId_ the interface ID to check + * @return true if the passed interface ID is supported, otherwise false + */ + function supportsInterface(bytes4 interfaceId_) public view virtual override returns (bool) { + return + interfaceId_ == type(IERC721Metadata).interfaceId || + interfaceId_ == type(ISBT).interfaceId || + super.supportsInterface(interfaceId_); + } + /** * @notice The function to mint the token * @param to_ the receiver of the token @@ -148,7 +161,7 @@ abstract contract SBT is ISBT, Initializable { _balances[to_].add(tokenId_); _tokenOwners[tokenId_] = to_; - emit Minted(to_, tokenId_); + emit Transfer(address(0), to_, tokenId_); } /** @@ -166,7 +179,7 @@ abstract contract SBT is ISBT, Initializable { delete _tokenURIs[tokenId_]; - emit Burned(owner_, tokenId_); + emit Transfer(owner_, address(0), tokenId_); } /** diff --git a/package-lock.json b/package-lock.json index 87565b45..ed940d02 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@solarity/solidity-lib", - "version": "2.6.0", + "version": "2.6.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@solarity/solidity-lib", - "version": "2.6.0", + "version": "2.6.5", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 65e190af..be31966d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@solarity/solidity-lib", - "version": "2.6.0", + "version": "2.6.5", "license": "MIT", "author": "Distributed Lab", "description": "Solidity Library by Distributed Lab", diff --git a/test/diamond/DiamondERC20.test.ts b/test/diamond/DiamondERC20.test.ts index 43568daa..9d6173a3 100644 --- a/test/diamond/DiamondERC20.test.ts +++ b/test/diamond/DiamondERC20.test.ts @@ -66,7 +66,7 @@ describe("DiamondERC20 and InitializableStorage", () => { expect(tx) .to.emit(contract, "Initialized") - .withArgs("0x53a65a27f49c2031551d6b34b2c7a820391e4944344eb7ed8a0fcb6ebb483840"); + .withArgs(await erc20.DIAMOND_ERC20_STORAGE_SLOT()); await expect(contract.disableInitializers()).to.be.revertedWith("Initializable: contract is initializing"); }); diff --git a/test/diamond/DiamondERC721.test.ts b/test/diamond/DiamondERC721.test.ts new file mode 100644 index 00000000..aa75ac63 --- /dev/null +++ b/test/diamond/DiamondERC721.test.ts @@ -0,0 +1,338 @@ +import { ethers } from "hardhat"; +import { SignerWithAddress } from "@nomicfoundation/hardhat-ethers/signers"; +import { expect } from "chai"; +import { Reverter } from "@/test/helpers/reverter"; +import { getSelectors, FacetAction } from "@/test/helpers/diamond-helper"; +import { ZERO_ADDR } from "@/scripts/utils/constants"; + +import { OwnableDiamondMock, DiamondERC721Mock, Diamond } from "@ethers-v6"; + +describe("DiamondERC721 and InitializableStorage", () => { + const reverter = new Reverter(); + + let OWNER: SignerWithAddress; + let SECOND: SignerWithAddress; + let THIRD: SignerWithAddress; + + let erc721: DiamondERC721Mock; + let diamond: OwnableDiamondMock; + + before("setup", async () => { + [OWNER, SECOND, THIRD] = await ethers.getSigners(); + + const OwnableDiamond = await ethers.getContractFactory("OwnableDiamondMock"); + const DiamondERC721Mock = await ethers.getContractFactory("DiamondERC721Mock"); + + diamond = await OwnableDiamond.deploy(); + erc721 = await DiamondERC721Mock.deploy(); + + const facets: Diamond.FacetStruct[] = [ + { + facetAddress: await erc721.getAddress(), + action: FacetAction.Add, + functionSelectors: getSelectors(erc721.interface), + }, + ]; + + await diamond.diamondCutShort(facets); + + erc721 = DiamondERC721Mock.attach(await diamond.getAddress()); + + await erc721.__DiamondERC721Mock_init("Mock Token", "MT"); + + await reverter.snapshot(); + }); + + afterEach(reverter.revert); + + describe("access", () => { + it("should initialize only once", async () => { + await expect(erc721.__DiamondERC721Mock_init("Mock Token", "MT")).to.be.revertedWith( + "Initializable: contract is already initialized" + ); + }); + + it("should initialize only by top level contract", async () => { + await expect(erc721.__DiamondERC721Direct_init("Mock Token", "MT")).to.be.revertedWith( + "Initializable: contract is not initializing" + ); + }); + + it("should disable implementation initialization", async () => { + const DiamondERC721Mock = await ethers.getContractFactory("DiamondERC721Mock"); + const contract = await DiamondERC721Mock.deploy(); + + let tx = contract.deploymentTransaction(); + + expect(tx) + .to.emit(contract, "Initialized") + .withArgs(await erc721.DIAMOND_ERC721_STORAGE_SLOT()); + + await expect(contract.disableInitializers()).to.be.revertedWith("Initializable: contract is initializing"); + }); + }); + + describe("getters", () => { + it("should return base data", async () => { + expect(await erc721.name()).to.equal("Mock Token"); + expect(await erc721.symbol()).to.equal("MT"); + + await erc721.mint(OWNER.address, 1); + + expect(await erc721.balanceOf(OWNER.address)).to.equal(1); + expect(await erc721.totalSupply()).to.equal(1); + + expect(await erc721.tokenOfOwnerByIndex(OWNER.address, 0)).to.equal(1); + expect(await erc721.tokenByIndex(0)).to.equal(1); + expect(await erc721.ownerOf(1)).to.equal(OWNER.address); + + await expect(erc721.tokenOfOwnerByIndex(OWNER.address, 10)).to.be.revertedWith( + "ERC721Enumerable: owner index out of bounds" + ); + await expect(erc721.tokenByIndex(10)).to.be.revertedWith("ERC721Enumerable: global index out of bounds"); + + expect(await erc721.tokenURI(1)).to.equal(""); + await erc721.setBaseURI("https://example.com/"); + expect(await erc721.tokenURI(1)).to.equal("https://example.com/1"); + + await expect(erc721.tokenURI(10)).to.be.revertedWith("ERC721: invalid token ID"); + }); + + it("should support all necessary interfaces", async () => { + // IERC721 + expect(await erc721.supportsInterface("0x80ac58cd")).to.be.true; + // IERC721Metadata + expect(await erc721.supportsInterface("0x5b5e139f")).to.be.true; + // IERC721Enumerable + expect(await erc721.supportsInterface("0x780e9d63")).to.be.true; + // IERC165 + expect(await erc721.supportsInterface("0x01ffc9a7")).to.be.true; + }); + }); + + describe("DiamondERC721 functions", () => { + describe("mint", () => { + it("should mint tokens", async () => { + const tx = erc721.mint(OWNER.address, 1); + + await expect(tx).to.emit(erc721, "Transfer").withArgs(ZERO_ADDR, OWNER.address, 1); + + expect(await erc721.balanceOf(OWNER.address)).to.equal(1); + }); + + it("should not mint tokens to zero address", async () => { + await expect(erc721.mint(ZERO_ADDR, 1)).to.be.revertedWith("ERC721: mint to the zero address"); + }); + + it("should not mint tokens if it's alredy minted", async () => { + await erc721.mint(OWNER.address, 1); + await expect(erc721.mint(OWNER.address, 1)).to.be.revertedWith("ERC721: token already minted"); + }); + + it("should not mint tokens if token is minted after `_beforeTokenTransfer` hook", async () => { + await erc721.toggleReplaceOwner(); + + await expect(erc721.mint(OWNER.address, 1)).to.be.revertedWith("ERC721: token already minted"); + }); + + it("should not mint token if the reciever is a contract and doesn't implement onERC721Received correctly", async () => { + const contract1 = await (await ethers.getContractFactory("DiamondERC721Mock")).deploy(); + + await expect(erc721.mint(await contract1.getAddress(), 1)).to.be.revertedWith( + "ERC721: transfer to non ERC721Receiver implementer" + ); + + const contract2 = await (await ethers.getContractFactory("NonERC721Receiver")).deploy(); + + await expect(erc721.mint(await contract2.getAddress(), 1)).to.be.revertedWith( + "ERC721Receiver: reverting onERC721Received" + ); + }); + }); + + describe("burn", () => { + it("should burn tokens", async () => { + await erc721.mint(OWNER.address, 1); + + expect(await erc721.balanceOf(OWNER.address)).to.equal(1); + + const tx = erc721.burn(1); + + await expect(tx).to.emit(erc721, "Transfer").withArgs(OWNER.address, ZERO_ADDR, 1); + + expect(await erc721.balanceOf(OWNER.address)).to.equal(0); + }); + + it("should not burn an incorrect token", async () => { + await expect(erc721.burn(1)).to.be.revertedWith("ERC721: invalid token ID"); + }); + }); + + describe("before token transfer hook", () => { + it("before token transfer hook should only accept one token", async () => { + expect(await erc721.beforeTokenTransfer(1)).not.to.be.reverted; + }); + + it("before token transfer hook should not accept more than one token", async () => { + await expect(erc721.beforeTokenTransfer(2)).to.be.revertedWith( + "ERC721Enumerable: consecutive transfers not supported" + ); + }); + }); + + describe("transfer/safeTransfer", () => { + it("should transfer tokens", async () => { + await erc721.mint(OWNER.address, 1); + + expect(await erc721.balanceOf(OWNER.address)).to.equal(1); + expect(await erc721.balanceOf(SECOND.address)).to.equal(0); + + const tx = erc721.transferFrom(OWNER.address, SECOND, 1); + + await expect(tx).to.emit(erc721, "Transfer").withArgs(OWNER.address, SECOND.address, 1); + + expect(await erc721.balanceOf(OWNER.address)).to.equal(0); + expect(await erc721.balanceOf(SECOND.address)).to.equal(1); + }); + + it("should safely transfer tokens", async () => { + await erc721.mint(OWNER.address, 1); + await erc721.mint(OWNER.address, 2); + + expect(await erc721.balanceOf(OWNER.address)).to.equal(2); + expect(await erc721.balanceOf(SECOND.address)).to.equal(0); + + const tx = erc721.safeTransferFromMock(OWNER.address, SECOND, 1); + + await expect(tx).to.emit(erc721, "Transfer").withArgs(OWNER.address, SECOND.address, 1); + + expect(await erc721.balanceOf(OWNER.address)).to.equal(1); + expect(await erc721.balanceOf(SECOND.address)).to.equal(1); + }); + + it("should safely transfer tokens to the contract if it implements onERC721Received correctly", async () => { + await erc721.mint(OWNER.address, 1); + + expect(await erc721.balanceOf(OWNER.address)).to.equal(1); + expect(await erc721.balanceOf(SECOND.address)).to.equal(0); + + const receiver = await (await ethers.getContractFactory("ERC721Holder")).deploy(); + const tx = erc721.safeTransferFromMock(OWNER.address, await receiver.getAddress(), 1); + + await expect(tx) + .to.emit(erc721, "Transfer") + .withArgs(OWNER.address, await receiver.getAddress(), 1); + + expect(await erc721.balanceOf(OWNER.address)).to.equal(0); + expect(await erc721.balanceOf(await receiver.getAddress())).to.equal(1); + }); + + it("should not transfer tokens when caller is not an owner or not approved", async () => { + await erc721.mint(OWNER.address, 1); + + await expect(erc721.connect(SECOND).transferFrom(OWNER.address, SECOND.address, 1)).to.be.revertedWith( + "ERC721: caller is not token owner or approved" + ); + await expect(erc721.connect(SECOND).safeTransferFromMock(OWNER.address, SECOND.address, 1)).to.be.revertedWith( + "ERC721: caller is not token owner or approved" + ); + }); + + it("should not transfer tokens when call is not an owner", async () => { + await erc721.mint(OWNER.address, 1); + + await expect(erc721.transferFromMock(SECOND.address, OWNER.address, 1)).to.be.revertedWith( + "ERC721: transfer from incorrect owner" + ); + }); + + it("should not transfer tokens to zero address", async () => { + await erc721.mint(OWNER.address, 1); + + await expect(erc721.transferFromMock(OWNER.address, ZERO_ADDR, 1)).to.be.revertedWith( + "ERC721: transfer to the zero address" + ); + }); + + it("should not transfer tokens if owner is changed after `_beforeTokenTransfer` hook", async () => { + await erc721.mint(OWNER.address, 1); + + await erc721.toggleReplaceOwner(); + + await expect(erc721.transferFromMock(OWNER.address, SECOND.address, 1)).to.be.revertedWith( + "ERC721: transfer from incorrect owner" + ); + }); + + it("should not transfer token if the reciever is a contract and doesn't implement onERC721Received", async () => { + await erc721.mint(OWNER.address, 1); + + const contract = await (await ethers.getContractFactory("DiamondERC721Mock")).deploy(); + + await expect(erc721.safeTransferFromMock(OWNER.address, await contract.getAddress(), 1)).to.be.revertedWith( + "ERC721: transfer to non ERC721Receiver implementer" + ); + }); + }); + + describe("approve/approveAll", () => { + it("should approve tokens", async () => { + await erc721.mint(OWNER.address, 1); + + const tx = erc721.approve(SECOND.address, 1); + + await expect(tx).to.emit(erc721, "Approval").withArgs(OWNER.address, SECOND.address, 1); + + expect(await erc721.getApproved(1)).to.equal(SECOND.address); + expect(await erc721.connect(SECOND).transferFrom(OWNER.address, THIRD.address, 1)).not.to.be.reverted; + + await erc721.mint(OWNER.address, 2); + await erc721.mint(OWNER.address, 3); + await erc721.setApprovalForAll(SECOND.address, true); + + await erc721.connect(SECOND).approve(THIRD.address, 3); + + expect(await erc721.getApproved(3)).to.equal(THIRD.address); + expect(await erc721.connect(THIRD).transferFrom(OWNER.address, SECOND.address, 3)).not.to.be.reverted; + }); + + it("should not approve incorrect token", async () => { + await expect(erc721.approve(OWNER.address, 1)).to.be.revertedWith("ERC721: invalid token ID"); + }); + + it("should not approve token if caller is not an owner", async () => { + await erc721.mint(OWNER.address, 1); + await expect(erc721.connect(SECOND).approve(THIRD.address, 1)).to.be.revertedWith( + "ERC721: approve caller is not token owner or approved for all" + ); + }); + + it("should not approve token if spender and caller are the same", async () => { + await erc721.mint(OWNER.address, 1); + + await expect(erc721.approve(OWNER.address, 1)).to.be.revertedWith("ERC721: approval to current owner"); + }); + + it("should approve all tokens", async () => { + await erc721.mint(OWNER.address, 1); + await erc721.mint(OWNER.address, 2); + await erc721.mint(OWNER.address, 3); + const tx = erc721.setApprovalForAll(SECOND.address, true); + + await expect(tx).to.emit(erc721, "ApprovalForAll").withArgs(OWNER.address, SECOND.address, true); + + expect(await erc721.isApprovedForAll(OWNER.address, SECOND.address)).to.be.true; + + expect(await erc721.connect(SECOND).transferFrom(OWNER.address, THIRD.address, 1)).not.to.be.reverted; + }); + + it("should not approve all tokens if owner the same as operator", async () => { + await erc721.mint(OWNER.address, 1); + await erc721.mint(OWNER.address, 2); + await erc721.mint(OWNER.address, 3); + + await expect(erc721.setApprovalForAll(OWNER.address, true)).to.be.revertedWith("ERC721: approve to caller"); + }); + }); + }); +}); diff --git a/test/token/SBT.test.ts b/test/token/SBT.test.ts index 036002f1..1b924f1e 100644 --- a/test/token/SBT.test.ts +++ b/test/token/SBT.test.ts @@ -47,7 +47,7 @@ describe("SBT", () => { describe("mint()", () => { it("should correctly mint", async () => { - await sbt.mint(FIRST.address, 1337); + const tx = await sbt.mint(FIRST.address, 1337); expect(await sbt.tokenExists(1337)).to.be.true; @@ -58,6 +58,8 @@ describe("SBT", () => { expect(await sbt.tokensOf(FIRST.address)).to.deep.equal([1337n]); expect(await sbt.tokenURI(1337)).to.equal(""); + + expect(tx).to.emit(sbt, "Transfer").withArgs(ZERO_ADDR, FIRST.address, 1337); }); it("should not mint to null address", async () => { @@ -73,7 +75,7 @@ describe("SBT", () => { describe("burn()", () => { it("should correctly burn", async () => { - await sbt.mint(FIRST.address, 1337); + const tx = await sbt.mint(FIRST.address, 1337); await sbt.burn(1337); @@ -83,6 +85,8 @@ describe("SBT", () => { expect(await sbt.ownerOf(0)).to.equal(ZERO_ADDR); expect(await sbt.tokensOf(FIRST.address)).to.deep.equal([]); + + expect(tx).to.emit(sbt, "Transfer").withArgs(FIRST.address, ZERO_ADDR, 1337); }); it("should not burn SBT that doesn't exist", async () => { @@ -129,4 +133,20 @@ describe("SBT", () => { expect(await sbt.tokenURI(1337)).to.equal("test"); }); }); + + describe("supportsInterface()", () => { + it("should return correct values", async () => { + const IERC721MetadaInterfaceID = "0x5b5e139f"; + const ISBTInterfaceID = "0xddd872b5"; + const IERC165InterfaceID = "0x01ffc9a7"; + + expect(await sbt.supportsInterface(IERC721MetadaInterfaceID)).to.be.eq(true); + expect(await sbt.supportsInterface(ISBTInterfaceID)).to.be.eq(true); + expect(await sbt.supportsInterface(IERC165InterfaceID)).to.be.eq(true); + + const randomInterfaceID = "0xaaa1234d"; + + expect(await sbt.supportsInterface(randomInterfaceID)).to.be.eq(false); + }); + }); });