422 lines
14 KiB
Solidity
422 lines
14 KiB
Solidity
|
// SPDX-License-Identifier: CC0-1.0
|
||
|
pragma solidity ^0.8.9;
|
||
|
|
||
|
import "@openzeppelin/contracts/access/Ownable.sol";
|
||
|
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
|
||
|
import "@openzeppelin/contracts/token/ERC1155/IERC1155Receiver.sol";
|
||
|
import "@openzeppelin/contracts/token/ERC1155/extensions/ERC1155Supply.sol";
|
||
|
import "@openzeppelin/contracts/access/AccessControl.sol";
|
||
|
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721Enumerable.sol";
|
||
|
import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol";
|
||
|
|
||
|
contract ERC721Full is ERC721Enumerable, ERC721URIStorage {
|
||
|
/// @dev Initializes the contract by setting a `name` and a `symbol` to the token collection.
|
||
|
/// @param name is a non-empty string
|
||
|
/// @param symbol is a non-empty string
|
||
|
constructor(string memory name, string memory symbol)
|
||
|
ERC721(name, symbol)
|
||
|
{}
|
||
|
|
||
|
/// @dev Hook that is called before any token transfer. This includes minting and burning. `from`'s `tokenId` will be transferred to `to`
|
||
|
/// @param from is an non-zero address
|
||
|
/// @param to is an non-zero address
|
||
|
/// @param tokenId is an uint256 which determine token transferred from `from` to `to`
|
||
|
function _beforeTokenTransfer(
|
||
|
address from,
|
||
|
address to,
|
||
|
uint256 tokenId
|
||
|
) internal virtual override(ERC721Enumerable, ERC721) {
|
||
|
ERC721Enumerable._beforeTokenTransfer(from, to, tokenId);
|
||
|
}
|
||
|
|
||
|
/// @notice Interface of the ERC165 standard
|
||
|
/// @param interfaceId is a byte4 which determine interface used
|
||
|
/// @return true if this contract implements the interface defined by `interfaceId`
|
||
|
function supportsInterface(bytes4 interfaceId)
|
||
|
public
|
||
|
view
|
||
|
virtual
|
||
|
override(ERC721Enumerable, ERC721)
|
||
|
returns (bool)
|
||
|
{
|
||
|
return
|
||
|
ERC721.supportsInterface(interfaceId) ||
|
||
|
ERC721Enumerable.supportsInterface(interfaceId);
|
||
|
}
|
||
|
|
||
|
/// @notice the Uniform Resource Identifier (URI) for `tokenId` token
|
||
|
/// @param tokenId is unit256
|
||
|
/// @return string of (URI) for `tokenId` token
|
||
|
function tokenURI(uint256 tokenId)
|
||
|
public
|
||
|
view
|
||
|
virtual
|
||
|
override(ERC721URIStorage, ERC721)
|
||
|
returns (string memory)
|
||
|
{
|
||
|
return ERC721URIStorage.tokenURI(tokenId);
|
||
|
}
|
||
|
|
||
|
function _burn(uint256 tokenId)
|
||
|
internal
|
||
|
override(ERC721, ERC721URIStorage)
|
||
|
{}
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Interface of the Multiverse NFT standard as defined in the EIP.
|
||
|
*/
|
||
|
interface IMultiverseNFT {
|
||
|
/**
|
||
|
* @dev struct to store delegate token details
|
||
|
*
|
||
|
*/
|
||
|
struct DelegateData {
|
||
|
address contractAddress;
|
||
|
uint256 tokenId;
|
||
|
uint256 quantity;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* @dev Emitted when one or more new delegate NFTs are added to a Multiverse NFT
|
||
|
*
|
||
|
*/
|
||
|
event Bundled(
|
||
|
uint256 multiverseTokenID,
|
||
|
DelegateData[] delegateData,
|
||
|
address ownerAddress
|
||
|
);
|
||
|
|
||
|
/**
|
||
|
* @dev Emitted when one or more delegate NFTs are removed from a Multiverse NFT
|
||
|
*/
|
||
|
event Unbundled(uint256 multiverseTokenID, DelegateData[] delegateData);
|
||
|
|
||
|
/**
|
||
|
* @dev Accepts the tokenId of the Multiverse NFT and returns an array of delegate token data
|
||
|
*/
|
||
|
function delegateTokens(uint256 multiverseTokenID)
|
||
|
external
|
||
|
view
|
||
|
returns (DelegateData[] memory);
|
||
|
|
||
|
/**
|
||
|
* @dev Removes one or more delegate NFTs from a Multiverse NFT
|
||
|
* This function accepts the delegate NFT details, and transfer those NFTs out of the Multiverse NFT contract to the owner's wallet
|
||
|
*/
|
||
|
function unbundle(
|
||
|
DelegateData[] memory delegateData,
|
||
|
uint256 multiverseTokenID
|
||
|
) external;
|
||
|
|
||
|
/**
|
||
|
* @dev Adds one or more delegate NFTs to a Multiverse NFT
|
||
|
* This function accepts the delegate NFT details, and transfers those NFTs to the Multiverse NFT contract
|
||
|
* Need to ensure that approval is given to this Multiverse NFT contract for the delegate NFTs so that they can be transferred programmatically
|
||
|
*/
|
||
|
function bundle(
|
||
|
DelegateData[] memory delegateData,
|
||
|
uint256 multiverseTokenID
|
||
|
) external;
|
||
|
|
||
|
/**
|
||
|
* @dev Initializes a new bundle, mints a Multiverse NFT and assigns it to msg.sender
|
||
|
* Returns the token ID of a new Multiverse NFT
|
||
|
* Note - When a new Multiverse NFT is initialized, it is empty, it does not contain any delegate NFTs
|
||
|
*/
|
||
|
function initBundle(DelegateData[] memory delegateData) external;
|
||
|
}
|
||
|
|
||
|
abstract contract MultiverseNFT is
|
||
|
IMultiverseNFT,
|
||
|
Ownable,
|
||
|
ERC721Full,
|
||
|
IERC1155Receiver,
|
||
|
AccessControl
|
||
|
{
|
||
|
using SafeMath for uint256;
|
||
|
bytes32 public constant BUNDLER_ROLE = keccak256("BUNDLER_ROLE");
|
||
|
|
||
|
uint256 currentMultiverseTokenID;
|
||
|
|
||
|
mapping(uint256 => DelegateData[]) public multiverseNFTDelegateData;
|
||
|
mapping(uint256 => mapping(address => mapping(uint256 => uint256)))
|
||
|
public tokenBalances;
|
||
|
|
||
|
constructor(address bundlerAddress) {
|
||
|
_setupRole(DEFAULT_ADMIN_ROLE, msg.sender);
|
||
|
_setupRole(BUNDLER_ROLE, msg.sender);
|
||
|
_setRoleAdmin(BUNDLER_ROLE, DEFAULT_ADMIN_ROLE);
|
||
|
_setupRole(BUNDLER_ROLE, bundlerAddress);
|
||
|
}
|
||
|
|
||
|
function delegateTokens(uint256 multiverseTokenID)
|
||
|
external
|
||
|
view
|
||
|
returns (DelegateData[] memory)
|
||
|
{
|
||
|
return multiverseNFTDelegateData[multiverseTokenID];
|
||
|
}
|
||
|
|
||
|
function initBundle(DelegateData[] memory delegateData) external {
|
||
|
uint256 tokenId = currentMultiverseTokenID.add(1);
|
||
|
for (uint256 i = 0; i < delegateData.length; i = i.add(1)) {
|
||
|
bool isERC721 = _isERC721(delegateData[i].contractAddress);
|
||
|
if (isERC721) {
|
||
|
require(
|
||
|
delegateData[i].quantity == 1,
|
||
|
"ERC721 quantity must be 1"
|
||
|
);
|
||
|
}
|
||
|
multiverseNFTDelegateData[tokenId].push(delegateData[i]);
|
||
|
}
|
||
|
|
||
|
_incrementMultiverseTokenID();
|
||
|
_safeMint(msg.sender, tokenId);
|
||
|
}
|
||
|
|
||
|
function bundle(
|
||
|
DelegateData[] memory delegateData,
|
||
|
uint256 multiverseTokenID
|
||
|
) external {
|
||
|
require(
|
||
|
hasRole(BUNDLER_ROLE, msg.sender) ||
|
||
|
ownerOf(multiverseTokenID) == msg.sender,
|
||
|
"msg.sender neither have bundler role nor multiversetoken owner"
|
||
|
);
|
||
|
_bundle(delegateData, multiverseTokenID);
|
||
|
}
|
||
|
|
||
|
function unbundle(
|
||
|
DelegateData[] memory delegateData,
|
||
|
uint256 multiverseTokenID
|
||
|
) external {
|
||
|
require(
|
||
|
ownerOf(multiverseTokenID) == msg.sender,
|
||
|
"msg.sender is not a multiversetoken owner"
|
||
|
);
|
||
|
for (uint256 i = 0; i < delegateData.length; i = i.add(1)) {
|
||
|
require(
|
||
|
_ensureDelegateBelongsToMultiverseNFT(
|
||
|
delegateData[i],
|
||
|
multiverseTokenID
|
||
|
),
|
||
|
"delegate not assigned to multiverse token"
|
||
|
);
|
||
|
uint256 balance = tokenBalances[multiverseTokenID][
|
||
|
delegateData[i].contractAddress
|
||
|
][delegateData[i].tokenId];
|
||
|
require(
|
||
|
delegateData[i].quantity <= balance,
|
||
|
"quantity exceeds balance"
|
||
|
);
|
||
|
require(
|
||
|
_ensureMultiverseContractOwnsDelegate(delegateData[i]),
|
||
|
"delegate not owned by contract"
|
||
|
);
|
||
|
|
||
|
address contractAddress = delegateData[i].contractAddress;
|
||
|
uint256 tokenId = delegateData[i].tokenId;
|
||
|
uint256 quantity = delegateData[i].quantity;
|
||
|
|
||
|
_updateDelegateBalances(delegateData[i], multiverseTokenID);
|
||
|
|
||
|
if (_isERC721(contractAddress)) {
|
||
|
ERC721Full erc721Instance = ERC721Full(contractAddress);
|
||
|
erc721Instance.transferFrom(address(this), msg.sender, tokenId);
|
||
|
} else if (_isERC1155(contractAddress)) {
|
||
|
ERC1155Supply erc1155Instance = ERC1155Supply(contractAddress);
|
||
|
erc1155Instance.safeTransferFrom(
|
||
|
address(this),
|
||
|
msg.sender,
|
||
|
tokenId,
|
||
|
quantity,
|
||
|
""
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
emit Unbundled(multiverseTokenID, delegateData);
|
||
|
}
|
||
|
|
||
|
function supportsInterface(bytes4 interfaceId)
|
||
|
public
|
||
|
view
|
||
|
override(AccessControl, ERC721Full, IERC165)
|
||
|
returns (bool)
|
||
|
{
|
||
|
return
|
||
|
AccessControl.supportsInterface(interfaceId) ||
|
||
|
ERC721Full.supportsInterface(interfaceId);
|
||
|
}
|
||
|
|
||
|
function _bundle(
|
||
|
DelegateData[] memory delegateData,
|
||
|
uint256 multiverseTokenID
|
||
|
) internal {
|
||
|
for (uint256 i = 0; i < delegateData.length; i = i.add(1)) {
|
||
|
require(
|
||
|
_ensureDelegateBelongsToMultiverseNFT(
|
||
|
delegateData[i],
|
||
|
multiverseTokenID
|
||
|
),
|
||
|
"delegate not assigned to multiversetoken"
|
||
|
);
|
||
|
require(
|
||
|
_ensureDelegateQuantityLimitForMMultiverseNFT(
|
||
|
delegateData[i],
|
||
|
multiverseTokenID
|
||
|
),
|
||
|
"delegate quantity assigned to multiversetoken exceeds"
|
||
|
);
|
||
|
|
||
|
address contractAddress = delegateData[i].contractAddress;
|
||
|
uint256 tokenId = delegateData[i].tokenId;
|
||
|
uint256 quantity = delegateData[i].quantity;
|
||
|
|
||
|
tokenBalances[multiverseTokenID][contractAddress][
|
||
|
tokenId
|
||
|
] = tokenBalances[multiverseTokenID][contractAddress][tokenId].add(
|
||
|
quantity
|
||
|
);
|
||
|
|
||
|
if (_isERC721(contractAddress)) {
|
||
|
require(
|
||
|
quantity == 1,
|
||
|
"ERC721 cannot have quantity more than 1"
|
||
|
);
|
||
|
ERC721Full erc721Instance = ERC721Full(contractAddress);
|
||
|
erc721Instance.transferFrom(msg.sender, address(this), tokenId);
|
||
|
} else if (_isERC1155(contractAddress)) {
|
||
|
ERC1155Supply erc1155Instance = ERC1155Supply(contractAddress);
|
||
|
erc1155Instance.safeTransferFrom(
|
||
|
msg.sender,
|
||
|
address(this),
|
||
|
tokenId,
|
||
|
quantity,
|
||
|
""
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
emit Bundled(
|
||
|
multiverseTokenID,
|
||
|
delegateData,
|
||
|
ownerOf(multiverseTokenID)
|
||
|
);
|
||
|
}
|
||
|
|
||
|
function _ensureDelegateBelongsToMultiverseNFT(
|
||
|
DelegateData memory delegateData,
|
||
|
uint256 multiverseTokenID
|
||
|
) internal view returns (bool) {
|
||
|
DelegateData[] memory storedData = multiverseNFTDelegateData[
|
||
|
multiverseTokenID
|
||
|
];
|
||
|
for (uint256 i = 0; i < storedData.length; i = i.add(1)) {
|
||
|
if (
|
||
|
delegateData.contractAddress == storedData[i].contractAddress &&
|
||
|
delegateData.tokenId == storedData[i].tokenId
|
||
|
) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
function _ensureMultiverseContractOwnsDelegate(
|
||
|
DelegateData memory delegateData
|
||
|
) internal view returns (bool) {
|
||
|
if (_isERC721(delegateData.contractAddress)) {
|
||
|
ERC721Full erc721Instance = ERC721Full(
|
||
|
delegateData.contractAddress
|
||
|
);
|
||
|
if (address(this) == erc721Instance.ownerOf(delegateData.tokenId)) {
|
||
|
return true;
|
||
|
}
|
||
|
} else if (_isERC1155(delegateData.contractAddress)) {
|
||
|
ERC1155Supply erc1155Instance = ERC1155Supply(
|
||
|
delegateData.contractAddress
|
||
|
);
|
||
|
if (
|
||
|
erc1155Instance.balanceOf(
|
||
|
address(this),
|
||
|
delegateData.tokenId
|
||
|
) >= delegateData.quantity
|
||
|
) {
|
||
|
return true;
|
||
|
}
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
|
||
|
function _ensureDelegateQuantityLimitForMMultiverseNFT(
|
||
|
DelegateData memory delegateData,
|
||
|
uint256 multiverseTokenID
|
||
|
) internal view returns (bool) {
|
||
|
DelegateData[] memory storedData = multiverseNFTDelegateData[
|
||
|
multiverseTokenID
|
||
|
];
|
||
|
for (uint256 i = 0; i < storedData.length; i = i.add(1)) {
|
||
|
if (
|
||
|
delegateData.contractAddress == storedData[i].contractAddress &&
|
||
|
delegateData.tokenId == storedData[i].tokenId
|
||
|
) {
|
||
|
uint256 balance = tokenBalances[multiverseTokenID][
|
||
|
delegateData.contractAddress
|
||
|
][delegateData.tokenId];
|
||
|
if (
|
||
|
balance.add(delegateData.quantity) <= storedData[i].quantity
|
||
|
) {
|
||
|
return true;
|
||
|
}
|
||
|
return false;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function _updateDelegateBalances(
|
||
|
DelegateData memory delegateData,
|
||
|
uint256 multiverseTokenID
|
||
|
) internal returns (uint256) {
|
||
|
address contractAddress = delegateData.contractAddress;
|
||
|
uint256 tokenId = delegateData.tokenId;
|
||
|
tokenBalances[multiverseTokenID][contractAddress][
|
||
|
tokenId
|
||
|
] = tokenBalances[multiverseTokenID][contractAddress][tokenId].sub(
|
||
|
delegateData.quantity
|
||
|
);
|
||
|
return tokenBalances[multiverseTokenID][contractAddress][tokenId];
|
||
|
}
|
||
|
|
||
|
function onERC1155Received(
|
||
|
address operator,
|
||
|
address from,
|
||
|
uint256 id,
|
||
|
uint256 value,
|
||
|
bytes calldata data
|
||
|
) external pure override returns (bytes4) {
|
||
|
return this.onERC1155Received.selector;
|
||
|
}
|
||
|
|
||
|
function onERC1155BatchReceived(
|
||
|
address operator,
|
||
|
address from,
|
||
|
uint256[] calldata ids,
|
||
|
uint256[] calldata values,
|
||
|
bytes calldata data
|
||
|
) external pure override returns (bytes4) {
|
||
|
return this.onERC1155BatchReceived.selector;
|
||
|
}
|
||
|
|
||
|
function _isERC1155(address contractAddress) internal view returns (bool) {
|
||
|
return IERC1155(contractAddress).supportsInterface(0xd9b67a26);
|
||
|
}
|
||
|
|
||
|
function _isERC721(address contractAddress) internal view returns (bool) {
|
||
|
return IERC721(contractAddress).supportsInterface(0x80ac58cd);
|
||
|
}
|
||
|
|
||
|
function _incrementMultiverseTokenID() internal {
|
||
|
currentMultiverseTokenID = currentMultiverseTokenID.add(1);
|
||
|
}
|
||
|
}
|