Skip to content

Commit f03a1c6

Browse files
nkrishangKrishang Nadgauda
and
Krishang Nadgauda
authored
Contracts SDK: base/ERC721Multiwrap (#197)
* initial multiwrap * Add helper/getter isApprovedOrOwner to ERC721Base * Add SoulboundERC721A to /extension * Add helper fn restrictTransfers to SoulboundERC721A * cleanup + finish ERC721Multiwrap * docs update * run prettier * docs update * pkg release * solhint code-complexity 7 -> 9 * remove external dependencies from ERC721Multiwrap * pkg release Co-authored-by: Krishang Nadgauda <nkrishang@Krishangs-MBP.lan>
1 parent b8402a0 commit f03a1c6

18 files changed

+2117
-6
lines changed

.solhint.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
"rules": {
55
"imports-on-top": "error",
66
"no-unused-vars": "error",
7-
"code-complexity": ["error", 7],
7+
"code-complexity": ["error", 9],
88
"compiler-version": ["error", "^0.8.0"],
99
"const-name-snakecase": "error",
1010
"contract-name-camelcase": "error",

contracts/base/ERC721Base.sol

+13
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,19 @@ contract ERC721Base is ERC721A, ContractMetadata, Multicall, Ownable, Royalty, B
140140
return _currentIndex;
141141
}
142142

143+
/// @notice Returns whether a given address is the owner, or approved to transfer an NFT.
144+
function isApprovedOrOwner(address _operator, uint256 _tokenId)
145+
public
146+
view
147+
virtual
148+
returns (bool isApprovedOrOwnerOf)
149+
{
150+
address owner = ownerOf(_tokenId);
151+
isApprovedOrOwnerOf = (_operator == owner ||
152+
isApprovedForAll(owner, _operator) ||
153+
getApproved(_tokenId) == _operator);
154+
}
155+
143156
/*//////////////////////////////////////////////////////////////
144157
Internal (overrideable) functions
145158
//////////////////////////////////////////////////////////////*/

contracts/base/ERC721Multiwrap.sol

+165
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,165 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.0;
3+
4+
import "./ERC721Base.sol";
5+
6+
import { TokenStore, ERC1155Receiver, IERC1155Receiver } from "../extension/TokenStore.sol";
7+
import { Multicall } from "../extension/Multicall.sol";
8+
import "../extension/SoulboundERC721A.sol";
9+
10+
/**
11+
* BASE: ERC721Base
12+
* EXTENSION: TokenStore, SoulboundERC721A
13+
*
14+
* The `ERC721Multiwrap` contract uses the `ERC721Base` contract, along with the `TokenStore` and
15+
* `SoulboundERC721A` extension.
16+
*
17+
* The `ERC721Multiwrap` contract lets you wrap arbitrary ERC20, ERC721 and ERC1155 tokens you own
18+
* into a single wrapped token / NFT.
19+
*
20+
* The `SoulboundERC721A` extension lets you make your NFTs 'soulbound' i.e. non-transferrable.
21+
*
22+
*/
23+
24+
contract ERC721Multiwrap is Multicall, TokenStore, SoulboundERC721A, ERC721Base {
25+
/*//////////////////////////////////////////////////////////////
26+
Permission control roles
27+
//////////////////////////////////////////////////////////////*/
28+
29+
/// @dev Only MINTER_ROLE holders can wrap tokens, when wrapping is restricted.
30+
bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE");
31+
/// @dev Only UNWRAP_ROLE holders can unwrap tokens, when unwrapping is restricted.
32+
bytes32 private constant UNWRAP_ROLE = keccak256("UNWRAP_ROLE");
33+
/// @dev Only assets with ASSET_ROLE can be wrapped, when wrapping is restricted to particular assets.
34+
bytes32 private constant ASSET_ROLE = keccak256("ASSET_ROLE");
35+
36+
/*//////////////////////////////////////////////////////////////
37+
Events
38+
//////////////////////////////////////////////////////////////*/
39+
40+
/// @dev Emitted when tokens are wrapped.
41+
event TokensWrapped(
42+
address indexed wrapper,
43+
address indexed recipientOfWrappedToken,
44+
uint256 indexed tokenIdOfWrappedToken,
45+
Token[] wrappedContents
46+
);
47+
48+
/// @dev Emitted when tokens are unwrapped.
49+
event TokensUnwrapped(
50+
address indexed unwrapper,
51+
address indexed recipientOfWrappedContents,
52+
uint256 indexed tokenIdOfWrappedToken
53+
);
54+
55+
/*//////////////////////////////////////////////////////////////
56+
Events
57+
//////////////////////////////////////////////////////////////*/
58+
59+
/// @notice Checks whether the caller holds `role`, when restrictions for `role` are switched on.
60+
modifier onlyRoleWithSwitch(bytes32 role) {
61+
_checkRoleWithSwitch(role, msg.sender);
62+
_;
63+
}
64+
65+
/*//////////////////////////////////////////////////////////////
66+
Constructor
67+
//////////////////////////////////////////////////////////////*/
68+
69+
constructor(
70+
string memory _name,
71+
string memory _symbol,
72+
address _royaltyRecipient,
73+
uint128 _royaltyBps,
74+
address _nativeTokenWrapper
75+
) ERC721Base(_name, _symbol, _royaltyRecipient, _royaltyBps) TokenStore(_nativeTokenWrapper) {
76+
restrictTransfers(false);
77+
}
78+
79+
/*///////////////////////////////////////////////////////////////
80+
Public gette functions
81+
//////////////////////////////////////////////////////////////*/
82+
83+
/// @dev See ERC-165
84+
function supportsInterface(bytes4 interfaceId)
85+
public
86+
view
87+
virtual
88+
override(ERC1155Receiver, ERC721Base)
89+
returns (bool)
90+
{
91+
return
92+
super.supportsInterface(interfaceId) ||
93+
ERC721Base.supportsInterface(interfaceId) ||
94+
interfaceId == type(IERC1155Receiver).interfaceId;
95+
}
96+
97+
/*///////////////////////////////////////////////////////////////
98+
Wrapping / Unwrapping logic
99+
//////////////////////////////////////////////////////////////*/
100+
101+
/**
102+
* @notice Wrap multiple ERC1155, ERC721, ERC20 tokens into a single wrapped NFT.
103+
*
104+
* @param _tokensToWrap The tokens to wrap.
105+
* @param _uriForWrappedToken The metadata URI for the wrapped NFT.
106+
* @param _recipient The recipient of the wrapped NFT.
107+
*
108+
* @return tokenId The tokenId of the wrapped NFT minted.
109+
*/
110+
function wrap(
111+
Token[] calldata _tokensToWrap,
112+
string calldata _uriForWrappedToken,
113+
address _recipient
114+
) public payable virtual onlyRoleWithSwitch(MINTER_ROLE) returns (uint256 tokenId) {
115+
if (!hasRole(ASSET_ROLE, address(0))) {
116+
for (uint256 i = 0; i < _tokensToWrap.length; i += 1) {
117+
_checkRole(ASSET_ROLE, _tokensToWrap[i].assetContract);
118+
}
119+
}
120+
121+
tokenId = nextTokenIdToMint();
122+
123+
_storeTokens(msg.sender, _tokensToWrap, _uriForWrappedToken, tokenId);
124+
125+
_safeMint(_recipient, 1);
126+
127+
emit TokensWrapped(msg.sender, _recipient, tokenId, _tokensToWrap);
128+
}
129+
130+
/**
131+
* @notice Unwrap a wrapped NFT to retrieve underlying ERC1155, ERC721, ERC20 tokens.
132+
*
133+
* @param _tokenId The token Id of the wrapped NFT to unwrap.
134+
* @param _recipient The recipient of the underlying ERC1155, ERC721, ERC20 tokens of the wrapped NFT.
135+
*/
136+
function unwrap(uint256 _tokenId, address _recipient) public virtual onlyRoleWithSwitch(UNWRAP_ROLE) {
137+
require(_tokenId < nextTokenIdToMint(), "wrapped NFT DNE.");
138+
require(isApprovedOrOwner(msg.sender, _tokenId), "caller not approved for unwrapping.");
139+
140+
_burn(_tokenId);
141+
_releaseTokens(_recipient, _tokenId);
142+
143+
emit TokensUnwrapped(msg.sender, _recipient, _tokenId);
144+
}
145+
146+
/*///////////////////////////////////////////////////////////////
147+
Internal functions
148+
//////////////////////////////////////////////////////////////*/
149+
150+
/// @dev See {ERC721-_beforeTokenTransfer}.
151+
function _beforeTokenTransfers(
152+
address from,
153+
address to,
154+
uint256 startTokenId,
155+
uint256 quantity
156+
) internal virtual override(ERC721A, SoulboundERC721A) {
157+
super._beforeTokenTransfers(from, to, startTokenId, quantity);
158+
SoulboundERC721A._beforeTokenTransfers(from, to, startTokenId, quantity);
159+
}
160+
161+
/// @dev Returns whether transfers can be restricted in a given execution context.
162+
function _canRestrictTransfers() internal virtual override returns (bool) {
163+
return msg.sender == owner();
164+
}
165+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
// SPDX-License-Identifier: MIT
2+
// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/IERC1155Receiver.sol)
3+
4+
pragma solidity ^0.8.0;
5+
6+
import "./IERC165.sol";
7+
8+
/**
9+
* @dev _Available since v3.1._
10+
*/
11+
interface IERC1155Receiver is IERC165 {
12+
/**
13+
* @dev Handles the receipt of a single ERC1155 token type. This function is
14+
* called at the end of a `safeTransferFrom` after the balance has been updated.
15+
*
16+
* NOTE: To accept the transfer, this must return
17+
* `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))`
18+
* (i.e. 0xf23a6e61, or its own function selector).
19+
*
20+
* @param operator The address which initiated the transfer (i.e. msg.sender)
21+
* @param from The address which previously owned the token
22+
* @param id The ID of the token being transferred
23+
* @param value The amount of tokens being transferred
24+
* @param data Additional data with no specified format
25+
* @return `bytes4(keccak256("onERC1155Received(address,address,uint256,uint256,bytes)"))` if transfer is allowed
26+
*/
27+
function onERC1155Received(
28+
address operator,
29+
address from,
30+
uint256 id,
31+
uint256 value,
32+
bytes calldata data
33+
) external returns (bytes4);
34+
35+
/**
36+
* @dev Handles the receipt of a multiple ERC1155 token types. This function
37+
* is called at the end of a `safeBatchTransferFrom` after the balances have
38+
* been updated.
39+
*
40+
* NOTE: To accept the transfer(s), this must return
41+
* `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))`
42+
* (i.e. 0xbc197c81, or its own function selector).
43+
*
44+
* @param operator The address which initiated the batch transfer (i.e. msg.sender)
45+
* @param from The address which previously owned the token
46+
* @param ids An array containing ids of each token being transferred (order and length must match values array)
47+
* @param values An array containing amounts of each token being transferred (order and length must match ids array)
48+
* @param data Additional data with no specified format
49+
* @return `bytes4(keccak256("onERC1155BatchReceived(address,address,uint256[],uint256[],bytes)"))` if transfer is allowed
50+
*/
51+
function onERC1155BatchReceived(
52+
address operator,
53+
address from,
54+
uint256[] calldata ids,
55+
uint256[] calldata values,
56+
bytes calldata data
57+
) external returns (bytes4);
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// SPDX-License-Identifier: MIT
2+
// OpenZeppelin Contracts v4.4.1 (token/ERC721/IERC721Receiver.sol)
3+
4+
pragma solidity ^0.8.0;
5+
6+
/**
7+
* @title ERC721 token receiver interface
8+
* @dev Interface for any contract that wants to support safeTransfers
9+
* from ERC721 asset contracts.
10+
*/
11+
interface IERC721Receiver {
12+
/**
13+
* @dev Whenever an {IERC721} `tokenId` token is transferred to this contract via {IERC721-safeTransferFrom}
14+
* by `operator` from `from`, this function is called.
15+
*
16+
* It must return its Solidity selector to confirm the token transfer.
17+
* If any other value is returned or the interface is not implemented by the recipient, the transfer will be reverted.
18+
*
19+
* The selector can be obtained in Solidity with `IERC721.onERC721Received.selector`.
20+
*/
21+
function onERC721Received(
22+
address operator,
23+
address from,
24+
uint256 tokenId,
25+
bytes calldata data
26+
) external returns (bytes4);
27+
}
+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
pragma solidity ^0.8.0;
3+
4+
import "./Permissions.sol";
5+
6+
/**
7+
* The `SoulboundERC721A` extension smart contract is meant to be used with ERC721A contracts as its base. It
8+
* provides the appropriate `before transfer` hook for ERC721A, where it checks whether a given transfer is
9+
* valid to go through or not.
10+
*
11+
* This contract uses the `Permissions` extension, and creates a role 'TRANSFER_ROLE'.
12+
* - If `address(0)` holds the transfer role, then all transfers go through.
13+
* - Else, a transfer goes through only if either the sender or recipient holds the transfe role.
14+
*/
15+
16+
abstract contract SoulboundERC721A is Permissions {
17+
/// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted.
18+
bytes32 public constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE");
19+
20+
event TransfersRestricted(bool isRestricted);
21+
22+
/**
23+
* @notice Restrict transfers of NFTs.
24+
* @dev Restricting transfers means revoking the TRANSFER_ROLE from address(0). Making
25+
* transfers unrestricted means granting the TRANSFER_ROLE to address(0).
26+
*
27+
* @param _toRestrict Whether to restrict transfers or not.
28+
*/
29+
function restrictTransfers(bool _toRestrict) public virtual {
30+
if (_toRestrict) {
31+
_revokeRole(TRANSFER_ROLE, address(0));
32+
} else {
33+
_setupRole(TRANSFER_ROLE, address(0));
34+
}
35+
}
36+
37+
/// @dev Returns whether transfers can be restricted in a given execution context.
38+
function _canRestrictTransfers() internal virtual returns (bool);
39+
40+
/// @dev See {ERC721A-_beforeTokenTransfers}.
41+
function _beforeTokenTransfers(
42+
address from,
43+
address to,
44+
uint256,
45+
uint256
46+
) internal virtual {
47+
// If transfers are restricted on the contract, we still want to allow burning and minting.
48+
if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) {
49+
if (!hasRole(TRANSFER_ROLE, from) && !hasRole(TRANSFER_ROLE, to)) {
50+
revert("!TRANSFER_ROLE");
51+
}
52+
}
53+
}
54+
}

contracts/extension/TokenStore.sol

+4-4
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ pragma solidity ^0.8.0;
33

44
// ========== External imports ==========
55

6-
import "@openzeppelin/contracts/token/ERC1155/IERC1155.sol";
7-
import "@openzeppelin/contracts/token/ERC721/IERC721.sol";
6+
import "../eip/interface/IERC1155.sol";
7+
import "../eip/interface/IERC721.sol";
88

9-
import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol";
10-
import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
9+
import "../openzeppelin-presets/utils/ERC1155/ERC1155Holder.sol";
10+
import "../openzeppelin-presets/utils/ERC721/ERC721Holder.sol";
1111

1212
// ========== Internal imports ==========
1313

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// SPDX-License-Identifier: MIT
2+
// OpenZeppelin Contracts (last updated v4.5.0) (token/ERC1155/utils/ERC1155Holder.sol)
3+
4+
pragma solidity ^0.8.0;
5+
6+
import "./ERC1155Receiver.sol";
7+
8+
/**
9+
* Simple implementation of `ERC1155Receiver` that will allow a contract to hold ERC1155 tokens.
10+
*
11+
* IMPORTANT: When inheriting this contract, you must include a way to use the received tokens, otherwise they will be
12+
* stuck.
13+
*
14+
* @dev _Available since v3.1._
15+
*/
16+
contract ERC1155Holder is ERC1155Receiver {
17+
function onERC1155Received(
18+
address,
19+
address,
20+
uint256,
21+
uint256,
22+
bytes memory
23+
) public virtual override returns (bytes4) {
24+
return this.onERC1155Received.selector;
25+
}
26+
27+
function onERC1155BatchReceived(
28+
address,
29+
address,
30+
uint256[] memory,
31+
uint256[] memory,
32+
bytes memory
33+
) public virtual override returns (bytes4) {
34+
return this.onERC1155BatchReceived.selector;
35+
}
36+
}

0 commit comments

Comments
 (0)