diff --git a/assets/multiwrap-diagram.png b/assets/multiwrap-diagram.png new file mode 100644 index 000000000..e435acb27 Binary files /dev/null and b/assets/multiwrap-diagram.png differ diff --git a/contracts/feature/ContractMetadata.sol b/contracts/feature/ContractMetadata.sol index 136bf7c60..694cff244 100644 --- a/contracts/feature/ContractMetadata.sol +++ b/contracts/feature/ContractMetadata.sol @@ -8,9 +8,12 @@ abstract contract ContractMetadata is IContractMetadata { string public override contractURI; /// @dev Lets a contract admin set the URI for contract-level metadata. - function setContractURI(string calldata _uri) external override { + function setContractURI(string memory _uri) public override { require(_canSetContractURI(), "Not authorized"); + string memory prevURI = contractURI; contractURI = _uri; + + emit ContractURIUpdated(prevURI, _uri); } /// @dev Returns whether contract metadata can be set in the given execution context. diff --git a/contracts/feature/Permissions.sol b/contracts/feature/Permissions.sol index 60b8e265c..d7a1500b8 100644 --- a/contracts/feature/Permissions.sol +++ b/contracts/feature/Permissions.sol @@ -19,32 +19,31 @@ contract Permissions is IPermissions { return _hasRole[role][account]; } + function hasRoleWithSwitch(bytes32 role, address account) public view returns (bool) { + if (!_hasRole[role][address(0)]) { + return _hasRole[role][account]; + } + + return true; + } + function getRoleAdmin(bytes32 role) public view override returns (bytes32) { return _getRoleAdmin[role]; } function grantRole(bytes32 role, address account) public virtual override { _checkRole(_getRoleAdmin[role], msg.sender); - - _hasRole[role][account] = true; - - emit RoleGranted(role, account, msg.sender); + _setupRole(role, account); } function revokeRole(bytes32 role, address account) public virtual override { _checkRole(_getRoleAdmin[role], msg.sender); - - delete _hasRole[role][account]; - - emit RoleRevoked(role, account, msg.sender); + _revokeRole(role, account); } function renounceRole(bytes32 role, address account) public virtual override { require(msg.sender == account, "Can only renounce for self"); - - delete _hasRole[role][account]; - - emit RoleRevoked(role, account, msg.sender); + _revokeRole(role, account); } function _setRoleAdmin(bytes32 role, bytes32 adminRole) internal virtual { @@ -58,12 +57,32 @@ contract Permissions is IPermissions { emit RoleGranted(role, account, msg.sender); } + function _revokeRole(bytes32 role, address account) internal virtual { + delete _hasRole[role][account]; + emit RoleRevoked(role, account, msg.sender); + } + function _checkRole(bytes32 role, address account) internal view virtual { if (!_hasRole[role][account]) { revert( string( abi.encodePacked( - "AccessControl: account ", + "Permissions: account ", + Strings.toHexString(uint160(account), 20), + " is missing role ", + Strings.toHexString(uint256(role), 32) + ) + ) + ); + } + } + + function _checkRoleWithSwitch(bytes32 role, address account) internal view virtual { + if (!hasRoleWithSwitch(role, account)) { + revert( + string( + abi.encodePacked( + "Permissions: account ", Strings.toHexString(uint160(account), 20), " is missing role ", Strings.toHexString(uint256(role), 32) diff --git a/contracts/feature/TokenStore.sol b/contracts/feature/TokenStore.sol new file mode 100644 index 000000000..220a2a770 --- /dev/null +++ b/contracts/feature/TokenStore.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +// ========== External imports ========== + +import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol"; +import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; + +import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; +import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol"; + +// ========== Internal imports ========== + +import "./TokenBundle.sol"; +import "../lib/CurrencyTransferLib.sol"; + +contract TokenStore is TokenBundle, ERC721Holder, ERC1155Holder { + /// @dev The address of the native token wrapper contract. + address private immutable nativeTokenWrapper; + + constructor(address _nativeTokenWrapper) { + nativeTokenWrapper = _nativeTokenWrapper; + } + + /// @dev Store / escrow multiple ERC1155, ERC721, ERC20 tokens. + function _storeTokens( + address _tokenOwner, + Token[] calldata _tokens, + string calldata _uriForTokens, + uint256 _idForTokens + ) internal { + _setBundle(_tokens, _idForTokens); + _setUriOfBundle(_uriForTokens, _idForTokens); + _transferTokenBatch(_tokenOwner, address(this), _tokens); + } + + /// @dev Release stored / escrowed ERC1155, ERC721, ERC20 tokens. + function _releaseTokens(address _recipient, uint256 _idForContent) internal { + uint256 count = getTokenCountOfBundle(_idForContent); + Token[] memory tokensToRelease = new Token[](count); + + for (uint256 i = 0; i < count; i += 1) { + tokensToRelease[i] = getTokenOfBundle(_idForContent, i); + } + + _deleteBundle(_idForContent); + + _transferTokenBatch(address(this), _recipient, tokensToRelease); + } + + /// @dev Transfers an arbitrary ERC20 / ERC721 / ERC1155 token. + function _transferToken( + address _from, + address _to, + Token memory _token + ) internal { + if (_token.tokenType == TokenType.ERC20) { + CurrencyTransferLib.transferCurrencyWithWrapper( + _token.assetContract, + _from, + _to, + _token.totalAmount, + nativeTokenWrapper + ); + } else if (_token.tokenType == TokenType.ERC721) { + IERC721(_token.assetContract).safeTransferFrom(_from, _to, _token.tokenId); + } else if (_token.tokenType == TokenType.ERC1155) { + IERC1155(_token.assetContract).safeTransferFrom(_from, _to, _token.tokenId, _token.totalAmount, ""); + } + } + + /// @dev Transfers multiple arbitrary ERC20 / ERC721 / ERC1155 tokens. + function _transferTokenBatch( + address _from, + address _to, + Token[] memory _tokens + ) internal { + for (uint256 i = 0; i < _tokens.length; i += 1) { + _transferToken(_from, _to, _tokens[i]); + } + } +} diff --git a/contracts/feature/interface/IContractMetadata.sol b/contracts/feature/interface/IContractMetadata.sol index afbd2b5df..bf61c4979 100644 --- a/contracts/feature/interface/IContractMetadata.sol +++ b/contracts/feature/interface/IContractMetadata.sol @@ -10,4 +10,6 @@ interface IContractMetadata { * Only module admin can call this function. */ function setContractURI(string calldata _uri) external; + + event ContractURIUpdated(string prevURI, string newURI); } diff --git a/contracts/interfaces/IMultiwrap.sol b/contracts/interfaces/IMultiwrap.sol index eba4e8b9a..afe1facf7 100644 --- a/contracts/interfaces/IMultiwrap.sol +++ b/contracts/interfaces/IMultiwrap.sol @@ -1,6 +1,8 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; +import "../feature/interface/ITokenBundle.sol"; + /** * Thirdweb's Multiwrap contract lets you wrap arbitrary ERC20, ERC721 and ERC1155 * tokens you own into a single wrapped token / NFT. @@ -8,40 +10,7 @@ pragma solidity ^0.8.11; * A wrapped NFT can be unwrapped i.e. burned in exchange for its underlying contents. */ -interface IMultiwrap { - /// @notice The type of assets that can be wrapped. - enum TokenType { - ERC20, - ERC721, - ERC1155 - } - - /** - * @notice A generic interface to describe a token to wrap. - * - * @param assetContract The contract address of the asset to wrap. - * @param tokenType The token type (ERC20 / ERC721 / ERC1155) of the asset to wrap. - * @param tokenId The token Id of the asset to wrap, if the asset is an ERC721 / ERC1155 NFT. - * @param amount The amount of the asset to wrap, if the asset is an ERC20 / ERC1155 fungible token. - */ - struct Token { - address assetContract; - TokenType tokenType; - uint256 tokenId; - uint256 amount; - } - - /** - * @notice An internal data structure to track the wrapped contents of a wrapped NFT. - * - * @param count The total kinds of assets i.e. `Token` wrapped. - * @param token Mapping from a UID -> to the asset i.e. `Token` at that UID. - */ - struct WrappedContents { - uint256 count; - mapping(uint256 => Token) token; - } - +interface IMultiwrap is ITokenBundle { /// @dev Emitted when tokens are wrapped. event TokensWrapped( address indexed wrapper, @@ -54,8 +23,7 @@ interface IMultiwrap { event TokensUnwrapped( address indexed unwrapper, address indexed recipientOfWrappedContents, - uint256 indexed tokenIdOfWrappedToken, - Token[] wrappedContents + uint256 indexed tokenIdOfWrappedToken ); /** diff --git a/contracts/multiwrap/Multiwrap.sol b/contracts/multiwrap/Multiwrap.sol index 12da71c82..78bfd9032 100644 --- a/contracts/multiwrap/Multiwrap.sol +++ b/contracts/multiwrap/Multiwrap.sol @@ -4,39 +4,33 @@ pragma solidity ^0.8.11; // ========== External imports ========== import "@openzeppelin/contracts-upgradeable/token/ERC721/ERC721Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; - -import "@openzeppelin/contracts-upgradeable/token/ERC1155/utils/ERC1155HolderUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC721/utils/ERC721HolderUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC1155/IERC1155Upgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/token/ERC721/IERC721Upgradeable.sol"; - import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; -import "@openzeppelin/contracts-upgradeable/access/AccessControlEnumerableUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; // ========== Internal imports ========== -import "../interfaces/IThirdwebContract.sol"; -import "../feature/interface/IRoyalty.sol"; -import "../feature/interface/IOwnable.sol"; - import "../interfaces/IMultiwrap.sol"; -import "../lib/CurrencyTransferLib.sol"; import "../openzeppelin-presets/metatx/ERC2771ContextUpgradeable.sol"; +// ========== Features ========== + +import "../feature/ContractMetadata.sol"; +import "../feature/Royalty.sol"; +import "../feature/Ownable.sol"; +import "../feature/PermissionsEnumerable.sol"; +import "../feature/TokenStore.sol"; + contract Multiwrap is - IThirdwebContract, - IOwnable, - IRoyalty, + Initializable, + ContractMetadata, + Royalty, + Ownable, + PermissionsEnumerable, + TokenStore, ReentrancyGuardUpgradeable, ERC2771ContextUpgradeable, MulticallUpgradeable, - AccessControlEnumerableUpgradeable, - ERC1155HolderUpgradeable, - ERC721HolderUpgradeable, ERC721Upgradeable, IMultiwrap { @@ -53,48 +47,17 @@ contract Multiwrap is bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); /// @dev Only UNWRAP_ROLE holders can unwrap tokens, when unwrapping is restricted. bytes32 private constant UNWRAP_ROLE = keccak256("UNWRAP_ROLE"); - - /// @dev Owner of the contract (purpose: OpenSea compatibility) - address private _owner; - - /// @dev The address of the native token wrapper contract. - address private immutable nativeTokenWrapper; + /// @dev Only assets with ASSET_ROLE can be wrapped, when wrapping is restricted to particular assets. + bytes32 private constant ASSET_ROLE = keccak256("ASSET_ROLE"); /// @dev The next token ID of the NFT to mint. uint256 public nextTokenIdToMint; - /// @dev The (default) address that receives all royalty value. - address private royaltyRecipient; - - /// @dev The (default) % of a sale to take as royalty (in basis points). - uint128 private royaltyBps; - - /// @dev Max bps in the thirdweb system. - uint128 private constant MAX_BPS = 10_000; - - /// @dev Contract level metadata. - string public contractURI; - - /*/////////////////////////////////////////////////////////////// - Mappings - //////////////////////////////////////////////////////////////*/ - - /// @dev Mapping from tokenId of wrapped NFT => royalty recipient and bps for token. - mapping(uint256 => RoyaltyInfo) private royaltyInfoForToken; - - /// @dev Mapping from tokenId of wrapped NFT => uri for the token. - mapping(uint256 => string) private uri; - - /// @dev Mapping from tokenId of wrapped NFT => wrapped contents of the token. - mapping(uint256 => WrappedContents) private wrappedContents; - /*/////////////////////////////////////////////////////////////// Constructor + initializer logic //////////////////////////////////////////////////////////////*/ - constructor(address _nativeTokenWrapper) initializer { - nativeTokenWrapper = _nativeTokenWrapper; - } + constructor(address _nativeTokenWrapper) TokenStore(_nativeTokenWrapper) initializer {} /// @dev Initiliazes the contract, like a constructor. function initialize( @@ -111,29 +74,36 @@ contract Multiwrap is __ERC2771Context_init(_trustedForwarders); __ERC721_init(_name, _symbol); + // Revoked at the end of the function. + _setupRole(DEFAULT_ADMIN_ROLE, _msgSender()); + // Initialize this contract's state. - royaltyRecipient = _royaltyRecipient; - royaltyBps = uint128(_royaltyBps); - contractURI = _contractURI; - _owner = _defaultAdmin; + setDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + setOwner(_defaultAdmin); + setContractURI(_contractURI); _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); _setupRole(MINTER_ROLE, _defaultAdmin); _setupRole(TRANSFER_ROLE, _defaultAdmin); + + // note: see `_beforeTokenTransfer` for TRANSFER_ROLE behaviour. _setupRole(TRANSFER_ROLE, address(0)); + + // note: see `onlyRoleWithSwitch` for UNWRAP_ROLE behaviour. _setupRole(UNWRAP_ROLE, address(0)); + + // note: see `onlyRoleWithSwitch` for UNWRAP_ROLE behaviour. + _setupRole(ASSET_ROLE, address(0)); + + _revokeRole(DEFAULT_ADMIN_ROLE, _msgSender()); } /*/////////////////////////////////////////////////////////////// Modifiers //////////////////////////////////////////////////////////////*/ - modifier onlyMinter() { - // if transfer is restricted on the contract, we still want to allow burning and minting - if (!hasRole(MINTER_ROLE, address(0))) { - require(hasRole(MINTER_ROLE, _msgSender()), "restricted to MINTER_ROLE holders."); - } - + modifier onlyRoleWithSwitch(bytes32 role) { + _checkRoleWithSwitch(role, _msgSender()); _; } @@ -151,20 +121,13 @@ contract Multiwrap is return uint8(VERSION); } - /** - * @dev Returns the address of the current owner. - */ - function owner() external view returns (address) { - return hasRole(DEFAULT_ADMIN_ROLE, _owner) ? _owner : address(0); - } - /*/////////////////////////////////////////////////////////////// ERC 165 / 721 / 2981 logic //////////////////////////////////////////////////////////////*/ /// @dev Returns the URI for a given tokenId. function tokenURI(uint256 _tokenId) public view override returns (string memory) { - return uri[_tokenId]; + return getUriOfBundle(_tokenId); } /// @dev See ERC 165 @@ -172,7 +135,7 @@ contract Multiwrap is public view virtual - override(AccessControlEnumerableUpgradeable, ERC1155ReceiverUpgradeable, ERC721Upgradeable) + override(ERC1155Receiver, ERC721Upgradeable) returns (bool) { return @@ -181,169 +144,74 @@ contract Multiwrap is interfaceId == type(IERC2981Upgradeable).interfaceId; } - /// @dev Returns the royalty recipient and amount, given a tokenId and sale price. - function royaltyInfo(uint256 tokenId, uint256 salePrice) - external - view - virtual - returns (address receiver, uint256 royaltyAmount) - { - (address recipient, uint256 bps) = getRoyaltyInfoForToken(tokenId); - receiver = recipient; - royaltyAmount = (salePrice * bps) / MAX_BPS; - } - /*/////////////////////////////////////////////////////////////// Wrapping / Unwrapping logic //////////////////////////////////////////////////////////////*/ /// @dev Wrap multiple ERC1155, ERC721, ERC20 tokens into a single wrapped NFT. function wrap( - Token[] calldata _wrappedContents, + Token[] calldata _tokensToWrap, string calldata _uriForWrappedToken, address _recipient - ) external payable nonReentrant onlyMinter returns (uint256 tokenId) { + ) external payable nonReentrant onlyRoleWithSwitch(MINTER_ROLE) returns (uint256 tokenId) { + if (!hasRole(ASSET_ROLE, address(0))) { + for (uint256 i = 0; i < _tokensToWrap.length; i += 1) { + _checkRole(ASSET_ROLE, _tokensToWrap[i].assetContract); + } + } + tokenId = nextTokenIdToMint; nextTokenIdToMint += 1; - for (uint256 i = 0; i < _wrappedContents.length; i += 1) { - wrappedContents[tokenId].token[i] = _wrappedContents[i]; - } - wrappedContents[tokenId].count = _wrappedContents.length; - - uri[tokenId] = _uriForWrappedToken; + _storeTokens(_msgSender(), _tokensToWrap, _uriForWrappedToken, tokenId); _safeMint(_recipient, tokenId); - transferTokenBatch(_msgSender(), address(this), _wrappedContents); - - emit TokensWrapped(_msgSender(), _recipient, tokenId, _wrappedContents); + emit TokensWrapped(_msgSender(), _recipient, tokenId, _tokensToWrap); } /// @dev Unwrap a wrapped NFT to retrieve underlying ERC1155, ERC721, ERC20 tokens. - function unwrap(uint256 _tokenId, address _recipient) external nonReentrant { - if (!hasRole(TRANSFER_ROLE, address(0))) { - require(hasRole(TRANSFER_ROLE, _msgSender()), "restricted to UNWRAP_ROLE holders."); - } - require(_tokenId < nextTokenIdToMint, "invalid tokenId"); - require(_isApprovedOrOwner(_msgSender(), _tokenId), "unapproved called"); + function unwrap(uint256 _tokenId, address _recipient) external nonReentrant onlyRoleWithSwitch(UNWRAP_ROLE) { + require(_tokenId < nextTokenIdToMint, "Multiwrap: wrapped NFT DNE."); + require(_isApprovedOrOwner(_msgSender(), _tokenId), "Multiwrap: caller not approved for unwrapping."); _burn(_tokenId); + _releaseTokens(_recipient, _tokenId); - uint256 count = wrappedContents[_tokenId].count; - Token[] memory tokensUnwrapped = new Token[](count); - - for (uint256 i = 0; i < count; i += 1) { - tokensUnwrapped[i] = wrappedContents[_tokenId].token[i]; - transferToken(address(this), _recipient, tokensUnwrapped[i]); - } - - delete wrappedContents[_tokenId]; - - emit TokensUnwrapped(_msgSender(), _recipient, _tokenId, tokensUnwrapped); - } - - /// @dev Transfers an arbitrary ERC20 / ERC721 / ERC1155 token. - function transferToken( - address _from, - address _to, - Token memory _token - ) internal { - if (_token.tokenType == TokenType.ERC20) { - CurrencyTransferLib.transferCurrencyWithWrapper( - _token.assetContract, - _from, - _to, - _token.amount, - nativeTokenWrapper - ); - } else if (_token.tokenType == TokenType.ERC721) { - IERC721Upgradeable(_token.assetContract).safeTransferFrom(_from, _to, _token.tokenId); - } else if (_token.tokenType == TokenType.ERC1155) { - IERC1155Upgradeable(_token.assetContract).safeTransferFrom(_from, _to, _token.tokenId, _token.amount, ""); - } - } - - /// @dev Transfers multiple arbitrary ERC20 / ERC721 / ERC1155 tokens. - function transferTokenBatch( - address _from, - address _to, - Token[] memory _tokens - ) internal { - for (uint256 i = 0; i < _tokens.length; i += 1) { - transferToken(_from, _to, _tokens[i]); - } + emit TokensUnwrapped(_msgSender(), _recipient, _tokenId); } /*/////////////////////////////////////////////////////////////// Getter functions //////////////////////////////////////////////////////////////*/ - /// @dev Returns the platform fee bps and recipient. - function getDefaultRoyaltyInfo() external view returns (address, uint16) { - return (royaltyRecipient, uint16(royaltyBps)); - } - - /// @dev Returns the royalty recipient for a particular token Id. - function getRoyaltyInfoForToken(uint256 _tokenId) public view returns (address, uint16) { - RoyaltyInfo memory royaltyForToken = royaltyInfoForToken[_tokenId]; - - return - royaltyForToken.recipient == address(0) - ? (royaltyRecipient, uint16(royaltyBps)) - : (royaltyForToken.recipient, uint16(royaltyForToken.bps)); - } - - /// @dev Returns the underlygin contents of a wrapped NFT. + /// @dev Returns the underlying contents of a wrapped NFT. function getWrappedContents(uint256 _tokenId) external view returns (Token[] memory contents) { - uint256 total = wrappedContents[_tokenId].count; + uint256 total = getTokenCountOfBundle(_tokenId); contents = new Token[](total); for (uint256 i = 0; i < total; i += 1) { - contents[i] = wrappedContents[_tokenId].token[i]; + contents[i] = getTokenOfBundle(_tokenId, i); } } /*/////////////////////////////////////////////////////////////// - Setter functions + Internal functions //////////////////////////////////////////////////////////////*/ - /// @dev Lets a module admin update the royalty bps and recipient. - function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) - external - onlyRole(DEFAULT_ADMIN_ROLE) - { - require(_royaltyBps <= MAX_BPS, "exceed royalty bps"); - - royaltyRecipient = _royaltyRecipient; - royaltyBps = uint128(_royaltyBps); - - emit DefaultRoyalty(_royaltyRecipient, _royaltyBps); - } - - /// @dev Lets a module admin set the royalty recipient for a particular token Id. - function setRoyaltyInfoForToken( - uint256 _tokenId, - address _recipient, - uint256 _bps - ) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(_bps <= MAX_BPS, "exceed royalty bps"); - - royaltyInfoForToken[_tokenId] = RoyaltyInfo({ recipient: _recipient, bps: _bps }); - - emit RoyaltyForToken(_tokenId, _recipient, _bps); + /// @dev Returns whether owner can be set in the given execution context. + function _canSetOwner() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); } - /// @dev Lets a module admin set a new owner for the contract. The new owner must be a module admin. - function setOwner(address _newOwner) external onlyRole(DEFAULT_ADMIN_ROLE) { - require(hasRole(DEFAULT_ADMIN_ROLE, _newOwner), "new owner not module admin."); - emit OwnerUpdated(_owner, _newOwner); - _owner = _newOwner; + /// @dev Returns whether royalty info can be set in the given execution context. + function _canSetRoyaltyInfo() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); } - /// @dev Lets a module admin set the URI for contract-level metadata. - function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { - contractURI = _uri; + /// @dev Returns whether contract metadata can be set in the given execution context. + function _canSetContractURI() internal view override returns (bool) { + return hasRole(DEFAULT_ADMIN_ROLE, _msgSender()); } /*/////////////////////////////////////////////////////////////// @@ -362,7 +230,7 @@ contract Multiwrap is // if transfer is restricted on the contract, we still want to allow burning and minting if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { - require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "restricted to TRANSFER_ROLE holders."); + require(hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), "!TRANSFER_ROLE"); } } diff --git a/contracts/multiwrap/multiwrap.md b/contracts/multiwrap/multiwrap.md index b11ec5870..015fbdce6 100644 --- a/contracts/multiwrap/multiwrap.md +++ b/contracts/multiwrap/multiwrap.md @@ -1,8 +1,8 @@ # Multiwrap design document. -This is a live document that explains what the [thirdweb](https://thirdweb.com/) `Multiwrap` smart contract is, how it works and can be used, and why it is written the way it is. +This is a live document that explains what the [thirdweb](https://thirdweb.com/) `Multiwrap` smart contract is, how it works and can be used, and why it is designed the way it is. -The document is written for technical and non-technical readers. To ask further questions about thirdweb’s `Multiwrap`, please join the [thirdweb discord](https://discord.gg/thirdweb) or create a github issue. +The document is written for technical and non-technical readers. To ask further questions about thirdweb’s `Multiwrap` contract, please join the [thirdweb discord](https://discord.gg/thirdweb) or create a github issue. --- @@ -10,29 +10,47 @@ The document is written for technical and non-technical readers. To ask further The thirdweb Multiwrap contract lets you wrap arbitrary ERC20, ERC721 and ERC1155 tokens you own into a single wrapped token / NFT. -The `Multiwrap` contract is meant to be used for bundling up multiple assets (ERC20 / ERC721 / ERC1155) into a single wrapped token, which can then -be unwrapped in exchange for the underlying tokens. +The `Multiwrap` contract is meant to be used for bundling up multiple assets (ERC20 / ERC721 / ERC1155) into a single wrapped token, which can then be unwrapped in exchange for the underlying tokens. -The single wrapped token received on bundling up multiple assets, as mentioned above, is an ERC721 NFT. It can be transferred, sold on any NFT Marketplace, and -generate royalties just like any other NFTs. +The single wrapped token received on bundling up multiple assets, as mentioned above, is an ERC721 NFT. It can be transferred, sold on any NFT Marketplace, and generate royalties just like any other NFTs. + +### How the `Multiwrap` product *should* work + +![multiwrap-diagram.png](/assets/multiwrap-diagram.png) + +A token owner should be able to wrap any combination of *n* ERC20, ERC721 or ERC1155 tokens as a wrapped NFT. When wrapping, the token owner should be able to specify a recipient for the wrapped NFT. At the time of wrapping, the token owner should be able to set the metadata of the wrapped NFT that will be minted. + +The wrapped NFT owner should be able to unwrap the the NFT to retrieve the underlying tokens of the wrapped NFT. At the time of unwrapping, the wrapped NFT owner should be able to specify a recipient for the underlying tokens of the wrapped NFT. + +The `Multiwrap` contract creator should be able to apply the following role-based restrictions: + +- Restrict what assets can be wrapped on the contract. +- Restrict which wallets can wrap tokens on the contract. +- Restrict what wallets can unwrap owned wrapped NFTs. + +### Core parts of the `Multiwrap` product +- A token owner should be able to wrap any combination of *n* ERC20, ERC721 or ERC1155 tokens as a wrapped token. +- A wrapped token owner should be able to unwrap the token to retrieve the underlying contents of the wrapped token. ### Why we’re building `Multiwrap` -We're building `Multiwrap` for cases where an application wishes to bundle up / distribute / transact over *n* independent tokens all at once, as a single asset. This opens -up several novel NFT use cases. +We're building `Multiwrap` for cases where an application wishes to bundle up / distribute / transact over *n* independent tokens all at once, as a single asset. This opens up several novel NFT use cases. -For example, consider a lending service where people can take out a loan while putting up an NFT as a collateral. Using `Multiwrap`, a borrower can wrap their NFT with -some ether, and put up the resultant wrapped ERC721 NFT as collateral on the lending service. Now, the bowwoer's NFT, as collateral, has a floor value. +For example, consider a lending service where people can take out a loan while putting up an NFT as a collateral. Using `Multiwrap`, a borrower can wrap their NFT with some ether, and put up the resultant wrapped ERC721 NFT as collateral on the lending service. Now, the borrower's NFT, as collateral, has a floor value. ## Technical Details -The `Multiwrap` contract itself is an ERC721 contract. It lets you wrap arbitrary ERC20, ERC721 and ERC1155 tokens you own into a single wrapped token / NFT. This means -escrowing the relevant ERC20, ERC721 and ERC1155 tokens into the `Multiwrap` contract, and receiving the wrapped NFT in exchange. This wrapped NFT can later be 'unwrapped' -i.e. burned in exchange for the underlying tokens. +The `Multiwrap`contract itself is an ERC721 contract. + +It lets you wrap arbitrary ERC20, ERC721 or ERC1155 tokens you own into a single wrapped token / NFT. This means escrowing the relevant ERC20, ERC721 and ERC1155 tokens into the `Multiwrap` contract, and receiving the wrapped NFT in exchange. + +This wrapped NFT can later be 'unwrapped' i.e. burned in exchange for the underlying tokens. ### Wrapping tokens -To wrap multiple ERC20, ERC721 or ERC1155 tokens as a single wrapped NFT, a token owner must first approve the relevant tokens to be transfered by the `Multiwrap` contract, and the token owner must then specify the tokens to wrapped into a single wrapped NFT. The following is the format in which each token to be wrapped must be specified: +To wrap multiple ERC20, ERC721 or ERC1155 tokens as a single wrapped NFT, a token owner must: +- approve the relevant tokens to be transferred by the `Multiwrap` contract. +- specify the tokens to be wrapped into a single wrapped NFT. The following is the format in which each token to be wrapped must be specified: ```solidity /// @notice The type of assets that can be wrapped. @@ -42,7 +60,7 @@ struct Token { address assetContract; TokenType tokenType; uint256 tokenId; - uint256 amount; + uint256 totalAmount; } ``` @@ -51,15 +69,15 @@ struct Token { | assetContract | address | The contract address of the asset to wrap. | | tokenType | TokenType | The token type (ERC20 / ERC721 / ERC1155) of the asset to wrap. | | tokenId | uint256 | The token Id of the asset to wrap, if the asset is an ERC721 / ERC1155 NFT. | -| amount | uint256 | The amount of the asset to wrap, if the asset is an ERC20 / ERC1155 fungible token. | +| totalAmount | uint256 | The amount of the asset to wrap, if the asset is an ERC20 / ERC1155 fungible token. | -Each token in the bundle of tokens to be wrapped as a single wrapped NFT must be specified to the `Multiwrap` contract in the form of the `Token` struct. The contract handles the respective token based on the value of `tokenType` provided. Any incorrect values passed (e.g. the `amount` specified to be wrapped exceeds the token owner's token balance) will cause the wrapping transaction to revert. +Each token in the bundle of tokens to be wrapped as a single wrapped NFT must be specified to the `Multiwrap` contract in the form of the `Token` struct. The contract handles the respective token based on the value of `tokenType` provided. Any incorrect values passed (e.g. the `totalAmount` specified to be wrapped exceeds the token owner's token balance) will cause the wrapping transaction to revert. Multiple tokens can be wrapped as a single wrapped NFT by calling the following function: ```solidity function wrap( - Token[] memory wrappedContents, + Token[] memory tokensToWrap, string calldata uriForWrappedToken, address recipient ) external payable returns (uint256 tokenId); @@ -67,15 +85,19 @@ function wrap( | Parameters | Type | Description | | --- | --- | --- | -| wrappedContents | Token[] | The tokens to wrap. | +| tokensToWrap | Token[] | The tokens to wrap. | | uriForWrappedToken | string | The metadata URI for the wrapped NFT. | | recipient | address | The recipient of the wrapped NFT. | -When wrapping multiple assets into a single wrapped NFT, the assets are escrowed in the `Multiwrap` contract until the wrapped NFT is unwrapped. - ### Unwrapping the wrapped NFT -The single wrapped NFT, received on wrapping multiple assets as explained in the previous section, can be unwrapped in exchange for the underlying assets. To unwrap a wrapped NFT, the wrapped NFT owner must specify the wrapped NFT's tokenId, and a recipient who shall receive the wrapped NFT's underlying assets. +The single wrapped NFT, received on wrapping multiple assets as explained in the previous section, can be unwrapped in exchange for the underlying assets. + +A wrapped NFT can be unwrapped either by the owner, or a wallet approved by the owner to transfer the NFT via `setApprovalForAll` or `approve` ERC721 functions. + +When unwrapping the wrapped NFT, the wrapped NFT is burned.**** + +A wrapped NFT can be unwrapped by calling the following function: ```solidity function unwrap( @@ -86,21 +108,31 @@ function unwrap( | Parameters | Type | Description | | --- | --- | --- | -| tokenId | Token[] | The token Id of the wrapped NFT to unwrap.. | -| recipient | address | The recipient of the underlying ERC1155, ERC721, ERC20 tokens of the wrapped NFT. | - -When unwrapping the single wrapped NFT, the wrapped NFT is burned. - -### EIPs supported / implemented - -The `Multiwrap` contract itself is an ERC721 contract i.e. it implements the [ERC721 standard](https://eips.ethereum.org/EIPS/eip-721). The contract also implements receiver interfaces for ERC721 and ERC1155 so it can receive, and thus, escrow ERC721 and ERC1155 tokens. - -The contract also implements the [ERC2981](https://eips.ethereum.org/EIPS/eip-2981) royalty standard. That means the single wrapped token received on bundling up multiple assets can generate royalties just like any other NFTs. - - -## Limitations - -Given the same interface for `wrap` and `unwrap`, the contract needs to be optimized for gas i.e. consume as much less gas as possible. +| tokenId | Token[] | The token Id of the wrapped NFT to unwrap. | +| recipient | address | The recipient of the underlying ERC20, ERC721 or ERC1155 tokens of the wrapped NFT. | + +## Permissions + +| Role name | Type (Switch / !Switch) | Purpose | +| -- | -- | -- | +| TRANSFER_ROLE | Switch | Only token transfers to or from role holders are allowed. | +| MINTER_ROLE | Switch | Only role holders can wrap tokens. | +| UNWRAP_ROLE | Switch | Only role holders can unwrap wrapped NFTs. | +| ASSET_ROLE | Switch | Only assets with the role can be wrapped. | + +What does **Type (Switch / !Switch)** mean? +- **Switch:** If `address(0)` has `ROLE`, then the `ROLE` restrictions don't apply. +- **!Switch:** `ROLE` restrictions always apply. + +## Relevant EIPs + +| EIP | Link | Relation to `Multiwrap` | +| -- | -- | -- | +| 721 | https://eips.ethereum.org/EIPS/eip-721 | Multiwrap itself is an ERC721 contract. The wrapped NFT received by a token owner on wrapping is an ERC721 NFT. Additionally, ERC721 tokens can be wrapped. | +| 20 | https://eips.ethereum.org/EIPS/eip-20 | ERC20 tokens can be wrapped. | +| 1155 | https://eips.ethereum.org/EIPS/eip-1155 | ERC1155 tokens can be wrapped. | +| 2981 | https://eips.ethereum.org/EIPS/eip-2981 | Multiwrap implements ERC 2981 for distributing royalties for sales of the wrapped NFTs. | +| 2771 | https://eips.ethereum.org/EIPS/eip-2771 | Multiwrap implements ERC 2771 to support meta-transactions (aka “gasless” transactions). | ## Authors - [nkrishang](https://github.com/nkrishang) diff --git a/contracts/package.json b/contracts/package.json index 0dccc8624..74fe7c829 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -1,7 +1,7 @@ { "name": "@thirdweb-dev/contracts", "description": "Collection of smart contracts deployable via the thirdweb SDK, dashboard and CLI", - "version": "2.3.7-0", + "version": "2.3.7", "license": "Apache-2.0", "repository": { "type": "git", @@ -12,7 +12,6 @@ }, "author": "thirdweb engineering ", "homepage": "https://thirdweb.com", - "dependencies": {}, "files": [ "**/*.sol", "/abi" diff --git a/docs/ByocFactory.md b/docs/ByocFactory.md deleted file mode 100644 index 94593efa8..000000000 --- a/docs/ByocFactory.md +++ /dev/null @@ -1,439 +0,0 @@ -# ByocFactory - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### deployInstance - -```solidity -function deployInstance(address _publisher, bytes _contractBytecode, bytes _constructorArgs, bytes32 _salt, uint256 _value, string publishMetadataUri) external nonpayable returns (address deployedAddress) -``` - -Deploys an instance of a published contract directly. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _publisher | address | undefined -| _contractBytecode | bytes | undefined -| _constructorArgs | bytes | undefined -| _salt | bytes32 | undefined -| _value | uint256 | undefined -| publishMetadataUri | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| deployedAddress | address | undefined - -### deployInstanceProxy - -```solidity -function deployInstanceProxy(address _publisher, address _implementation, bytes _initializeData, bytes32 _salt, uint256 _value, string publishMetadataUri) external nonpayable returns (address deployedAddress) -``` - -Deploys a clone pointing to an implementation of a published contract. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _publisher | address | undefined -| _implementation | address | undefined -| _initializeData | bytes | undefined -| _salt | bytes32 | undefined -| _value | uint256 | undefined -| publishMetadataUri | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| deployedAddress | address | undefined - -### getContractDeployer - -```solidity -function getContractDeployer(address) external view returns (address) -``` - - - -*contract address deployed through the factory => deployer* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isPaused - -```solidity -function isPaused() external view returns (bool) -``` - - - -*Whether the registry is paused.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### setPause - -```solidity -function setPause(bool _pause) external nonpayable -``` - - - -*Lets a contract admin pause the registry.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _pause | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - - - -## Events - -### ContractDeployed - -```solidity -event ContractDeployed(address indexed deployer, address indexed publisher, address deployedContract) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| deployer `indexed` | address | undefined | -| publisher `indexed` | address | undefined | -| deployedContract | address | undefined | - -### Paused - -```solidity -event Paused(bool isPaused) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| isPaused | bool | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/ByocRegistry.md b/docs/ByocRegistry.md deleted file mode 100644 index ac08a3c4b..000000000 --- a/docs/ByocRegistry.md +++ /dev/null @@ -1,669 +0,0 @@ -# ByocRegistry - - - - - - - - - -## Methods - -### DEFAULT_ADMIN_ROLE - -```solidity -function DEFAULT_ADMIN_ROLE() external view returns (bytes32) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### addToPublicList - -```solidity -function addToPublicList(address _publisher, string _contractId) external nonpayable -``` - -Lets an account add a published contract (and all its versions). The account must be approved by the publisher, or be the publisher. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _publisher | address | undefined -| _contractId | string | undefined - -### approveOperator - -```solidity -function approveOperator(address _operator, bool _toApprove) external nonpayable -``` - -Lets a publisher (caller) approve an operator to publish / unpublish contracts on their behalf. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _operator | address | undefined -| _toApprove | bool | undefined - -### getAllPublicPublishedContracts - -```solidity -function getAllPublicPublishedContracts() external view returns (struct IByocRegistry.CustomContractInstance[] published) -``` - -Returns the latest version of all contracts published by a publisher. - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| published | IByocRegistry.CustomContractInstance[] | undefined - -### getAllPublishedContracts - -```solidity -function getAllPublishedContracts(address _publisher) external view returns (struct IByocRegistry.CustomContractInstance[] published) -``` - -Returns the latest version of all contracts published by a publisher. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _publisher | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| published | IByocRegistry.CustomContractInstance[] | undefined - -### getPublicId - -```solidity -function getPublicId(address _publisher, string _contractId) external view returns (uint256 publicId) -``` - -Returns the public id of a published contract, if it is public. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _publisher | address | undefined -| _contractId | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| publicId | uint256 | undefined - -### getPublishedContract - -```solidity -function getPublishedContract(address _publisher, string _contractId) external view returns (struct IByocRegistry.CustomContractInstance published) -``` - -Returns the latest version of a contract published by a publisher. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _publisher | address | undefined -| _contractId | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| published | IByocRegistry.CustomContractInstance | undefined - -### getPublishedContractVersions - -```solidity -function getPublishedContractVersions(address _publisher, string _contractId) external view returns (struct IByocRegistry.CustomContractInstance[] published) -``` - -Returns all versions of a published contract. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _publisher | address | undefined -| _contractId | string | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| published | IByocRegistry.CustomContractInstance[] | undefined - -### getRoleAdmin - -```solidity -function getRoleAdmin(bytes32 role) external view returns (bytes32) -``` - - - -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### getRoleMember - -```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) -``` - - - -*Returns one of the accounts that have `role`. `index` must be a value between 0 and {getRoleMemberCount}, non-inclusive. Role bearers are not sorted in any particular way, and their ordering may change at any point. WARNING: When using {getRoleMember} and {getRoleMemberCount}, make sure you perform all queries on the same block. See the following https://forum.openzeppelin.com/t/iterating-over-elements-on-enumerableset-in-openzeppelin-contracts/2296[forum post] for more information.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| index | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined - -### getRoleMemberCount - -```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) -``` - - - -*Returns the number of accounts that have `role`. Can be used together with {getRoleMember} to enumerate all bearers of a role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### grantRole - -```solidity -function grantRole(bytes32 role, address account) external nonpayable -``` - - - -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### hasRole - -```solidity -function hasRole(bytes32 role, address account) external view returns (bool) -``` - - - -*Returns `true` if `account` has been granted `role`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isApprovedByPublisher - -```solidity -function isApprovedByPublisher(address, address) external view returns (bool) -``` - - - -*Mapping from publisher address => operator address => whether publisher has approved operator to publish / unpublish contracts on their behalf.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isPaused - -```solidity -function isPaused() external view returns (bool) -``` - - - -*Whether the registry is paused.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### isTrustedForwarder - -```solidity -function isTrustedForwarder(address forwarder) external view returns (bool) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| forwarder | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### multicall - -```solidity -function multicall(bytes[] data) external nonpayable returns (bytes[] results) -``` - - - -*Receives and executes a batch of function calls on this contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| data | bytes[] | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| results | bytes[] | undefined - -### nextPublicId - -```solidity -function nextPublicId() external view returns (uint256) -``` - - - -*The global Id for publicly published contracts.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -### publishContract - -```solidity -function publishContract(address _publisher, string _publishMetadataUri, bytes32 _bytecodeHash, address _implementation, string _contractId) external nonpayable -``` - -Let's an account publish a contract. The account must be approved by the publisher, or be the publisher. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _publisher | address | undefined -| _publishMetadataUri | string | undefined -| _bytecodeHash | bytes32 | undefined -| _implementation | address | undefined -| _contractId | string | undefined - -### removeFromPublicList - -```solidity -function removeFromPublicList(address _publisher, string _contractId) external nonpayable -``` - -Lets an account remove a published contract (and all its versions). The account must be approved by the publisher, or be the publisher. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _publisher | address | undefined -| _contractId | string | undefined - -### renounceRole - -```solidity -function renounceRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### revokeRole - -```solidity -function revokeRole(bytes32 role, address account) external nonpayable -``` - - - -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role | bytes32 | undefined -| account | address | undefined - -### setPause - -```solidity -function setPause(bool _pause) external nonpayable -``` - - - -*Lets a contract admin pause the registry.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _pause | bool | undefined - -### supportsInterface - -```solidity -function supportsInterface(bytes4 interfaceId) external view returns (bool) -``` - - - -*See {IERC165-supportsInterface}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| interfaceId | bytes4 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bool | undefined - -### unpublishContract - -```solidity -function unpublishContract(address _publisher, string _contractId) external nonpayable -``` - -Lets an account unpublish a contract and all its versions. The account must be approved by the publisher, or be the publisher. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _publisher | address | undefined -| _contractId | string | undefined - - - -## Events - -### AddedContractToPublicList - -```solidity -event AddedContractToPublicList(address indexed publisher, string indexed contractId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| publisher `indexed` | address | undefined | -| contractId `indexed` | string | undefined | - -### Approved - -```solidity -event Approved(address indexed publisher, address indexed operator, bool isApproved) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| publisher `indexed` | address | undefined | -| operator `indexed` | address | undefined | -| isApproved | bool | undefined | - -### ContractPublished - -```solidity -event ContractPublished(address indexed operator, address indexed publisher, IByocRegistry.CustomContractInstance publishedContract) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| publisher `indexed` | address | undefined | -| publishedContract | IByocRegistry.CustomContractInstance | undefined | - -### ContractUnpublished - -```solidity -event ContractUnpublished(address indexed operator, address indexed publisher, string indexed contractId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| operator `indexed` | address | undefined | -| publisher `indexed` | address | undefined | -| contractId `indexed` | string | undefined | - -### Paused - -```solidity -event Paused(bool isPaused) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| isPaused | bool | undefined | - -### RemovedContractToPublicList - -```solidity -event RemovedContractToPublicList(address indexed publisher, string indexed contractId) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| publisher `indexed` | address | undefined | -| contractId `indexed` | string | undefined | - -### RoleAdminChanged - -```solidity -event RoleAdminChanged(bytes32 indexed role, bytes32 indexed previousAdminRole, bytes32 indexed newAdminRole) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| previousAdminRole `indexed` | bytes32 | undefined | -| newAdminRole `indexed` | bytes32 | undefined | - -### RoleGranted - -```solidity -event RoleGranted(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - -### RoleRevoked - -```solidity -event RoleRevoked(bytes32 indexed role, address indexed account, address indexed sender) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| role `indexed` | bytes32 | undefined | -| account `indexed` | address | undefined | -| sender `indexed` | address | undefined | - - - diff --git a/docs/ContractMetadata.md b/docs/ContractMetadata.md index 3b856786b..fb1e1fdec 100644 --- a/docs/ContractMetadata.md +++ b/docs/ContractMetadata.md @@ -45,4 +45,24 @@ function setContractURI(string _uri) external nonpayable +## Events + +### ContractURIUpdated + +```solidity +event ContractURIUpdated(string prevURI, string newURI) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| prevURI | string | undefined | +| newURI | string | undefined | + + diff --git a/docs/ERC1155Holder.md b/docs/ERC1155Holder.md new file mode 100644 index 000000000..b33b6dc2d --- /dev/null +++ b/docs/ERC1155Holder.md @@ -0,0 +1,89 @@ +# ERC1155Holder + + + + + +Simple implementation of `ERC1155Receiver` that will allow a contract to hold ERC1155 tokens. IMPORTANT: When inheriting this contract, you must include a way to use the received tokens, otherwise they will be stuck. + +*_Available since v3.1._* + +## Methods + +### onERC1155BatchReceived + +```solidity +function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) external nonpayable returns (bytes4) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined +| _1 | address | undefined +| _2 | uint256[] | undefined +| _3 | uint256[] | undefined +| _4 | bytes | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bytes4 | undefined + +### onERC1155Received + +```solidity +function onERC1155Received(address, address, uint256, uint256, bytes) external nonpayable returns (bytes4) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined +| _1 | address | undefined +| _2 | uint256 | undefined +| _3 | uint256 | undefined +| _4 | bytes | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bytes4 | undefined + +### supportsInterface + +```solidity +function supportsInterface(bytes4 interfaceId) external view returns (bool) +``` + + + +*See {IERC165-supportsInterface}.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| interfaceId | bytes4 | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined + + + + diff --git a/docs/ERC1155Receiver.md b/docs/ERC1155Receiver.md new file mode 100644 index 000000000..b328787a6 --- /dev/null +++ b/docs/ERC1155Receiver.md @@ -0,0 +1,89 @@ +# ERC1155Receiver + + + + + + + +*_Available since v3.1._* + +## Methods + +### onERC1155BatchReceived + +```solidity +function onERC1155BatchReceived(address operator, address from, uint256[] ids, uint256[] values, bytes data) external nonpayable returns (bytes4) +``` + + + +*Handles the receipt of a multiple ERC1155 token types. This function is called at the end of a `safeBatchTransferFrom` after the balances have been updated. NOTE: To accept the transfer(s), this must return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` (i.e. 0xbc197c81, or its own function selector).* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| operator | address | The address which initiated the batch transfer (i.e. msg.sender) +| from | address | The address which previously owned the token +| ids | uint256[] | An array containing ids of each token being transferred (order and length must match values array) +| values | uint256[] | An array containing amounts of each token being transferred (order and length must match ids array) +| data | bytes | Additional data with no specified format + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bytes4 | `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed + +### onERC1155Received + +```solidity +function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes data) external nonpayable returns (bytes4) +``` + + + +*Handles the receipt of a single ERC1155 token type. This function is called at the end of a `safeTransferFrom` after the balance has been updated. NOTE: To accept the transfer, this must return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` (i.e. 0xf23a6e61, or its own function selector).* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| operator | address | The address which initiated the transfer (i.e. msg.sender) +| from | address | The address which previously owned the token +| id | uint256 | The ID of the token being transferred +| value | uint256 | The amount of tokens being transferred +| data | bytes | Additional data with no specified format + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bytes4 | `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed + +### supportsInterface + +```solidity +function supportsInterface(bytes4 interfaceId) external view returns (bool) +``` + + + +*See {IERC165-supportsInterface}.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| interfaceId | bytes4 | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined + + + + diff --git a/docs/ERC721Holder.md b/docs/ERC721Holder.md new file mode 100644 index 000000000..6727c7c6f --- /dev/null +++ b/docs/ERC721Holder.md @@ -0,0 +1,40 @@ +# ERC721Holder + + + + + + + +*Implementation of the {IERC721Receiver} interface. Accepts all token transfers. Make sure the contract is able to use its token with {IERC721-safeTransferFrom}, {IERC721-approve} or {IERC721-setApprovalForAll}.* + +## Methods + +### onERC721Received + +```solidity +function onERC721Received(address, address, uint256, bytes) external nonpayable returns (bytes4) +``` + + + +*See {IERC721Receiver-onERC721Received}. Always returns `IERC721Receiver.onERC721Received.selector`.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined +| _1 | address | undefined +| _2 | uint256 | undefined +| _3 | bytes | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bytes4 | undefined + + + + diff --git a/docs/IContractMetadata.md b/docs/IContractMetadata.md index 7bc9a4eee..720d81fcc 100644 --- a/docs/IContractMetadata.md +++ b/docs/IContractMetadata.md @@ -45,4 +45,24 @@ function setContractURI(string _uri) external nonpayable +## Events + +### ContractURIUpdated + +```solidity +event ContractURIUpdated(string prevURI, string newURI) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| prevURI | string | undefined | +| newURI | string | undefined | + + diff --git a/docs/IERC1155Receiver.md b/docs/IERC1155Receiver.md new file mode 100644 index 000000000..5701c5057 --- /dev/null +++ b/docs/IERC1155Receiver.md @@ -0,0 +1,89 @@ +# IERC1155Receiver + + + + + + + +*_Available since v3.1._* + +## Methods + +### onERC1155BatchReceived + +```solidity +function onERC1155BatchReceived(address operator, address from, uint256[] ids, uint256[] values, bytes data) external nonpayable returns (bytes4) +``` + + + +*Handles the receipt of a multiple ERC1155 token types. This function is called at the end of a `safeBatchTransferFrom` after the balances have been updated. NOTE: To accept the transfer(s), this must return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` (i.e. 0xbc197c81, or its own function selector).* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| operator | address | The address which initiated the batch transfer (i.e. msg.sender) +| from | address | The address which previously owned the token +| ids | uint256[] | An array containing ids of each token being transferred (order and length must match values array) +| values | uint256[] | An array containing amounts of each token being transferred (order and length must match ids array) +| data | bytes | Additional data with no specified format + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bytes4 | `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed + +### onERC1155Received + +```solidity +function onERC1155Received(address operator, address from, uint256 id, uint256 value, bytes data) external nonpayable returns (bytes4) +``` + + + +*Handles the receipt of a single ERC1155 token type. This function is called at the end of a `safeTransferFrom` after the balance has been updated. NOTE: To accept the transfer, this must return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` (i.e. 0xf23a6e61, or its own function selector).* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| operator | address | The address which initiated the transfer (i.e. msg.sender) +| from | address | The address which previously owned the token +| id | uint256 | The ID of the token being transferred +| value | uint256 | The amount of tokens being transferred +| data | bytes | Additional data with no specified format + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bytes4 | `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed + +### supportsInterface + +```solidity +function supportsInterface(bytes4 interfaceId) external view returns (bool) +``` + + + +*Returns true if this contract implements the interface defined by `interfaceId`. See the corresponding https://eips.ethereum.org/EIPS/eip-165#how-interfaces-are-identified[EIP section] to learn more about how these ids are created. This function call must use less than 30 000 gas.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| interfaceId | bytes4 | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined + + + + diff --git a/docs/IMultiwrap.md b/docs/IMultiwrap.md index e9e866147..e640e4d16 100644 --- a/docs/IMultiwrap.md +++ b/docs/IMultiwrap.md @@ -30,7 +30,7 @@ Unwrap a wrapped NFT to retrieve underlying ERC1155, ERC721, ERC20 tokens. ### wrap ```solidity -function wrap(IMultiwrap.Token[] wrappedContents, string uriForWrappedToken, address recipient) external payable returns (uint256 tokenId) +function wrap(ITokenBundle.Token[] wrappedContents, string uriForWrappedToken, address recipient) external payable returns (uint256 tokenId) ``` Wrap multiple ERC1155, ERC721, ERC20 tokens into a single wrapped NFT. @@ -41,7 +41,7 @@ Wrap multiple ERC1155, ERC721, ERC20 tokens into a single wrapped NFT. | Name | Type | Description | |---|---|---| -| wrappedContents | IMultiwrap.Token[] | The tokens to wrap. +| wrappedContents | ITokenBundle.Token[] | The tokens to wrap. | uriForWrappedToken | string | The metadata URI for the wrapped NFT. | recipient | address | The recipient of the wrapped NFT. @@ -58,7 +58,7 @@ Wrap multiple ERC1155, ERC721, ERC20 tokens into a single wrapped NFT. ### TokensUnwrapped ```solidity -event TokensUnwrapped(address indexed unwrapper, address indexed recipientOfWrappedContents, uint256 indexed tokenIdOfWrappedToken, IMultiwrap.Token[] wrappedContents) +event TokensUnwrapped(address indexed unwrapper, address indexed recipientOfWrappedContents, uint256 indexed tokenIdOfWrappedToken) ``` @@ -72,12 +72,11 @@ event TokensUnwrapped(address indexed unwrapper, address indexed recipientOfWrap | unwrapper `indexed` | address | undefined | | recipientOfWrappedContents `indexed` | address | undefined | | tokenIdOfWrappedToken `indexed` | uint256 | undefined | -| wrappedContents | IMultiwrap.Token[] | undefined | ### TokensWrapped ```solidity -event TokensWrapped(address indexed wrapper, address indexed recipientOfWrappedToken, uint256 indexed tokenIdOfWrappedToken, IMultiwrap.Token[] wrappedContents) +event TokensWrapped(address indexed wrapper, address indexed recipientOfWrappedToken, uint256 indexed tokenIdOfWrappedToken, ITokenBundle.Token[] wrappedContents) ``` @@ -91,7 +90,7 @@ event TokensWrapped(address indexed wrapper, address indexed recipientOfWrappedT | wrapper `indexed` | address | undefined | | recipientOfWrappedToken `indexed` | address | undefined | | tokenIdOfWrappedToken `indexed` | uint256 | undefined | -| wrappedContents | IMultiwrap.Token[] | undefined | +| wrappedContents | ITokenBundle.Token[] | undefined | diff --git a/docs/Multiwrap.md b/docs/Multiwrap.md index 7758a46d1..332f7930c 100644 --- a/docs/Multiwrap.md +++ b/docs/Multiwrap.md @@ -91,7 +91,7 @@ function contractURI() external view returns (string) -*Contract level metadata.* + #### Returns @@ -147,7 +147,7 @@ function getDefaultRoyaltyInfo() external view returns (address, uint16) -*Returns the platform fee bps and recipient.* +*Returns the default royalty recipient and bps.* #### Returns @@ -165,7 +165,7 @@ function getRoleAdmin(bytes32 role) external view returns (bytes32) -*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {_setRoleAdmin}.* +*Returns the admin role that controls `role`. See {grantRole} and {revokeRole}. To change a role's admin, use {AccessControl-_setRoleAdmin}.* #### Parameters @@ -182,7 +182,7 @@ function getRoleAdmin(bytes32 role) external view returns (bytes32) ### getRoleMember ```solidity -function getRoleMember(bytes32 role, uint256 index) external view returns (address) +function getRoleMember(bytes32 role, uint256 index) external view returns (address member) ``` @@ -200,12 +200,12 @@ function getRoleMember(bytes32 role, uint256 index) external view returns (addre | Name | Type | Description | |---|---|---| -| _0 | address | undefined +| member | address | undefined ### getRoleMemberCount ```solidity -function getRoleMemberCount(bytes32 role) external view returns (uint256) +function getRoleMemberCount(bytes32 role) external view returns (uint256 count) ``` @@ -222,7 +222,7 @@ function getRoleMemberCount(bytes32 role) external view returns (uint256) | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined +| count | uint256 | undefined ### getRoyaltyInfoForToken @@ -232,7 +232,7 @@ function getRoyaltyInfoForToken(uint256 _tokenId) external view returns (address -*Returns the royalty recipient for a particular token Id.* +*Returns the royalty recipient and bps for a particular token Id.* #### Parameters @@ -247,15 +247,82 @@ function getRoyaltyInfoForToken(uint256 _tokenId) external view returns (address | _0 | address | undefined | _1 | uint16 | undefined +### getTokenCountOfBundle + +```solidity +function getTokenCountOfBundle(uint256 _bundleId) external view returns (uint256) +``` + + + +*Returns the total number of assets in a particular bundle.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _bundleId | uint256 | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined + +### getTokenOfBundle + +```solidity +function getTokenOfBundle(uint256 _bundleId, uint256 index) external view returns (struct ITokenBundle.Token) +``` + + + +*Returns an asset contained in a particular bundle, at a particular index.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _bundleId | uint256 | undefined +| index | uint256 | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | ITokenBundle.Token | undefined + +### getUriOfBundle + +```solidity +function getUriOfBundle(uint256 _bundleId) external view returns (string) +``` + + + +*Returns the uri of a particular bundle.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _bundleId | uint256 | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | string | undefined + ### getWrappedContents ```solidity -function getWrappedContents(uint256 _tokenId) external view returns (struct IMultiwrap.Token[] contents) +function getWrappedContents(uint256 _tokenId) external view returns (struct ITokenBundle.Token[] contents) ``` -*Returns the underlygin contents of a wrapped NFT.* +*Returns the underlying contents of a wrapped NFT.* #### Parameters @@ -267,7 +334,7 @@ function getWrappedContents(uint256 _tokenId) external view returns (struct IMul | Name | Type | Description | |---|---|---| -| contents | IMultiwrap.Token[] | undefined +| contents | ITokenBundle.Token[] | undefined ### grantRole @@ -277,7 +344,7 @@ function grantRole(bytes32 role, address account) external nonpayable -*Grants `role` to `account`. If `account` had not been already granted `role`, emits a {RoleGranted} event. Requirements: - the caller must have ``role``'s admin role.* + #### Parameters @@ -296,6 +363,29 @@ function hasRole(bytes32 role, address account) external view returns (bool) *Returns `true` if `account` has been granted `role`.* +#### Parameters + +| Name | Type | Description | +|---|---|---| +| role | bytes32 | undefined +| account | address | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined + +### hasRoleWithSwitch + +```solidity +function hasRoleWithSwitch(bytes32 role, address account) external view returns (bool) +``` + + + + + #### Parameters | Name | Type | Description | @@ -517,7 +607,7 @@ function owner() external view returns (address) -*Returns the address of the current owner.* + #### Returns @@ -556,7 +646,7 @@ function renounceRole(bytes32 role, address account) external nonpayable -*Revokes `role` from the calling account. Roles are often managed via {grantRole} and {revokeRole}: this function's purpose is to provide a mechanism for accounts to lose their privileges if they are compromised (such as when a trusted device is misplaced). If the calling account had been revoked `role`, emits a {RoleRevoked} event. Requirements: - the caller must be `account`.* + #### Parameters @@ -573,7 +663,7 @@ function revokeRole(bytes32 role, address account) external nonpayable -*Revokes `role` from `account`. If `account` had been granted `role`, emits a {RoleRevoked} event. Requirements: - the caller must have ``role``'s admin role.* + #### Parameters @@ -650,7 +740,7 @@ function setContractURI(string _uri) external nonpayable -*Lets a module admin set the URI for contract-level metadata.* +*Lets a contract admin set the URI for contract-level metadata.* #### Parameters @@ -666,7 +756,7 @@ function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) e -*Lets a module admin update the royalty bps and recipient.* +*Lets a contract admin update the default royalty recipient and bps.* #### Parameters @@ -683,7 +773,7 @@ function setOwner(address _newOwner) external nonpayable -*Lets a module admin set a new owner for the contract. The new owner must be a module admin.* +*Lets a contract admin set a new owner for the contract. The new owner must be a contract admin.* #### Parameters @@ -699,7 +789,7 @@ function setRoyaltyInfoForToken(uint256 _tokenId, address _recipient, uint256 _b -*Lets a module admin set the royalty recipient for a particular token Id.* +*Lets a contract admin set the royalty recipient and bps for a particular token Id.* #### Parameters @@ -808,7 +898,7 @@ function unwrap(uint256 _tokenId, address _recipient) external nonpayable ### wrap ```solidity -function wrap(IMultiwrap.Token[] _wrappedContents, string _uriForWrappedToken, address _recipient) external payable returns (uint256 tokenId) +function wrap(ITokenBundle.Token[] _tokensToWrap, string _uriForWrappedToken, address _recipient) external payable returns (uint256 tokenId) ``` @@ -819,7 +909,7 @@ function wrap(IMultiwrap.Token[] _wrappedContents, string _uriForWrappedToken, a | Name | Type | Description | |---|---|---| -| _wrappedContents | IMultiwrap.Token[] | undefined +| _tokensToWrap | ITokenBundle.Token[] | undefined | _uriForWrappedToken | string | undefined | _recipient | address | undefined @@ -869,6 +959,23 @@ event ApprovalForAll(address indexed owner, address indexed operator, bool appro | operator `indexed` | address | undefined | | approved | bool | undefined | +### ContractURIUpdated + +```solidity +event ContractURIUpdated(string prevURI, string newURI) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| prevURI | string | undefined | +| newURI | string | undefined | + ### DefaultRoyalty ```solidity @@ -978,7 +1085,7 @@ event RoyaltyForToken(uint256 indexed tokenId, address royaltyRecipient, uint256 ### TokensUnwrapped ```solidity -event TokensUnwrapped(address indexed unwrapper, address indexed recipientOfWrappedContents, uint256 indexed tokenIdOfWrappedToken, IMultiwrap.Token[] wrappedContents) +event TokensUnwrapped(address indexed unwrapper, address indexed recipientOfWrappedContents, uint256 indexed tokenIdOfWrappedToken) ``` @@ -992,12 +1099,11 @@ event TokensUnwrapped(address indexed unwrapper, address indexed recipientOfWrap | unwrapper `indexed` | address | undefined | | recipientOfWrappedContents `indexed` | address | undefined | | tokenIdOfWrappedToken `indexed` | uint256 | undefined | -| wrappedContents | IMultiwrap.Token[] | undefined | ### TokensWrapped ```solidity -event TokensWrapped(address indexed wrapper, address indexed recipientOfWrappedToken, uint256 indexed tokenIdOfWrappedToken, IMultiwrap.Token[] wrappedContents) +event TokensWrapped(address indexed wrapper, address indexed recipientOfWrappedToken, uint256 indexed tokenIdOfWrappedToken, ITokenBundle.Token[] wrappedContents) ``` @@ -1011,7 +1117,7 @@ event TokensWrapped(address indexed wrapper, address indexed recipientOfWrappedT | wrapper `indexed` | address | undefined | | recipientOfWrappedToken `indexed` | address | undefined | | tokenIdOfWrappedToken `indexed` | uint256 | undefined | -| wrappedContents | IMultiwrap.Token[] | undefined | +| wrappedContents | ITokenBundle.Token[] | undefined | ### Transfer diff --git a/docs/Permissions.md b/docs/Permissions.md index f26d26128..56b39fc63 100644 --- a/docs/Permissions.md +++ b/docs/Permissions.md @@ -76,6 +76,29 @@ function hasRole(bytes32 role, address account) external view returns (bool) *Returns `true` if `account` has been granted `role`.* +#### Parameters + +| Name | Type | Description | +|---|---|---| +| role | bytes32 | undefined +| account | address | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined + +### hasRoleWithSwitch + +```solidity +function hasRoleWithSwitch(bytes32 role, address account) external view returns (bool) +``` + + + + + #### Parameters | Name | Type | Description | diff --git a/docs/PermissionsEnumerable.md b/docs/PermissionsEnumerable.md index 093764ae6..0f46e6af5 100644 --- a/docs/PermissionsEnumerable.md +++ b/docs/PermissionsEnumerable.md @@ -121,6 +121,29 @@ function hasRole(bytes32 role, address account) external view returns (bool) *Returns `true` if `account` has been granted `role`.* +#### Parameters + +| Name | Type | Description | +|---|---|---| +| role | bytes32 | undefined +| account | address | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined + +### hasRoleWithSwitch + +```solidity +function hasRoleWithSwitch(bytes32 role, address account) external view returns (bool) +``` + + + + + #### Parameters | Name | Type | Description | diff --git a/docs/SignatureDrop.md b/docs/SignatureDrop.md index 2a5405029..d5fab9b97 100644 --- a/docs/SignatureDrop.md +++ b/docs/SignatureDrop.md @@ -416,6 +416,29 @@ function hasRole(bytes32 role, address account) external view returns (bool) *Returns `true` if `account` has been granted `role`.* +#### Parameters + +| Name | Type | Description | +|---|---|---| +| role | bytes32 | undefined +| account | address | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined + +### hasRoleWithSwitch + +```solidity +function hasRoleWithSwitch(bytes32 role, address account) external view returns (bool) +``` + + + + + #### Parameters | Name | Type | Description | @@ -1103,6 +1126,23 @@ event ClaimConditionUpdated(IClaimCondition.ClaimCondition condition, bool reset | condition | IClaimCondition.ClaimCondition | undefined | | resetEligibility | bool | undefined | +### ContractURIUpdated + +```solidity +event ContractURIUpdated(string prevURI, string newURI) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| prevURI | string | undefined | +| newURI | string | undefined | + ### DefaultRoyalty ```solidity diff --git a/docs/TokenStore.md b/docs/TokenStore.md new file mode 100644 index 000000000..c0b95d538 --- /dev/null +++ b/docs/TokenStore.md @@ -0,0 +1,181 @@ +# TokenStore + + + + + + + + + +## Methods + +### getTokenCountOfBundle + +```solidity +function getTokenCountOfBundle(uint256 _bundleId) external view returns (uint256) +``` + + + +*Returns the total number of assets in a particular bundle.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _bundleId | uint256 | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | uint256 | undefined + +### getTokenOfBundle + +```solidity +function getTokenOfBundle(uint256 _bundleId, uint256 index) external view returns (struct ITokenBundle.Token) +``` + + + +*Returns an asset contained in a particular bundle, at a particular index.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _bundleId | uint256 | undefined +| index | uint256 | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | ITokenBundle.Token | undefined + +### getUriOfBundle + +```solidity +function getUriOfBundle(uint256 _bundleId) external view returns (string) +``` + + + +*Returns the uri of a particular bundle.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _bundleId | uint256 | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | string | undefined + +### onERC1155BatchReceived + +```solidity +function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) external nonpayable returns (bytes4) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined +| _1 | address | undefined +| _2 | uint256[] | undefined +| _3 | uint256[] | undefined +| _4 | bytes | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bytes4 | undefined + +### onERC1155Received + +```solidity +function onERC1155Received(address, address, uint256, uint256, bytes) external nonpayable returns (bytes4) +``` + + + + + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined +| _1 | address | undefined +| _2 | uint256 | undefined +| _3 | uint256 | undefined +| _4 | bytes | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bytes4 | undefined + +### onERC721Received + +```solidity +function onERC721Received(address, address, uint256, bytes) external nonpayable returns (bytes4) +``` + + + +*See {IERC721Receiver-onERC721Received}. Always returns `IERC721Receiver.onERC721Received.selector`.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined +| _1 | address | undefined +| _2 | uint256 | undefined +| _3 | bytes | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bytes4 | undefined + +### supportsInterface + +```solidity +function supportsInterface(bytes4 interfaceId) external view returns (bool) +``` + + + +*See {IERC165-supportsInterface}.* + +#### Parameters + +| Name | Type | Description | +|---|---|---| +| interfaceId | bytes4 | undefined + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | bool | undefined + + + + diff --git a/foundry.toml b/foundry.toml index 4cf4fdec2..86a143372 100644 --- a/foundry.toml +++ b/foundry.toml @@ -4,6 +4,7 @@ solc-version = "0.8.12" cache = true evm_version = 'london' force = false +gas_reports = ["Multiwrap"] libraries = [] libs = ['lib'] optimizer = true diff --git a/src/test/Multiwrap.t.sol b/src/test/Multiwrap.t.sol index fe9c76018..cd55627e6 100644 --- a/src/test/Multiwrap.t.sol +++ b/src/test/Multiwrap.t.sol @@ -1,275 +1,758 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.0; +import { Multiwrap } from "contracts/multiwrap/Multiwrap.sol"; +import { ITokenBundle } from "contracts/feature/interface/ITokenBundle.sol"; + +// Test imports +import { MockERC20 } from "./mocks/MockERC20.sol"; +import { Wallet } from "./utils/Wallet.sol"; import "./utils/BaseTest.sol"; -import "./utils/Wallet.sol"; -import "./mocks/MockERC20.sol"; -import "./mocks/MockERC721.sol"; -import "./mocks/MockERC1155.sol"; -import "contracts/multiwrap/Multiwrap.sol"; -import "contracts/interfaces/IMultiwrap.sol"; +contract MultiwrapReentrant is MockERC20, ITokenBundle { + Multiwrap internal multiwrap; + uint256 internal tokenIdOfWrapped = 0; + + constructor(address _multiwrap) { + multiwrap = Multiwrap(_multiwrap); + } + + function transferFrom( + address from, + address to, + uint256 amount + ) public override returns (bool) { + multiwrap.unwrap(0, address(this)); + return super.transferFrom(from, to, amount); + } +} -interface IMultiwrapData { +contract MultiwrapTest is BaseTest { /// @dev Emitted when tokens are wrapped. event TokensWrapped( address indexed wrapper, address indexed recipientOfWrappedToken, uint256 indexed tokenIdOfWrappedToken, - IMultiwrap.Token[] wrappedContents + ITokenBundle.Token[] wrappedContents ); /// @dev Emitted when tokens are unwrapped. event TokensUnwrapped( address indexed unwrapper, address indexed recipientOfWrappedContents, - uint256 indexed tokenIdOfWrappedToken, - IMultiwrap.Token[] wrappedContents + uint256 indexed tokenIdOfWrappedToken ); -} -contract MultiwrapTest is BaseTest, IMultiwrapData { - // Target contract + /*/////////////////////////////////////////////////////////////// + Setup + //////////////////////////////////////////////////////////////*/ + Multiwrap internal multiwrap; - // Actors Wallet internal tokenOwner; - Wallet internal wrappedTokenRecipient; - - // Test parameters - string internal uriForWrappedToken = "ipfs://wrappedNFT"; - IMultiwrap.Token[] internal wrappedContents; - - uint256 internal erc721TokenId = 0; - uint256 internal erc1155TokenId = 0; - uint256 internal erc1155Amount = 50; - uint256 internal erc20Amount = 100 ether; + string internal uriForWrappedToken; + ITokenBundle.Token[] internal wrappedContent; - // ===== Set up ===== function setUp() public override { super.setUp(); - // Get Multiwrap contract. + // Get target contract multiwrap = Multiwrap(getContract("Multiwrap")); - // Get test actors. - tokenOwner = new Wallet(); - wrappedTokenRecipient = new Wallet(); + // Set test vars + tokenOwner = getWallet(); + uriForWrappedToken = "ipfs://baseURI/"; - // Grant MINTER_ROLE to `tokenOwner` - vm.prank(deployer); - multiwrap.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); - - // Mint mock ERC20/721/1155 tokens to `tokenOwner` - erc20.mint(address(tokenOwner), erc20Amount); - erc721.mint(address(tokenOwner), 1); - erc1155.mint(address(tokenOwner), erc1155TokenId, erc1155Amount); - - // Allow Multiwrap to transfer tokens. - tokenOwner.setAllowanceERC20(address(erc20), address(multiwrap), erc20Amount); - tokenOwner.setApprovalForAllERC721(address(erc721), address(multiwrap), true); - tokenOwner.setApprovalForAllERC1155(address(erc1155), address(multiwrap), true); - - // Prepare wrapped contents. - wrappedContents.push( - IMultiwrap.Token({ + wrappedContent.push( + ITokenBundle.Token({ assetContract: address(erc20), - tokenType: IMultiwrap.TokenType.ERC20, + tokenType: ITokenBundle.TokenType.ERC20, tokenId: 0, - amount: erc20Amount + totalAmount: 10 ether }) ); - wrappedContents.push( - IMultiwrap.Token({ + wrappedContent.push( + ITokenBundle.Token({ assetContract: address(erc721), - tokenType: IMultiwrap.TokenType.ERC721, - tokenId: erc721TokenId, - amount: 1 + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 0, + totalAmount: 1 }) ); - wrappedContents.push( - IMultiwrap.Token({ + wrappedContent.push( + ITokenBundle.Token({ assetContract: address(erc1155), - tokenType: IMultiwrap.TokenType.ERC1155, - tokenId: erc1155TokenId, - amount: erc1155Amount + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 0, + totalAmount: 100 }) ); + + // Mint tokens-to-wrap to `tokenOwner` + erc20.mint(address(tokenOwner), 10 ether); + erc721.mint(address(tokenOwner), 1); + erc1155.mint(address(tokenOwner), 0, 100); + + // Token owner approves `Multiwrap` to transfer tokens. + tokenOwner.setAllowanceERC20(address(erc20), address(multiwrap), type(uint256).max); + tokenOwner.setApprovalForAllERC721(address(erc721), address(multiwrap), true); + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(multiwrap), true); + + // Grant MINTER_ROLE / requisite wrapping permissions to `tokenOwer` + vm.prank(deployer); + multiwrap.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); } - // ===== Initial state ===== + /** + * Unit tests for relevant functions: + * - `wrap` + * - `unwrap` + */ - function testInitialState() public { - (address recipient, uint256 bps) = multiwrap.getDefaultRoyaltyInfo(); - assertTrue(recipient == royaltyRecipient && bps == royaltyBps); + /*/////////////////////////////////////////////////////////////// + Unit tests: `wrap` + //////////////////////////////////////////////////////////////*/ - assertEq(multiwrap.contractURI(), CONTRACT_URI); - assertEq(multiwrap.name(), NAME); - assertEq(multiwrap.symbol(), SYMBOL); - assertEq(multiwrap.nextTokenIdToMint(), 0); + /** + * note: Testing state changes; token owner calls `wrap` to wrap owned tokens. + */ + function test_state_wrap() public { + uint256 expectedIdForWrappedToken = multiwrap.nextTokenIdToMint(); + address recipient = address(0x123); - assertEq(multiwrap.owner(), deployer); - assertTrue(multiwrap.hasRole(multiwrap.DEFAULT_ADMIN_ROLE(), deployer)); - assertTrue(multiwrap.hasRole(keccak256("MINTER_ROLE"), deployer)); - assertTrue(multiwrap.hasRole(keccak256("TRANSFER_ROLE"), deployer)); + vm.prank(address(tokenOwner)); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); + + assertEq(expectedIdForWrappedToken + 1, multiwrap.nextTokenIdToMint()); + + ITokenBundle.Token[] memory contentsOfWrappedToken = multiwrap.getWrappedContents(expectedIdForWrappedToken); + assertEq(contentsOfWrappedToken.length, wrappedContent.length); + for (uint256 i = 0; i < contentsOfWrappedToken.length; i += 1) { + assertEq(contentsOfWrappedToken[i].assetContract, wrappedContent[i].assetContract); + assertEq(uint256(contentsOfWrappedToken[i].tokenType), uint256(wrappedContent[i].tokenType)); + assertEq(contentsOfWrappedToken[i].tokenId, wrappedContent[i].tokenId); + assertEq(contentsOfWrappedToken[i].totalAmount, wrappedContent[i].totalAmount); + } + + assertEq(uriForWrappedToken, multiwrap.tokenURI(expectedIdForWrappedToken)); } - // ===== Functionality tests ===== + /* + * note: Testing state changes; token owner calls `wrap` to wrap native tokens. + */ + function test_state_wrap_nativeTokens() public { + uint256 expectedIdForWrappedToken = multiwrap.nextTokenIdToMint(); + address recipient = address(0x123); - /// @dev Test `wrap` - function test_wrap() public { - assertEq(erc20.balanceOf(address(tokenOwner)), erc20Amount); - assertEq(erc721.ownerOf(erc721TokenId), address(tokenOwner)); - assertEq(erc1155.balanceOf(address(tokenOwner), erc1155TokenId), erc1155Amount); + ITokenBundle.Token[] memory nativeTokenContentToWrap = new ITokenBundle.Token[](1); - assertEq(erc20.balanceOf(address(multiwrap)), 0); - assertEq(erc1155.balanceOf(address(multiwrap), erc1155TokenId), 0); + vm.deal(address(tokenOwner), 100 ether); + nativeTokenContentToWrap[0] = ITokenBundle.Token({ + assetContract: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 10 ether + }); + + vm.prank(address(tokenOwner)); + multiwrap.wrap{ value: 10 ether }(nativeTokenContentToWrap, uriForWrappedToken, recipient); + + assertEq(expectedIdForWrappedToken + 1, multiwrap.nextTokenIdToMint()); + + ITokenBundle.Token[] memory contentsOfWrappedToken = multiwrap.getWrappedContents(expectedIdForWrappedToken); + assertEq(contentsOfWrappedToken.length, nativeTokenContentToWrap.length); + for (uint256 i = 0; i < contentsOfWrappedToken.length; i += 1) { + assertEq(contentsOfWrappedToken[i].assetContract, nativeTokenContentToWrap[i].assetContract); + assertEq(uint256(contentsOfWrappedToken[i].tokenType), uint256(nativeTokenContentToWrap[i].tokenType)); + assertEq(contentsOfWrappedToken[i].tokenId, nativeTokenContentToWrap[i].tokenId); + assertEq(contentsOfWrappedToken[i].totalAmount, nativeTokenContentToWrap[i].totalAmount); + } + + assertEq(uriForWrappedToken, multiwrap.tokenURI(expectedIdForWrappedToken)); + } + + /** + * note: Testing state changes; token owner calls `wrap` to wrap owned tokens. + * Only assets with ASSET_ROLE can be wrapped. + */ + function test_state_wrap_withAssetRoleRestriction() public { + // ===== setup ===== + + vm.startPrank(deployer); + multiwrap.revokeRole(keccak256("ASSET_ROLE"), address(0)); + + for (uint256 i = 0; i < wrappedContent.length; i += 1) { + multiwrap.grantRole(keccak256("ASSET_ROLE"), wrappedContent[i].assetContract); + } - uint256 tokenIdOfWrapped = multiwrap.nextTokenIdToMint(); + vm.stopPrank(); + + // ===== target test content ===== + uint256 expectedIdForWrappedToken = multiwrap.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); + + assertEq(expectedIdForWrappedToken + 1, multiwrap.nextTokenIdToMint()); + + ITokenBundle.Token[] memory contentsOfWrappedToken = multiwrap.getWrappedContents(expectedIdForWrappedToken); + assertEq(contentsOfWrappedToken.length, wrappedContent.length); + for (uint256 i = 0; i < contentsOfWrappedToken.length; i += 1) { + assertEq(contentsOfWrappedToken[i].assetContract, wrappedContent[i].assetContract); + assertEq(uint256(contentsOfWrappedToken[i].tokenType), uint256(wrappedContent[i].tokenType)); + assertEq(contentsOfWrappedToken[i].tokenId, wrappedContent[i].tokenId); + assertEq(contentsOfWrappedToken[i].totalAmount, wrappedContent[i].totalAmount); + } + + assertEq(uriForWrappedToken, multiwrap.tokenURI(expectedIdForWrappedToken)); + } + + /** + * note: Testing event emission; token owner calls `wrap` to wrap owned tokens. + */ + function test_event_wrap_TokensWrapped() public { + uint256 expectedIdForWrappedToken = multiwrap.nextTokenIdToMint(); + address recipient = address(0x123); vm.prank(address(tokenOwner)); - multiwrap.wrap(wrappedContents, uriForWrappedToken, address(wrappedTokenRecipient)); - assertEq(multiwrap.tokenURI(tokenIdOfWrapped), uriForWrappedToken); - assertEq(multiwrap.ownerOf(tokenIdOfWrapped), address(wrappedTokenRecipient)); + vm.expectEmit(true, true, true, true); + emit TokensWrapped(address(tokenOwner), recipient, expectedIdForWrappedToken, wrappedContent); - assertEq(erc20.balanceOf(address(multiwrap)), erc20Amount); - assertEq(erc721.ownerOf(erc721TokenId), address(multiwrap)); - assertEq(erc1155.balanceOf(address(multiwrap), erc1155TokenId), erc1155Amount); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); + } + /** + * note: Testing token balances; token owner calls `wrap` to wrap owned tokens. + */ + function test_balances_wrap() public { + // ERC20 balance + assertEq(erc20.balanceOf(address(tokenOwner)), 10 ether); + assertEq(erc20.balanceOf(address(multiwrap)), 0); + + // ERC721 balance + assertEq(erc721.ownerOf(0), address(tokenOwner)); + + // ERC1155 balance + assertEq(erc1155.balanceOf(address(tokenOwner), 0), 100); + assertEq(erc1155.balanceOf(address(multiwrap), 0), 0); + + uint256 expectedIdForWrappedToken = multiwrap.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); + + // ERC20 balance assertEq(erc20.balanceOf(address(tokenOwner)), 0); - assertEq(erc1155.balanceOf(address(tokenOwner), erc1155TokenId), 0); + assertEq(erc20.balanceOf(address(multiwrap)), 10 ether); + + // ERC721 balance + assertEq(erc721.ownerOf(0), address(multiwrap)); + + // ERC1155 balance + assertEq(erc1155.balanceOf(address(tokenOwner), 0), 0); + assertEq(erc1155.balanceOf(address(multiwrap), 0), 100); + + // Multiwrap wrapped token balance + assertEq(multiwrap.ownerOf(expectedIdForWrappedToken), recipient); } - function test_wrap_revert_insufficientBalance1155() public { - tokenOwner.burnERC1155(address(erc1155), erc1155TokenId, erc1155Amount); + /** + * note: Testing revert condition; token owner calls `wrap` to wrap owned tokens. + */ + function test_revert_wrap_reentrancy() public { + MultiwrapReentrant reentrant = new MultiwrapReentrant(address(multiwrap)); + ITokenBundle.Token[] memory reentrantContentToWrap = new ITokenBundle.Token[](1); - vm.expectRevert("ERC1155: insufficient balance for transfer"); + reentrant.mint(address(tokenOwner), 10 ether); + reentrantContentToWrap[0] = ITokenBundle.Token({ + assetContract: address(reentrant), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 10 ether + }); + + tokenOwner.setAllowanceERC20(address(reentrant), address(multiwrap), 10 ether); + + address recipient = address(0x123); vm.prank(address(tokenOwner)); - multiwrap.wrap(wrappedContents, uriForWrappedToken, address(wrappedTokenRecipient)); + vm.expectRevert("ReentrancyGuard: reentrant call"); + multiwrap.wrap(reentrantContentToWrap, uriForWrappedToken, recipient); } - function test_wrap_revert_insufficientBalance721() public { - tokenOwner.burnERC721(address(erc721), erc721TokenId); + /** + * note: Testing revert condition; token owner calls `wrap` to wrap owned tokens. + * Only assets with ASSET_ROLE can be wrapped, but assets being wrapped don't have that role. + */ + function test_revert_wrap_access_ASSET_ROLE() public { + vm.prank(deployer); + multiwrap.revokeRole(keccak256("ASSET_ROLE"), address(0)); + + address recipient = address(0x123); - vm.expectRevert("ERC721: operator query for nonexistent token"); + string memory errorMsg = string( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(wrappedContent[0].assetContract), 20), + " is missing role ", + Strings.toHexString(uint256(keccak256("ASSET_ROLE")), 32) + ) + ); vm.prank(address(tokenOwner)); - multiwrap.wrap(wrappedContents, uriForWrappedToken, address(wrappedTokenRecipient)); + vm.expectRevert(bytes(errorMsg)); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); } - function test_wrap_revert_insufficientBalance20() public { - tokenOwner.burnERC20(address(erc20), 1); + /** + * note: Testing revert condition; token owner calls `wrap` to wrap owned tokens, without MINTER_ROLE. + */ + function test_revert_wrap_access_MINTER_ROLE() public { + vm.prank(address(tokenOwner)); + multiwrap.renounceRole(keccak256("MINTER_ROLE"), address(tokenOwner)); - vm.expectRevert("ERC20: transfer amount exceeds balance"); + address recipient = address(0x123); + + string memory errorMsg = string( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(address(tokenOwner)), 20), + " is missing role ", + Strings.toHexString(uint256(keccak256("MINTER_ROLE")), 32) + ) + ); vm.prank(address(tokenOwner)); - multiwrap.wrap(wrappedContents, uriForWrappedToken, address(wrappedTokenRecipient)); + vm.expectRevert(bytes(errorMsg)); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); } - function test_wrap_revert_unapprovedTransfer1155() public { - tokenOwner.setApprovalForAllERC1155(address(erc1155), address(multiwrap), false); + /** + * note: Testing revert condition; token owner calls `wrap` with insufficient value when wrapping native tokens. + */ + function test_revert_wrap_nativeTokens_insufficientValue() public { + address recipient = address(0x123); - vm.expectRevert("ERC1155: caller is not owner nor approved"); + ITokenBundle.Token[] memory nativeTokenContentToWrap = new ITokenBundle.Token[](1); + + vm.deal(address(tokenOwner), 100 ether); + nativeTokenContentToWrap[0] = ITokenBundle.Token({ + assetContract: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 10 ether + }); vm.prank(address(tokenOwner)); - multiwrap.wrap(wrappedContents, uriForWrappedToken, address(wrappedTokenRecipient)); + vm.expectRevert("msg.value != amount"); + multiwrap.wrap(nativeTokenContentToWrap, uriForWrappedToken, recipient); } - function test_wrap_revert_unapprovedTransfer721() public { - tokenOwner.setApprovalForAllERC721(address(erc721), address(multiwrap), false); + /** + * note: Testing revert condition; token owner calls `wrap` to wrap native tokens, but with multiple instances in `tokensToWrap` array. + */ + function test_revert_wrap_nativeTokens_insufficientValue_multipleInstances() public { + address recipient = address(0x123); + + ITokenBundle.Token[] memory nativeTokenContentToWrap = new ITokenBundle.Token[](2); + + vm.deal(address(tokenOwner), 100 ether); + nativeTokenContentToWrap[0] = ITokenBundle.Token({ + assetContract: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 5 ether + }); + nativeTokenContentToWrap[1] = ITokenBundle.Token({ + assetContract: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 5 ether + }); + + vm.prank(address(tokenOwner)); + vm.expectRevert("msg.value != amount"); + multiwrap.wrap{ value: 10 ether }(nativeTokenContentToWrap, uriForWrappedToken, recipient); + } + + /** + * note: Testing revert condition; token owner calls `wrap` to wrap un-owned ERC20 tokens. + */ + function test_revert_wrap_notOwner_ERC20() public { + tokenOwner.transferERC20(address(erc20), address(0x12), 10 ether); + + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + vm.expectRevert("ERC20: transfer amount exceeds balance"); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); + } + + /** + * note: Testing revert condition; token owner calls `wrap` to wrap un-owned ERC721 tokens. + */ + function test_revert_wrap_notOwner_ERC721() public { + tokenOwner.transferERC721(address(erc721), address(0x12), 0); + + address recipient = address(0x123); + vm.prank(address(tokenOwner)); vm.expectRevert("ERC721: transfer caller is not owner nor approved"); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); + } + + /** + * note: Testing revert condition; token owner calls `wrap` to wrap un-owned ERC1155 tokens. + */ + function test_revert_wrap_notOwner_ERC1155() public { + tokenOwner.transferERC1155(address(erc1155), address(0x12), 0, 100, ""); + + address recipient = address(0x123); vm.prank(address(tokenOwner)); - multiwrap.wrap(wrappedContents, uriForWrappedToken, address(wrappedTokenRecipient)); + vm.expectRevert("ERC1155: insufficient balance for transfer"); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); } - function test_wrap_revert_unapprovedTransfer20() public { + /** + * note: Testing revert condition; token owner calls `wrap` to wrap un-owned ERC20 tokens. + */ + function test_revert_wrap_notApprovedTransfer_ERC20() public { tokenOwner.setAllowanceERC20(address(erc20), address(multiwrap), 0); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); vm.expectRevert("ERC20: insufficient allowance"); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); + } + + /** + * note: Testing revert condition; token owner calls `wrap` to wrap un-owned ERC721 tokens. + */ + function test_revert_wrap_notApprovedTransfer_ERC721() public { + tokenOwner.setApprovalForAllERC721(address(erc721), address(multiwrap), false); + + address recipient = address(0x123); vm.prank(address(tokenOwner)); - multiwrap.wrap(wrappedContents, uriForWrappedToken, address(wrappedTokenRecipient)); + vm.expectRevert("ERC721: transfer caller is not owner nor approved"); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); } - function test_wrap_emit_TokensWrapped() public { - uint256 tokenIdOfWrapped = multiwrap.nextTokenIdToMint(); + /** + * note: Testing revert condition; token owner calls `wrap` to wrap un-owned ERC1155 tokens. + */ + function test_revert_wrap_notApprovedTransfer_ERC1155() public { + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(multiwrap), false); - IMultiwrap.Token[] memory contents = new IMultiwrap.Token[](wrappedContents.length); - for (uint256 i = 0; i < wrappedContents.length; i += 1) { - contents[i] = wrappedContents[i]; - } + address recipient = address(0x123); - vm.expectEmit(true, true, true, true); - emit TokensWrapped(address(tokenOwner), address(wrappedTokenRecipient), tokenIdOfWrapped, contents); + vm.prank(address(tokenOwner)); + vm.expectRevert("ERC1155: caller is not owner nor approved"); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); + } + + function test_revert_wrap_noTokensToWrap() public { + ITokenBundle.Token[] memory emptyContent; + + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + vm.expectRevert("TokenBundle: no tokens to bind."); + multiwrap.wrap(emptyContent, uriForWrappedToken, recipient); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `unwrap` + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing state changes; wrapped token owner calls `unwrap` to unwrap underlying tokens. + */ + function test_state_unwrap() public { + // ===== setup: wrap tokens ===== + uint256 expectedIdForWrappedToken = multiwrap.nextTokenIdToMint(); + address recipient = address(0x123); vm.prank(address(tokenOwner)); - multiwrap.wrap(wrappedContents, uriForWrappedToken, address(wrappedTokenRecipient)); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); + + // ===== target test content ===== + + vm.prank(recipient); + multiwrap.unwrap(expectedIdForWrappedToken, recipient); + + vm.expectRevert("ERC721: owner query for nonexistent token"); + multiwrap.ownerOf(expectedIdForWrappedToken); + + assertEq("", multiwrap.tokenURI(expectedIdForWrappedToken)); + + ITokenBundle.Token[] memory contentsOfWrappedToken = multiwrap.getWrappedContents(expectedIdForWrappedToken); + assertEq(contentsOfWrappedToken.length, 0); } - /// @dev Test `unwrap` + /** + * note: Testing state changes; wrapped token owner calls `unwrap` to unwrap underlying tokens. + */ + function test_state_unwrap_approvedCaller() public { + // ===== setup: wrap tokens ===== + uint256 expectedIdForWrappedToken = multiwrap.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); + + // ===== target test content ===== + + address approvedCaller = address(0x12); + + vm.prank(recipient); + multiwrap.setApprovalForAll(approvedCaller, true); + + vm.prank(approvedCaller); + multiwrap.unwrap(expectedIdForWrappedToken, recipient); + + vm.expectRevert("ERC721: owner query for nonexistent token"); + multiwrap.ownerOf(expectedIdForWrappedToken); + + assertEq("", multiwrap.tokenURI(expectedIdForWrappedToken)); + + ITokenBundle.Token[] memory contentsOfWrappedToken = multiwrap.getWrappedContents(expectedIdForWrappedToken); + assertEq(contentsOfWrappedToken.length, 0); + } + + function test_event_unwrap_TokensUnwrapped() public { + // ===== setup: wrap tokens ===== + uint256 expectedIdForWrappedToken = multiwrap.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); + + // ===== target test content ===== + + vm.prank(recipient); + + vm.expectEmit(true, true, true, true); + emit TokensUnwrapped(recipient, recipient, expectedIdForWrappedToken); + + multiwrap.unwrap(expectedIdForWrappedToken, recipient); + } - function test_unwrap() public { - uint256 tokenIdOfWrapped = multiwrap.nextTokenIdToMint(); + function test_balances_unwrap() public { + // ===== setup: wrap tokens ===== + uint256 expectedIdForWrappedToken = multiwrap.nextTokenIdToMint(); + address recipient = address(0x123); vm.prank(address(tokenOwner)); - multiwrap.wrap(wrappedContents, uriForWrappedToken, address(wrappedTokenRecipient)); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); - assertEq(multiwrap.ownerOf(tokenIdOfWrapped), address(wrappedTokenRecipient)); + // ===== target test content ===== - assertEq(erc20.balanceOf(address(multiwrap)), erc20Amount); - assertEq(erc721.ownerOf(erc721TokenId), address(multiwrap)); - assertEq(erc1155.balanceOf(address(multiwrap), erc1155TokenId), erc1155Amount); + // ERC20 balance + assertEq(erc20.balanceOf(address(recipient)), 0); + assertEq(erc20.balanceOf(address(multiwrap)), 10 ether); - assertEq(erc20.balanceOf(address(wrappedTokenRecipient)), 0); - assertEq(erc1155.balanceOf(address(wrappedTokenRecipient), erc1155TokenId), 0); + // ERC721 balance + assertEq(erc721.ownerOf(0), address(multiwrap)); - vm.prank(address(wrappedTokenRecipient)); - multiwrap.unwrap(tokenIdOfWrapped, address(wrappedTokenRecipient)); + // ERC1155 balance + assertEq(erc1155.balanceOf(address(recipient), 0), 0); + assertEq(erc1155.balanceOf(address(multiwrap), 0), 100); - assertEq(erc20.balanceOf(address(wrappedTokenRecipient)), erc20Amount); - assertEq(erc721.ownerOf(erc721TokenId), address(wrappedTokenRecipient)); - assertEq(erc1155.balanceOf(address(wrappedTokenRecipient), erc1155TokenId), erc1155Amount); + vm.prank(recipient); + multiwrap.unwrap(expectedIdForWrappedToken, recipient); + // ERC20 balance + assertEq(erc20.balanceOf(address(recipient)), 10 ether); assertEq(erc20.balanceOf(address(multiwrap)), 0); - assertEq(erc1155.balanceOf(address(multiwrap), erc1155TokenId), 0); + + // ERC721 balance + assertEq(erc721.ownerOf(0), address(recipient)); + + // ERC1155 balance + assertEq(erc1155.balanceOf(address(recipient), 0), 100); + assertEq(erc1155.balanceOf(address(multiwrap), 0), 0); } - function test_unwrap_revert_invalidTokenId() public { + function test_revert_unwrap_invalidTokenId() public { + // ===== setup: wrap tokens ===== + uint256 expectedIdForWrappedToken = multiwrap.nextTokenIdToMint(); + address recipient = address(0x123); + vm.prank(address(tokenOwner)); - multiwrap.wrap(wrappedContents, uriForWrappedToken, address(wrappedTokenRecipient)); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); - uint256 invalidId = multiwrap.nextTokenIdToMint(); + // ===== target test content ===== - vm.expectRevert("invalid tokenId"); + vm.prank(recipient); + vm.expectRevert("Multiwrap: wrapped NFT DNE."); + multiwrap.unwrap(expectedIdForWrappedToken + 1, recipient); + } + + function test_revert_unwrap_unapprovedCaller() public { + // ===== setup: wrap tokens ===== + uint256 expectedIdForWrappedToken = multiwrap.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); + + // ===== target test content ===== - vm.prank(address(wrappedTokenRecipient)); - multiwrap.unwrap(invalidId, address(wrappedTokenRecipient)); + vm.prank(address(0x12)); + vm.expectRevert("Multiwrap: caller not approved for unwrapping."); + multiwrap.unwrap(expectedIdForWrappedToken, recipient); } - function test_unwrap_emit_Unwrapped() public { - uint256 tokenIdOfWrapped = multiwrap.nextTokenIdToMint(); + function test_revert_unwrap_notOwner() public { + // ===== setup: wrap tokens ===== + uint256 expectedIdForWrappedToken = multiwrap.nextTokenIdToMint(); + address recipient = address(0x123); vm.prank(address(tokenOwner)); - multiwrap.wrap(wrappedContents, uriForWrappedToken, address(wrappedTokenRecipient)); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); - IMultiwrap.Token[] memory contents = new IMultiwrap.Token[](wrappedContents.length); - for (uint256 i = 0; i < wrappedContents.length; i += 1) { - contents[i] = wrappedContents[i]; - } + // ===== target test content ===== - vm.expectEmit(true, true, true, true); - emit TokensUnwrapped( - address(wrappedTokenRecipient), - address(wrappedTokenRecipient), - tokenIdOfWrapped, - contents + vm.prank(recipient); + multiwrap.transferFrom(recipient, address(0x12), 0); + + vm.prank(recipient); + vm.expectRevert("Multiwrap: caller not approved for unwrapping."); + multiwrap.unwrap(expectedIdForWrappedToken, recipient); + } + + function test_revert_unwrap_access_UNWRAP_ROLE() public { + // ===== setup: wrap tokens ===== + uint256 expectedIdForWrappedToken = multiwrap.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + multiwrap.wrap(wrappedContent, uriForWrappedToken, recipient); + + // ===== target test content ===== + + vm.prank(deployer); + multiwrap.revokeRole(keccak256("UNWRAP_ROLE"), address(0)); + + string memory errorMsg = string( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(recipient), 20), + " is missing role ", + Strings.toHexString(uint256(keccak256("UNWRAP_ROLE")), 32) + ) ); - vm.prank(address(wrappedTokenRecipient)); - multiwrap.unwrap(tokenIdOfWrapped, address(wrappedTokenRecipient)); + vm.prank(recipient); + vm.expectRevert(bytes(errorMsg)); + multiwrap.unwrap(expectedIdForWrappedToken, recipient); + } + + /** + * Fuzz testing: + * - Wrapping and unwrapping arbitrary kinds of tokens + */ + + uint256 internal constant MAX_TOKENS = 1000; + + function getTokensToWrap(uint256 x) internal returns (ITokenBundle.Token[] memory tokensToWrap) { + uint256 len = x % MAX_TOKENS; + tokensToWrap = new ITokenBundle.Token[](len); + + for (uint256 i = 0; i < len; i += 1) { + uint256 random = uint256(keccak256(abi.encodePacked(len + i))) % MAX_TOKENS; + uint256 selector = random % 3; + + if (selector == 0) { + tokensToWrap[i] = ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: random + }); + + erc20.mint(address(tokenOwner), tokensToWrap[i].totalAmount); + } else if (selector == 1) { + uint256 tokenId = erc721.nextTokenIdToMint(); + + tokensToWrap[i] = ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: tokenId, + totalAmount: 1 + }); + + erc721.mint(address(tokenOwner), 1); + } else if (selector == 2) { + tokensToWrap[i] = ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: random, + totalAmount: random + }); + + erc1155.mint(address(tokenOwner), tokensToWrap[i].tokenId, tokensToWrap[i].totalAmount); + } + } + } + + function test_fuzz_state_wrap(uint256 x) public { + ITokenBundle.Token[] memory tokensToWrap = getTokensToWrap(x); + if (tokensToWrap.length == 0) { + return; + } + + uint256 expectedIdForWrappedToken = multiwrap.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + multiwrap.wrap(tokensToWrap, uriForWrappedToken, recipient); + + assertEq(expectedIdForWrappedToken + 1, multiwrap.nextTokenIdToMint()); + + ITokenBundle.Token[] memory contentsOfWrappedToken = multiwrap.getWrappedContents(expectedIdForWrappedToken); + assertEq(contentsOfWrappedToken.length, tokensToWrap.length); + for (uint256 i = 0; i < contentsOfWrappedToken.length; i += 1) { + assertEq(contentsOfWrappedToken[i].assetContract, tokensToWrap[i].assetContract); + assertEq(uint256(contentsOfWrappedToken[i].tokenType), uint256(tokensToWrap[i].tokenType)); + assertEq(contentsOfWrappedToken[i].tokenId, tokensToWrap[i].tokenId); + assertEq(contentsOfWrappedToken[i].totalAmount, tokensToWrap[i].totalAmount); + } + + assertEq(uriForWrappedToken, multiwrap.tokenURI(expectedIdForWrappedToken)); + } + + function test_fuzz_state_unwrap(uint256 x) public { + // ===== setup: wrap tokens ===== + + ITokenBundle.Token[] memory tokensToWrap = getTokensToWrap(x); + if (tokensToWrap.length == 0) { + return; + } + + uint256 expectedIdForWrappedToken = multiwrap.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + multiwrap.wrap(tokensToWrap, uriForWrappedToken, recipient); + + // ===== target test content ===== + + vm.prank(recipient); + multiwrap.unwrap(expectedIdForWrappedToken, recipient); + + vm.expectRevert("ERC721: owner query for nonexistent token"); + multiwrap.ownerOf(expectedIdForWrappedToken); + + assertEq("", multiwrap.tokenURI(expectedIdForWrappedToken)); + + ITokenBundle.Token[] memory contentsOfWrappedToken = multiwrap.getWrappedContents(expectedIdForWrappedToken); + assertEq(contentsOfWrappedToken.length, 0); } } diff --git a/src/test/benchmark/MultiwrapBenchmark.t.sol b/src/test/benchmark/MultiwrapBenchmark.t.sol deleted file mode 100644 index 26621f187..000000000 --- a/src/test/benchmark/MultiwrapBenchmark.t.sol +++ /dev/null @@ -1,134 +0,0 @@ -// SPDX-License-Identifier: Apache-2.0 -pragma solidity ^0.8.0; - -import "../utils/BaseTest.sol"; -import "../utils/Wallet.sol"; -import "../mocks/MockERC20.sol"; -import "../mocks/MockERC721.sol"; -import "../mocks/MockERC1155.sol"; - -import "contracts/multiwrap/Multiwrap.sol"; -import "contracts/interfaces/IMultiwrap.sol"; - -contract MultiwrapBenchmarkTest is BaseTest { - // Target contract - Multiwrap internal multiwrap; - - // Actors - Wallet internal tokenOwner; - Wallet internal wrappedTokenRecipient; - - // Benchmark parameters - string internal uriForWrappedToken = "ipfs://wrappedNFT"; - IMultiwrap.Token[] internal wrappedContents; - - uint256 internal erc721TokenId = 0; - uint256 internal erc1155TokenId = 0; - uint256 internal erc1155Amount = 50; - uint256 internal erc20Amount = 100 ether; - - IMultiwrap.Token[] internal fiveERC721NFts; - IMultiwrap.Token[] internal oneERC721NFTWithERC20Token; - IMultiwrap.Token[] internal allThreeKindsOfTokens; - - // ===== Set up ===== - function setUp() public override { - super.setUp(); - - // Get Multiwrap contract. - multiwrap = Multiwrap(getContract("Multiwrap")); - - vm.label(address(erc20), "ERC20"); - vm.label(address(erc721), "ERC721"); - vm.label(address(erc1155), "ERC1155"); - vm.label(address(multiwrap), "Multiwrap"); - - // Get test actors. - tokenOwner = new Wallet(); - wrappedTokenRecipient = new Wallet(); - - // Grant MINTER_ROLE to `tokenOwner` - vm.prank(deployer); - multiwrap.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); - - // Mint mock ERC20/721/1155 tokens to `tokenOwner` - - erc20.mint(address(tokenOwner), erc20Amount); - erc721.mint(address(tokenOwner), 5); - erc1155.mint(address(tokenOwner), erc1155TokenId, erc1155Amount); - - // Allow Multiwrap to transfer tokens. - tokenOwner.setAllowanceERC20(address(erc20), address(multiwrap), erc20Amount); - tokenOwner.setApprovalForAllERC721(address(erc721), address(multiwrap), true); - tokenOwner.setApprovalForAllERC1155(address(erc1155), address(multiwrap), true); - - // Prepare wrapped contents. - - for (uint256 i = 0; i < 5; i += 1) { - fiveERC721NFts.push( - IMultiwrap.Token({ - assetContract: address(erc721), - tokenType: IMultiwrap.TokenType.ERC721, - tokenId: i, - amount: 1 - }) - ); - } - - wrappedContents.push( - IMultiwrap.Token({ - assetContract: address(erc20), - tokenType: IMultiwrap.TokenType.ERC20, - tokenId: 0, - amount: erc20Amount - }) - ); - wrappedContents.push( - IMultiwrap.Token({ - assetContract: address(erc721), - tokenType: IMultiwrap.TokenType.ERC721, - tokenId: erc721TokenId, - amount: 1 - }) - ); - wrappedContents.push( - IMultiwrap.Token({ - assetContract: address(erc1155), - tokenType: IMultiwrap.TokenType.ERC1155, - tokenId: erc1155TokenId, - amount: erc1155Amount - }) - ); - - oneERC721NFTWithERC20Token.push( - IMultiwrap.Token({ - assetContract: address(erc20), - tokenType: IMultiwrap.TokenType.ERC20, - tokenId: 0, - amount: erc20Amount - }) - ); - oneERC721NFTWithERC20Token.push( - IMultiwrap.Token({ - assetContract: address(erc721), - tokenType: IMultiwrap.TokenType.ERC721, - tokenId: erc721TokenId, - amount: 1 - }) - ); - - vm.startPrank(address(tokenOwner)); - } - - function testGas_wrap_fiveERC721NFTs() public { - multiwrap.wrap(fiveERC721NFts, uriForWrappedToken, address(wrappedTokenRecipient)); - } - - function testGas_wrap_oneERC721NFTWithERC20Token() public { - multiwrap.wrap(oneERC721NFTWithERC20Token, uriForWrappedToken, address(wrappedTokenRecipient)); - } - - function testGas_wrap_allThreeKindsOfTokens() public { - multiwrap.wrap(allThreeKindsOfTokens, uriForWrappedToken, address(wrappedTokenRecipient)); - } -} diff --git a/src/test/utils/BaseTest.sol b/src/test/utils/BaseTest.sol index 57dea3534..52ecc892c 100644 --- a/src/test/utils/BaseTest.sol +++ b/src/test/utils/BaseTest.sol @@ -13,7 +13,7 @@ import "contracts/Forwarder.sol"; import "contracts/TWFee.sol"; import "contracts/TWRegistry.sol"; import "contracts/TWFactory.sol"; -import "contracts/multiwrap/Multiwrap.sol"; +import { Multiwrap } from "contracts/multiwrap/Multiwrap.sol"; import "contracts/Pack.sol"; import "contracts/Split.sol"; import "contracts/drop/DropERC20.sol";