Skip to content

Multiwrap: using thirdweb-dev/contracts/feature/ #160

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
May 20, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added assets/multiwrap-diagram.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 4 additions & 1 deletion contracts/feature/ContractMetadata.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
45 changes: 32 additions & 13 deletions contracts/feature/Permissions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down
82 changes: 82 additions & 0 deletions contracts/feature/TokenStore.sol
Original file line number Diff line number Diff line change
@@ -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]);
}
}
}
2 changes: 2 additions & 0 deletions contracts/feature/interface/IContractMetadata.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
40 changes: 4 additions & 36 deletions contracts/interfaces/IMultiwrap.sol
Original file line number Diff line number Diff line change
@@ -1,47 +1,16 @@
// 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.
*
* 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,
Expand All @@ -54,8 +23,7 @@ interface IMultiwrap {
event TokensUnwrapped(
address indexed unwrapper,
address indexed recipientOfWrappedContents,
uint256 indexed tokenIdOfWrappedToken,
Token[] wrappedContents
uint256 indexed tokenIdOfWrappedToken
);

/**
Expand Down
Loading