From 93c881529bcbd097177218168caef2824eab48f4 Mon Sep 17 00:00:00 2001 From: Yash <72552910+kumaryash90@users.noreply.github.com> Date: Wed, 29 Jun 2022 04:20:44 +0530 Subject: [PATCH] Pack Contract (#175) * new pack content struct * update pack interface * mock pack implementation * update pack interface with amountToDistributePerOpen * foundry * update Pack interface comments * implementation up till openPack * transfer pack contents into the contract on createPack * handle native token transfer case * up till compile * costly implementation, but works * cleanup * move pack to a directory * Add design doc * feature imports * update CurrencyTransferLib usage * format table in doc * created TempPack and ITempPack * refactor TempPack and ITempPack * PackInfo struct update * createPack test for TempPack * tests for TempPack * fix issues in Pack; tests for Pack and TempPack * benchmark test * remove twFee from Pack * remove unused imports from Pack * cleanup Pack * compile and run prettier * remove twFee * rename to nextTokenIdToMint * cleanup TempPack * avoid stack too deep error * update TempPack in tests * fix issue: getRewardUnits returning zero amount * forge updates * make createPack payable, to allow native tokens * add asset role * unit tests for TempPack * fuzz tests for TempPack * return rewardUnits from openPack in TempPack * fuzz test for openPack * separate function for random value generation * Fix bug: calculate pack total supply correctly * logs for tests * handling native tokens for pack * receive and withdraw functions for native token transfers * fuzz tests, and test logs * update currentTotalSupply in openPack * comment-out Pack -- not deleting yet * update tests * run prettier * update tests * scenario test -- reentrancy * perUnitAmounts -> numOfRewardUnits * run prettier * restrict receive() to native token wrapper * rename TempPack to Pack; delete older Pack * update tests * pull from main and update initialze fn * pkg update * deployment script for Pack * remove unused caller * fix import path Co-authored-by: Krishang Nadgauda Co-authored-by: Krishang Nadgauda --- .gitmodules | 2 +- assets/pack-diag-1.png | Bin 0 -> 19967 bytes assets/pack-diag-2.png | Bin 0 -> 4343 bytes assets/pack-diag-3.png | Bin 0 -> 14932 bytes contracts/Pack.sol | 558 ----------------- contracts/feature/TokenStore.sol | 21 +- contracts/interfaces/IPack.sol | 72 ++- contracts/pack/Pack.sol | 404 +++++++++++++ contracts/pack/pack.md | 239 ++++++++ contracts/package.json | 2 +- docs/IDropSinglePhase.md | 92 --- docs/IPack.md | 253 +------- docs/LinkTokenInterface.md | 262 -------- docs/Multiwrap.md | 17 + docs/Pack.md | 499 ++++++---------- docs/TokenStore.md | 17 + docs/VRFConsumerBase.md | 32 - docs/VRFRequestIDBase.md | 12 - scripts/deploy/pack.ts | 35 ++ src/test/Pack.t.sol | 997 +++++++++++++++++++++++++++++++ src/test/PackBenchmark.t.sol | 258 ++++++++ src/test/utils/BaseTest.sol | 12 +- 22 files changed, 2264 insertions(+), 1520 deletions(-) create mode 100644 assets/pack-diag-1.png create mode 100644 assets/pack-diag-2.png create mode 100644 assets/pack-diag-3.png delete mode 100644 contracts/Pack.sol create mode 100644 contracts/pack/Pack.sol create mode 100644 contracts/pack/pack.md delete mode 100644 docs/IDropSinglePhase.md delete mode 100644 docs/LinkTokenInterface.md delete mode 100644 docs/VRFConsumerBase.md delete mode 100644 docs/VRFRequestIDBase.md create mode 100644 scripts/deploy/pack.ts create mode 100644 src/test/Pack.t.sol create mode 100644 src/test/PackBenchmark.t.sol diff --git a/.gitmodules b/.gitmodules index f8dd5146a..9b90b5f46 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ url = https://github.com/brockelmore/forge-std [submodule "lib/ds-test"] path = lib/ds-test - url = https://github.com/dapphub/ds-test + url = https://github.com/dapphub/ds-test \ No newline at end of file diff --git a/assets/pack-diag-1.png b/assets/pack-diag-1.png new file mode 100644 index 0000000000000000000000000000000000000000..99e980a4a95b2e077f18b8a61aa9a0a46eb9794f GIT binary patch literal 19967 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 literal 0 HcmV?d00001 diff --git a/assets/pack-diag-2.png b/assets/pack-diag-2.png new file mode 100644 index 0000000000000000000000000000000000000000..cc3658a31b0e3687509c8da1d5831ae06446b3bd GIT binary patch literal 4343 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 r4QN{cfwC_B*J%&@Pe%a~_O7w)@L={*J royalty recipient and bps for token - mapping(uint256 => RoyaltyInfo) private royaltyInfoForToken; - - /// @dev pack tokenId => The state of packs with id `tokenId`. - mapping(uint256 => PackState) public packs; - - /// @dev pack tokenId => rewards in pack with id `tokenId`. - mapping(uint256 => Rewards) public rewards; - - /// @dev Chainlink VRF requestId => Chainlink VRF request state with id `requestId`. - mapping(bytes32 => RandomnessRequest) public randomnessRequests; - - /// @dev pack tokenId => pack opener => Chainlink VRF request ID if there is an incomplete pack opening process. - mapping(uint256 => mapping(address => bytes32)) public currentRequestId; - - /// @dev Emitted when a set of packs is created. - event PackAdded( - uint256 indexed packId, - address indexed rewardContract, - address indexed creator, - uint256 packTotalSupply, - PackState packState, - Rewards rewards - ); - - /// @dev Emitted on a request to open a pack. - event PackOpenRequested(uint256 indexed packId, address indexed opener, bytes32 requestId); - - /// @dev Emitted when a request to open a pack is fulfilled. - event PackOpenFulfilled( - uint256 indexed packId, - address indexed opener, - bytes32 requestId, - address indexed rewardContract, - uint256[] rewardIds - ); - - constructor( - address _vrfCoordinator, - address _linkToken, - address _thirdwebFee - ) VRFConsumerBase(_vrfCoordinator, _linkToken) initializer { - thirdwebFee = ITWFee(_thirdwebFee); - } - - /// @dev Initiliazes the contract, like a constructor. - function initialize( - address _defaultAdmin, - string memory _name, - string memory _symbol, - string memory _contractURI, - address[] memory _trustedForwarders, - address _royaltyRecipient, - uint128 _royaltyBps, - uint128 _fees, - bytes32 _keyHash - ) external initializer { - // Initialize inherited contracts, most base-like -> most derived. - __ERC2771Context_init(_trustedForwarders); - __ERC1155Preset_init(_defaultAdmin, _contractURI); - - // Initialize this contract's state. - vrfKeyHash = _keyHash; - vrfFees = _fees; - - name = _name; - symbol = _symbol; - royaltyRecipient = _royaltyRecipient; - royaltyBps = _royaltyBps; - contractURI = _contractURI; - - _owner = _defaultAdmin; - _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, _defaultAdmin); - _setupRole(TRANSFER_ROLE, address(0)); - } - - /** - * Public functions - */ - - /// @dev Returns the module type of the contract. - function contractType() external pure returns (bytes32) { - return MODULE_TYPE; - } - - /// @dev Returns the version of the contract. - function contractVersion() external pure returns (uint8) { - return uint8(VERSION); - } - - /** - * @dev Returns the address of the current owner. - */ - function owner() public view returns (address) { - return hasRole(DEFAULT_ADMIN_ROLE, _owner) ? _owner : address(0); - } - - /** - * @dev See {ERC1155-_mint}. - */ - function mint( - address, - uint256, - uint256, - bytes memory - ) public virtual override { - revert("cannot freely mint more packs"); - } - - /** - * @dev See {ERC1155-_mintBatch}. - */ - function mintBatch( - address, - uint256[] memory, - uint256[] memory, - bytes memory - ) public virtual override { - revert("cannot freely mint more packs"); - } - - function onERC1155Received( - address, - address, - uint256, - uint256, - bytes memory - ) public virtual override returns (bytes4) { - revert("Must use batch transfer."); - } - - /// @dev Creates pack on receiving ERC 1155 reward tokens - function onERC1155BatchReceived( - address _operator, - address, - uint256[] memory _ids, - uint256[] memory _values, - bytes memory _data - ) public override whenNotPaused returns (bytes4) { - // Get parameters for creating packs. - (string memory packURI, uint256 secondsUntilOpenStart, uint256 rewardsPerOpen) = abi.decode( - _data, - (string, uint256, uint256) - ); - - // Create packs. - createPack(_operator, packURI, _msgSender(), _ids, _values, secondsUntilOpenStart, rewardsPerOpen); - - return this.onERC1155BatchReceived.selector; - } - - /** - * External functions. - **/ - - /// @dev See EIP-2981 - 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; - } - - /// @dev Lets a pack owner request to open a single pack. - function openPack(uint256 _packId) external whenNotPaused { - PackState memory packState = packs[_packId]; - - require(block.timestamp >= packState.openStart, "outside window to open packs."); - require(LINK.balanceOf(address(this)) >= vrfFees, "out of LINK."); - require(balanceOf(_msgSender(), _packId) > 0, "must own packs to open."); - require(currentRequestId[_packId][_msgSender()] == "", "must wait for the pending pack to open."); - - // Burn the pack being opened. - _burn(_msgSender(), _packId, 1); - - // Send random number request. - bytes32 requestId = requestRandomness(vrfKeyHash, vrfFees); - - // Update state to reflect the Chainlink VRF request. - randomnessRequests[requestId] = RandomnessRequest({ packId: _packId, opener: _msgSender() }); - currentRequestId[_packId][_msgSender()] = requestId; - - emit PackOpenRequested(_packId, _msgSender(), requestId); - } - - /// @dev Lets a module admin withdraw link from the contract. - function withdrawLink(address _to, uint256 _amount) external onlyRole(DEFAULT_ADMIN_ROLE) { - bool success = LINK.transfer(_to, _amount); - require(success, "failed to withdraw LINK."); - } - - /// @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)); - } - - /** - * External: setter functions - */ - - /// @dev Lets a module admin change the Chainlink VRF fee. - function setChainlinkFees(uint256 _newFees) external onlyRole(DEFAULT_ADMIN_ROLE) { - vrfFees = _newFees; - } - - /// @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."); - address _prevOwner = _owner; - _owner = _newOwner; - - emit OwnerUpdated(_prevOwner, _newOwner); - } - - /// @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 Sets contract URI for the storefront-level metadata of the contract. - function setContractURI(string calldata _uri) external onlyRole(DEFAULT_ADMIN_ROLE) { - contractURI = _uri; - } - - /** - * Internal functions. - **/ - - /// @dev Creates packs with rewards. - function createPack( - address _creator, - string memory _packURI, - address _rewardContract, - uint256[] memory _rewardIds, - uint256[] memory _rewardAmounts, - uint256 _secondsUntilOpenStart, - uint256 _rewardsPerOpen - ) internal whenNotPaused { - require( - IERC1155Upgradeable(_rewardContract).supportsInterface(type(IERC1155Upgradeable).interfaceId), - "Pack: reward contract does not implement ERC 1155." - ); - require(hasRole(MINTER_ROLE, _creator), "not minter."); - require(_rewardIds.length > 0, "must add at least one reward."); - - uint256 sumOfRewards = _sumArr(_rewardAmounts); - - require(sumOfRewards % _rewardsPerOpen == 0, "invalid number of rewards per open."); - - // Get pack tokenId and total supply. - uint256 packId = nextTokenId; - nextTokenId += 1; - - uint256 packTotalSupply = sumOfRewards / _rewardsPerOpen; - - // Store pack state. - PackState memory packState = PackState({ - creator: _creator, - uri: _packURI, - openStart: block.timestamp + _secondsUntilOpenStart - }); - - // Store reward state. - Rewards memory rewardsInPack = Rewards({ - source: _rewardContract, - tokenIds: _rewardIds, - amountsPacked: _rewardAmounts, - rewardsPerOpen: _rewardsPerOpen - }); - - packs[packId] = packState; - rewards[packId] = rewardsInPack; - - // Mint packs to creator. - _mint(_creator, packId, packTotalSupply, ""); - - emit PackAdded(packId, _rewardContract, _creator, packTotalSupply, packState, rewardsInPack); - } - - /// @dev Returns a reward tokenId using `_randomness` provided by RNG. - function getReward( - uint256 _packId, - uint256 _randomness, - Rewards memory _rewardsInPack - ) internal returns (uint256[] memory rewardTokenIds, uint256[] memory rewardAmounts) { - uint256 base = _sumArr(_rewardsInPack.amountsPacked); - uint256 step; - uint256 prob; - - rewardTokenIds = new uint256[](_rewardsInPack.rewardsPerOpen); - rewardAmounts = new uint256[](_rewardsInPack.rewardsPerOpen); - - for (uint256 j = 0; j < _rewardsInPack.rewardsPerOpen; j += 1) { - prob = uint256(keccak256(abi.encode(_randomness, j))) % base; - - for (uint256 i = 0; i < _rewardsInPack.tokenIds.length; i += 1) { - if (prob < (_rewardsInPack.amountsPacked[i] + step)) { - // Store the reward's tokenId - rewardTokenIds[j] = _rewardsInPack.tokenIds[i]; - rewardAmounts[j] = 1; - - // Update amount of reward available in pack. - _rewardsInPack.amountsPacked[i] -= 1; - - // Reset step - step = 0; - break; - } else { - step += _rewardsInPack.amountsPacked[i]; - } - } - } - - rewards[_packId] = _rewardsInPack; - } - - /// @dev Called by Chainlink VRF with a random number, completing the opening of a pack. - function fulfillRandomness(bytes32 _requestId, uint256 _randomness) internal override { - RandomnessRequest memory request = randomnessRequests[_requestId]; - - uint256 packId = request.packId; - address receiver = request.opener; - - // Pending request completed - delete currentRequestId[packId][receiver]; - - // Get tokenId of the reward to distribute. - Rewards memory rewardsInPack = rewards[packId]; - - (uint256[] memory rewardIds, uint256[] memory rewardAmounts) = getReward(packId, _randomness, rewardsInPack); - - // Distribute the reward to the pack opener. - IERC1155Upgradeable(rewardsInPack.source).safeBatchTransferFrom( - address(this), - receiver, - rewardIds, - rewardAmounts, - "" - ); - - emit PackOpenFulfilled(packId, receiver, _requestId, rewardsInPack.source, rewardIds); - } - - /// @dev Runs on every transfer. - function _beforeTokenTransfer( - address operator, - address from, - address to, - uint256[] memory ids, - uint256[] memory amounts, - bytes memory data - ) internal virtual override { - super._beforeTokenTransfer(operator, from, to, ids, amounts, data); - - if (!hasRole(TRANSFER_ROLE, address(0)) && from != address(0) && to != address(0)) { - require( - hasRole(TRANSFER_ROLE, from) || hasRole(TRANSFER_ROLE, to), - "transfers restricted to TRANSFER_ROLE holders" - ); - } - } - - /// @dev Returns the sum of all elements in the array - function _sumArr(uint256[] memory arr) internal pure returns (uint256 sum) { - for (uint256 i = 0; i < arr.length; i += 1) { - sum += arr[i]; - } - } - - /// @dev See EIP-2771 - function _msgSender() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (address sender) - { - return ERC2771ContextUpgradeable._msgSender(); - } - - /// @dev See EIP-2771 - function _msgData() - internal - view - virtual - override(ContextUpgradeable, ERC2771ContextUpgradeable) - returns (bytes calldata) - { - return ERC2771ContextUpgradeable._msgData(); - } - - /** - * Rest: view functions - **/ - - /// @dev See EIP 165 - function supportsInterface(bytes4 interfaceId) public view override(ERC1155PresetUpgradeable) returns (bool) { - return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; - } - - /// @dev See EIP 1155 - function uri(uint256 _id) public view override returns (string memory) { - return packs[_id].uri; - } - - /// @dev Returns a pack with its underlying rewards - function getPackWithRewards(uint256 _packId) - external - view - returns ( - PackState memory pack, - uint256 packTotalSupply, - address source, - uint256[] memory tokenIds, - uint256[] memory amountsPacked - ) - { - pack = packs[_packId]; - packTotalSupply = totalSupply(_packId); - source = rewards[_packId].source; - tokenIds = rewards[_packId].tokenIds; - amountsPacked = rewards[_packId].amountsPacked; - } -} diff --git a/contracts/feature/TokenStore.sol b/contracts/feature/TokenStore.sol index 220a2a770..0319f4b79 100644 --- a/contracts/feature/TokenStore.sol +++ b/contracts/feature/TokenStore.sol @@ -15,8 +15,11 @@ import "./TokenBundle.sol"; import "../lib/CurrencyTransferLib.sol"; contract TokenStore is TokenBundle, ERC721Holder, ERC1155Holder { + /// @dev The address interpreted as native token of the chain. + address public constant NATIVE_TOKEN = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + /// @dev The address of the native token wrapper contract. - address private immutable nativeTokenWrapper; + address internal immutable nativeTokenWrapper; constructor(address _nativeTokenWrapper) { nativeTokenWrapper = _nativeTokenWrapper; @@ -75,8 +78,22 @@ contract TokenStore is TokenBundle, ERC721Holder, ERC1155Holder { address _to, Token[] memory _tokens ) internal { + uint256 nativeTokenValue; for (uint256 i = 0; i < _tokens.length; i += 1) { - _transferToken(_from, _to, _tokens[i]); + if (_tokens[i].assetContract == NATIVE_TOKEN && _to == address(this)) { + nativeTokenValue += _tokens[i].totalAmount; + } else { + _transferToken(_from, _to, _tokens[i]); + } + } + if (nativeTokenValue != 0) { + Token memory _nativeToken = Token({ + assetContract: NATIVE_TOKEN, + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: nativeTokenValue + }); + _transferToken(_from, _to, _nativeToken); } } } diff --git a/contracts/interfaces/IPack.sol b/contracts/interfaces/IPack.sol index bf32d03ee..947da3d05 100644 --- a/contracts/interfaces/IPack.sol +++ b/contracts/interfaces/IPack.sol @@ -1,41 +1,71 @@ // SPDX-License-Identifier: Apache-2.0 pragma solidity ^0.8.11; -import "./IThirdwebContract.sol"; -import "../feature/interface/IOwnable.sol"; -import "../feature/interface/IRoyalty.sol"; +import "../feature/interface/ITokenBundle.sol"; -interface IPack is IThirdwebContract, IOwnable, IRoyalty { +/** + * The thirdweb `Pack` contract is a lootbox mechanism. An account can bundle up arbitrary ERC20, ERC721 and ERC1155 tokens into + * a set of packs. A pack can then be opened in return for a selection of the tokens in the pack. The selection of tokens distributed + * on opening a pack depends on the relative supply of all tokens in the packs. + */ + +interface IPack is ITokenBundle { /** - * @notice A pack can contain ERC1155 tokens from n number of ERC1155 contracts. - * You can add any kinds of tokens to a pack via Multiwrap. + * @notice All info relevant to packs. + * + * @param perUnitAmounts Mapping from a UID -> to the per-unit amount of that asset i.e. `Token` at that index. + * @param openStartTimestamp The timestamp after which packs can be opened. + * @param amountDistributedPerOpen The number of reward units distributed per open. */ - struct PackContents { - address[] erc1155AssetContracts; - uint256[][] erc1155TokensToWrap; - uint256[][] erc1155AmountsToWrap; + struct PackInfo { + uint256[] perUnitAmounts; + uint128 openStartTimestamp; + uint128 amountDistributedPerOpen; } + /// @notice Emitted when a set of packs is created. + event PackCreated( + uint256 indexed packId, + address indexed packCreator, + address recipient, + uint256 totalPacksCreated + ); + + /// @notice Emitted when a pack is opened. + event PackOpened( + uint256 indexed packId, + address indexed opener, + uint256 numOfPacksOpened, + Token[] rewardUnitsDistributed + ); + /** * @notice Creates a pack with the stated contents. * - * @param contents The contents of the packs to be created. - * @param uri The (metadata) URI assigned to the packs created. - * @param openStartTimestamp The timestamp after which a pack is opened. - * @param nftsPerOpen The number of NFTs received on opening one pack. + * @param contents The reward units to pack in the packs. + * @param numOfRewardUnits The number of reward units to create, for each asset specified in `contents`. + * @param packUri The (metadata) URI assigned to the packs created. + * @param openStartTimestamp The timestamp after which packs can be opened. + * @param amountDistributedPerOpen The number of reward units distributed per open. + * @param recipient The recipient of the packs created. + * + * @return packId The unique identifer of the created set of packs. + * @return packTotalSupply The total number of packs created. */ function createPack( - PackContents calldata contents, - string calldata uri, + Token[] calldata contents, + uint256[] calldata numOfRewardUnits, + string calldata packUri, uint128 openStartTimestamp, - uint128 nftsPerOpen - ) external; + uint128 amountDistributedPerOpen, + address recipient + ) external payable returns (uint256 packId, uint256 packTotalSupply); /** - * @notice Lets a pack owner open a pack and receive the pack's NFTs. + * @notice Lets a pack owner open a pack and receive the pack's reward unit. * - * @param packId The identifier of the pack to open. + * @param packId The identifier of the pack to open. * @param amountToOpen The number of packs to open at once. */ - function openPack(uint256 packId, uint256 amountToOpen) external; + function openPack(uint256 packId, uint256 amountToOpen) external returns (Token[] memory); } diff --git a/contracts/pack/Pack.sol b/contracts/pack/Pack.sol new file mode 100644 index 000000000..1737709db --- /dev/null +++ b/contracts/pack/Pack.sol @@ -0,0 +1,404 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.11; + +// ========== External imports ========== + +import "@openzeppelin/contracts-upgradeable/token/ERC1155/extensions/ERC1155PausableUpgradeable.sol"; + +import "@openzeppelin/contracts-upgradeable/proxy/utils/Initializable.sol"; +import "@openzeppelin/contracts-upgradeable/utils/MulticallUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/security/ReentrancyGuardUpgradeable.sol"; +import "@openzeppelin/contracts-upgradeable/interfaces/IERC2981Upgradeable.sol"; + +// ========== Internal imports ========== + +import "../interfaces/IPack.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 Pack is + Initializable, + ContractMetadata, + Ownable, + Royalty, + PermissionsEnumerable, + TokenStore, + ReentrancyGuardUpgradeable, + ERC2771ContextUpgradeable, + MulticallUpgradeable, + ERC1155PausableUpgradeable, + IPack +{ + /*/////////////////////////////////////////////////////////////// + State variables + //////////////////////////////////////////////////////////////*/ + + bytes32 private constant MODULE_TYPE = bytes32("Pack"); + uint256 private constant VERSION = 1; + + // Token name + string public name; + + // Token symbol + string public symbol; + + /// @dev Only transfers to or from TRANSFER_ROLE holders are valid, when transfers are restricted. + bytes32 private constant TRANSFER_ROLE = keccak256("TRANSFER_ROLE"); + /// @dev Only MINTER_ROLE holders can create packs. + bytes32 private constant MINTER_ROLE = keccak256("MINTER_ROLE"); + /// @dev Only assets with ASSET_ROLE can be packed, when packing is restricted to particular assets. + bytes32 private constant ASSET_ROLE = keccak256("ASSET_ROLE"); + + /// @dev The token Id of the next set of packs to be minted. + uint256 public nextTokenIdToMint; + + /*/////////////////////////////////////////////////////////////// + Mappings + //////////////////////////////////////////////////////////////*/ + + /// @dev Mapping from token ID => total circulating supply of token with that ID. + mapping(uint256 => uint256) public totalSupply; + + /// @dev Mapping from pack ID => The state of that set of packs. + mapping(uint256 => PackInfo) private packInfo; + + /*/////////////////////////////////////////////////////////////// + Constructor + initializer logic + //////////////////////////////////////////////////////////////*/ + + constructor(address _nativeTokenWrapper) TokenStore(_nativeTokenWrapper) initializer {} + + /// @dev Initiliazes the contract, like a constructor. + function initialize( + address _defaultAdmin, + string memory _name, + string memory _symbol, + string memory _contractURI, + address[] memory _trustedForwarders, + address _royaltyRecipient, + uint256 _royaltyBps + ) external initializer { + // Initialize inherited contracts, most base-like -> most derived. + __ReentrancyGuard_init(); + __ERC2771Context_init(_trustedForwarders); + __ERC1155Pausable_init(); + __ERC1155_init(_contractURI); + + name = _name; + symbol = _symbol; + + _setupContractURI(_contractURI); + _setupOwner(_defaultAdmin); + + _setupRole(DEFAULT_ADMIN_ROLE, _defaultAdmin); + _setupRole(TRANSFER_ROLE, _defaultAdmin); + _setupRole(MINTER_ROLE, _defaultAdmin); + _setupRole(TRANSFER_ROLE, address(0)); + + // note: see `onlyRoleWithSwitch` for ASSET_ROLE behaviour. + _setupRole(ASSET_ROLE, address(0)); + + _setupDefaultRoyaltyInfo(_royaltyRecipient, _royaltyBps); + } + + receive() external payable { + require(_msgSender() == nativeTokenWrapper, "Caller is not native token wrapper."); + } + + /*/////////////////////////////////////////////////////////////// + Modifiers + //////////////////////////////////////////////////////////////*/ + + modifier onlyRoleWithSwitch(bytes32 role) { + _checkRoleWithSwitch(role, _msgSender()); + _; + } + + /*/////////////////////////////////////////////////////////////// + Generic contract logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the type of the contract. + function contractType() external pure returns (bytes32) { + return MODULE_TYPE; + } + + /// @dev Returns the version of the contract. + function contractVersion() external pure returns (uint8) { + return uint8(VERSION); + } + + /// @dev Pauses / unpauses contract. + function pause(bool _toPause) internal onlyRole(DEFAULT_ADMIN_ROLE) { + if (_toPause) { + _pause(); + } else { + _unpause(); + } + } + + /*/////////////////////////////////////////////////////////////// + ERC 165 / 1155 / 2981 logic + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the URI for a given tokenId. + function uri(uint256 _tokenId) public view override returns (string memory) { + return getUriOfBundle(_tokenId); + } + + /// @dev See ERC 165 + function supportsInterface(bytes4 interfaceId) + public + view + virtual + override(ERC1155Receiver, ERC1155Upgradeable) + returns (bool) + { + return super.supportsInterface(interfaceId) || type(IERC2981Upgradeable).interfaceId == interfaceId; + } + + /*/////////////////////////////////////////////////////////////// + Pack logic: create | open packs. + //////////////////////////////////////////////////////////////*/ + + /// @dev Creates a pack with the stated contents. + function createPack( + Token[] calldata _contents, + uint256[] calldata _numOfRewardUnits, + string calldata _packUri, + uint128 _openStartTimestamp, + uint128 _amountDistributedPerOpen, + address _recipient + ) + external + payable + onlyRoleWithSwitch(MINTER_ROLE) + nonReentrant + whenNotPaused + returns (uint256 packId, uint256 packTotalSupply) + { + require(_contents.length > 0, "nothing to pack"); + require(_contents.length == _numOfRewardUnits.length, "invalid reward units"); + + if (!hasRole(ASSET_ROLE, address(0))) { + for (uint256 i = 0; i < _contents.length; i += 1) { + _checkRole(ASSET_ROLE, _contents[i].assetContract); + } + } + + packId = nextTokenIdToMint; + nextTokenIdToMint += 1; + + packTotalSupply = escrowPackContents(_contents, _numOfRewardUnits, _packUri, packId, _amountDistributedPerOpen); + + packInfo[packId].openStartTimestamp = _openStartTimestamp; + packInfo[packId].amountDistributedPerOpen = _amountDistributedPerOpen; + + _mint(_recipient, packId, packTotalSupply, ""); + + emit PackCreated(packId, _msgSender(), _recipient, packTotalSupply); + } + + /// @notice Lets a pack owner open packs and receive the packs' reward units. + function openPack(uint256 _packId, uint256 _amountToOpen) + external + nonReentrant + whenNotPaused + returns (Token[] memory) + { + address opener = _msgSender(); + + require(opener == tx.origin, "opener must be eoa"); + require(balanceOf(opener, _packId) >= _amountToOpen, "opening more than owned"); + + PackInfo memory pack = packInfo[_packId]; + require(pack.openStartTimestamp < block.timestamp, "cannot open yet"); + + Token[] memory rewardUnits = getRewardUnits(_packId, _amountToOpen, pack.amountDistributedPerOpen, pack); + + _burn(_msgSender(), _packId, _amountToOpen); + + _transferTokenBatch(address(this), _msgSender(), rewardUnits); + + emit PackOpened(_packId, _msgSender(), _amountToOpen, rewardUnits); + + return rewardUnits; + } + + function escrowPackContents( + Token[] calldata _contents, + uint256[] calldata _numOfRewardUnits, + string calldata _packUri, + uint256 packId, + uint256 amountPerOpen + ) internal returns (uint256 packTotalSupply) { + uint256 totalRewardUnits; + + for (uint256 i = 0; i < _contents.length; i += 1) { + require(_contents[i].totalAmount % _numOfRewardUnits[i] == 0, "invalid reward units"); + require( + _contents[i].tokenType != TokenType.ERC721 || _contents[i].totalAmount == 1, + "invalid erc721 rewards" + ); + + totalRewardUnits += _numOfRewardUnits[i]; + + packInfo[packId].perUnitAmounts.push(_contents[i].totalAmount / _numOfRewardUnits[i]); + } + + require(totalRewardUnits % amountPerOpen == 0, "invalid amount to distribute per open"); + packTotalSupply = totalRewardUnits / amountPerOpen; + + _storeTokens(_msgSender(), _contents, _packUri, packId); + } + + /// @dev Returns the reward units to distribute. + function getRewardUnits( + uint256 _packId, + uint256 _numOfPacksToOpen, + uint256 _rewardUnitsPerOpen, + PackInfo memory pack + ) internal returns (Token[] memory rewardUnits) { + uint256 numOfRewardUnitsToDistribute = _numOfPacksToOpen * _rewardUnitsPerOpen; + rewardUnits = new Token[](numOfRewardUnitsToDistribute); + + uint256 totalRewardUnits = totalSupply[_packId] * _rewardUnitsPerOpen; + uint256 totalRewardKinds = getTokenCountOfBundle(_packId); + + uint256 random = generateRandomValue(); + + for (uint256 i = 0; i < numOfRewardUnitsToDistribute; i += 1) { + uint256 randomVal = uint256(keccak256(abi.encode(random, i))); + uint256 target = randomVal % totalRewardUnits; + uint256 step; + + for (uint256 j = 0; j < totalRewardKinds; j += 1) { + uint256 id = _packId; + + Token memory _token = getTokenOfBundle(id, j); + uint256 totalRewardUnitsOfKind = _token.totalAmount / pack.perUnitAmounts[j]; + + if (target < step + totalRewardUnitsOfKind) { + _token.totalAmount -= pack.perUnitAmounts[j]; + _updateTokenInBundle(_token, id, j); + rewardUnits[i] = _token; + rewardUnits[i].totalAmount = pack.perUnitAmounts[j]; + + totalRewardUnits -= 1; + + break; + } else { + step += totalRewardUnitsOfKind; + } + } + } + } + + /*/////////////////////////////////////////////////////////////// + Getter functions + //////////////////////////////////////////////////////////////*/ + + /// @dev Returns the underlying contents of a pack. + function getPackContents(uint256 _packId) + external + view + returns (Token[] memory contents, uint256[] memory perUnitAmounts) + { + PackInfo memory pack = packInfo[_packId]; + uint256 total = getTokenCountOfBundle(_packId); + contents = new Token[](total); + perUnitAmounts = new uint256[](total); + + for (uint256 i = 0; i < total; i += 1) { + contents[i] = getTokenOfBundle(_packId, i); + perUnitAmounts[i] = pack.perUnitAmounts[i]; + } + } + + /*/////////////////////////////////////////////////////////////// + Internal functions + //////////////////////////////////////////////////////////////*/ + + /// @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 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 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()); + } + + /*/////////////////////////////////////////////////////////////// + Miscellaneous + //////////////////////////////////////////////////////////////*/ + + function generateRandomValue() internal view returns (uint256 random) { + random = uint256(keccak256(abi.encodePacked(_msgSender(), blockhash(block.number - 1), block.difficulty))); + } + + /** + * @dev See {ERC1155-_beforeTokenTransfer}. + */ + function _beforeTokenTransfer( + address operator, + address from, + address to, + uint256[] memory ids, + uint256[] memory amounts, + bytes memory data + ) internal virtual override { + super._beforeTokenTransfer(operator, from, to, ids, amounts, data); + + // 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."); + } + + if (from == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] += amounts[i]; + } + } + + if (to == address(0)) { + for (uint256 i = 0; i < ids.length; ++i) { + totalSupply[ids[i]] -= amounts[i]; + } + } + } + + /// @dev See EIP-2771 + function _msgSender() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (address sender) + { + return ERC2771ContextUpgradeable._msgSender(); + } + + /// @dev See EIP-2771 + function _msgData() + internal + view + virtual + override(ContextUpgradeable, ERC2771ContextUpgradeable) + returns (bytes calldata) + { + return ERC2771ContextUpgradeable._msgData(); + } +} diff --git a/contracts/pack/pack.md b/contracts/pack/pack.md new file mode 100644 index 000000000..7533c69ae --- /dev/null +++ b/contracts/pack/pack.md @@ -0,0 +1,239 @@ +# Pack design document. + +This is a live document that explains what the [thirdweb](https://thirdweb.com/) `Pack` 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 `Pack` contract, please join the [thirdweb discord](https://discord.gg/thirdweb) or create a github issue. + +# Background + +The thirdweb `Pack` contract is a lootbox mechanism. An account can bundle up arbitrary ERC20, ERC721 and ERC1155 tokens into a set of packs. A pack can then be opened in return for a selection of the tokens in the pack. The selection of tokens distributed on opening a pack depends on the relative supply of all tokens in the packs. + +## Product: How packs *should* work (without web3 terminology) + +Let's say we want to create a set of packs with three kinds of rewards - 80 **circles**, 15 **squares**, and 5 **stars** — and we want exactly 1 reward to be distributed when a pack is opened. + +In this case, with thirdweb’s `Pack` contract, each pack is guaranteed to yield exactly 1 reward. To deliver this guarantee, the number of packs created is equal to the sum of the supplies of each reward. So, we now have `80 + 15 + 5` i.e. `100` packs at hand. + +![pack-diag-1.png](/assets/pack-diag-1.png) + +On opening one of these 100 packs, the opener will receive one of the pack's rewards - either a **circle**, a **square**, or a **star**. The chances of receiving a particular reward is determined by how many of that reward exists across our set of packs. + +The percentage chance of receiving a particular kind of reward (e.g. a **star**) on opening a pack is calculated as:`(number_of_stars_packed) / (total number of packs)` + +In the beginning, 80 **circles**, 15 **squares**, and 5 **stars** exist across our set of 100 packs. That means the chances of receiving a **circle** upon opening a pack is `80/100` i.e. 80%. Similarly, a pack opener stands a 15% chance of receiving a **square**, and a 5% chance of receiving a **star** upon opening a pack. + +![pack-diag-2.png](/assets/pack-diag-2.png) + +The chances of receiving each kind of reward change as packs are opened. Let's say one of our 100 packs is opened, yielding a **circle**. We then have 99 packs remaining, with *79* **circles**, 15 **squares**, and 5 **stars** packed. + +For the next pack that is opened, the opener will have a `79/99` i.e. around 79.8% chance of receiving a **circle**, around 15.2% chance of receiving a **square**, and around 5.1% chance of receiving a **star**. + +### Core parts of `Pack` as a product + +Given the above illustration of ‘how packs *should* work’, we can now note down certain core parts of the `Pack` product, that any implementation of `Pack` should maintain: + +- A creator can pack arbitrary ERC20, ERC721 and ERC1155 tokens into a set of packs. +- The % chance of receiving a particular reward on opening a pack should be a function of the relative supplies of the rewards within a pack. That is, opening a pack *should not* be like a lottery, where there’s an unchanging % chance of being distributed, assigned to rewards in a set of packs. +- A pack opener *should not* be able to tell beforehand what reward they’ll receive on opening a pack. +- Each pack in a set of packs can be opened whenever the respective pack owner chooses to open the pack. +- Packs must be capable of being transferred and sold on a marketplace. + +## Why we’re building `Pack` + +Packs are designed to work as generic packs that contain rewards in them, where a pack can be opened to retrieve the rewards in that pack. + +Packs like these already exist as e.g. regular [Pokemon card packs](https://www.pokemoncenter.com/category/booster-packs), or in other forms that use blockchain technology, like [NBA Topshot](https://nbatopshot.com/) packs. This concept is ubiquitous across various cultures, sectors and products. + +As tokens continue to get legitimized as assets / items, we’re bringing ‘packs’ — a long-standing way of gamifying distribution of items — on-chain, as a primitive with a robust implementation that can be used across all chains, and for all kinds of use cases. + +# Technical details + +We’ll now go over the technical details of the `Pack` contract, with references to the example given in the previous section — ‘How packs work (without web3 terminology)’. + +## What can be packed in packs? + +You can create a set of packs with any combination of any number of ERC20, ERC721 and ERC1155 tokens. For example, you can create a set of packs with 10,000 [USDC](https://www.circle.com/en/usdc) (ERC20), 1 [Bored Ape Yatch Club](https://opensea.io/collection/boredapeyachtclub) NFT (ERC721), and 50 of [adidas originals’ first NFT](https://opensea.io/assets/0x28472a58a490c5e09a238847f66a68a47cc76f0f/0) (ERC1155). + +With strictly non-fungible tokens i.e. ERC721 NFTs, each NFT has a supply of 1. This means if a pack is opened and an ERC721 NFT is selected by the `Pack` contract to be distributed to the opener, that 1 NFT will be distributed to the opener. + +With fungible (ERC20) and semi-fungible (ERC1155) tokens, you must specify how many of those tokens must be distributed on opening a pack, as a unit. For example, if adding 10,000 USDC to a pack, you may specify that 20 USDC, as a unit, are meant to be distributed on opening a pack. This means you’re adding 500 units of 20 USDC to the set of packs you’re creating. + +And so, what can be packed in packs are *n* number of configurations like ‘500 units of 20 USDC’. These configurations are interpreted by the `Pack` contract as `PackContent`: + +```solidity +enum TokenType { ERC20, ERC721, ERC1155 } + +struct PackContent { + address assetContract; + TokenType tokenType; + uint256 tokenId; + uint256 totalAmountPacked; + uint256 amountDistributedPerOpen; +} +``` + +| Value | Description | +| --- | --- | +| assetContract | The contract address of the token. | +| tokenType | The type of the token -- ERC20 / ERC721 / ERC1155 | +| tokenId | The tokenId of the the token. (Not applicable for ERC20 tokens. The contract will ignore this value for ERC20 tokens.) | +| totalAmountPacked | The total amount of this token packed in the pack. (Not applicable for ERC721 tokens. The contract will always consider this as 1 for ERC721 tokens.) | +| amountDistributedPerOpen | The amount of this token to distribute as a unit, on opening a pack. (Not applicable for ERC721 tokens. The contract will always consider this as 1 for ERC721 tokens.) | + +**Note:** A pack can contain different configurations for the same token. For example, the same set of packs can contain ‘500 units of 20 USDC’ and ‘10 units of 1000 USDC’ as two independent types of underlying rewards. + +## Creating packs + +You can create packs with any ERC20, ERC721 or ERC1155 tokens that you own. To create packs, you must specify the following: + +```solidity +function createPack( + PackContent[] calldata contents, + string calldata packUri, + uint128 openStartTimestamp, + uint128 amountDistributedPerOpen +) external; +``` + +| Parameter | Description | +| --- | --- | +| contents | The reward units packed in the packs. | +| packUri | The (metadata) URI assigned to the packs created. | +| openStartTimestamp | The timestamp after which packs can be opened. | +| amountDistributedPerOpen | The number of reward units distributed per open. | + +### Packs are ERC1155 tokens i.e. NFTs + +Packs themselves are ERC1155 tokens. And so, a set of packs created with your tokens is itself identified by a unique tokenId, has an associated metadata URI and a variable supply. + +In the example given in the previous section — ‘How packs work (without web3 terminology)’, there is a set of 100 packs created, where that entire set of packs is identified by a unique tokenId. + +Since packs are ERC1155 tokens, you can publish multiple sets of packs using the same `Pack` contract. + +### Supply of packs + +When creating packs, you can specify the numer of reward units to distribute to the opener on opening a pack. And so, when creating a set of packs, the total number of packs in that set is calculated as: + +`total_supply_of_packs = (total_reward_units) / (reward_units_to_distribute_per_open)` + +This guarantees that each pack can be opened to retrieve the intended *n* reward units from inside the set of packs. + +## Opening packs + +Packs can be opened by owners of packs. A pack owner can open multiple packs at once. ‘Opening a pack’ essentially means burning the pack and receiving the intended *n* number of reward units from inside the set of packs, in exchange. + +```solidity +function openPack(uint256 packId, uint256 amountToOpen) external; +``` + +| Parameter | Description | +| --- | --- | +| packId | The identifier of the pack to open. | +| amountToOpen | The number of packs to open at once. | + +### How reward units are selected to distribute on opening packs + +We build on the example in the previous section — ‘How packs work (without web3 terminology)’. + +Each single **square**, **circle** or **star** is considered as a ‘reward unit’. For example, the 5 **stars** in the packs may be “5 units of 1000 USDC”, which is represented in the `Pack` contract as a single `PackContent` as follows: + +```solidity +struct PackContent { + address assetContract; // USDC address + TokenType tokenType; // TokenType.ERC20 + uint256 tokenId; // Not applicable + uint256 totalAmountPacked; // 5000 + uint256 amountDistributedPerOpen; // 1000 +} +``` + +The percentage chance of receiving a particular kind of reward (e.g. a **star**) on opening a pack is calculated as:`(number_of_stars_packed) / (total number of packs)`. Here, `number_of_stars_packed` refers to the total number of reward units of the **star** kind inside the set of packs e.g. a total of 5 units of 1000 USDC. + +Going back to the example in the previous section — ‘How packs work (without web3 terminology)’. — the supply of the reward units in the relevant set of packs - 80 **circles**, 15 **squares**, and 5 **stars -** can be represented on a number line, from zero to the total supply of packs - in this case, 100. + +![pack-diag-2.png](/assets/pack-diag-2.png) + +Whenever a pack is opened, the `Pack` contract uses a new *random* number in the range of the total supply of packs to determine what reward unit will be distributed to the pack opener. + +In our example case, the `Pack` contract uses a random number less than 100 to determine whether the pack opener will receive a **circle**, **square** or a **star**. + +So e.g. if the random number `num` is such that `0 <= num < 5`, the pack opener will receive a **star**. Similarly, if `5 <= num < 20`, the opener will receive a **square**, and if `20 <= num < 100`, the opener will receive a **circle**. + +Note that given this design, the opener truly has a 5% chance of receiving a **star**, a 15% chance of receiving a **square**, and an 80% chance of receiving a **circle**, as long as the random number used in the selection of the reward unit(s) to distribute is truly random. + +## The problem with random numbers + +From the previous section — ‘How reward units are selected to distribute on opening packs’: + +> Note that given this design, the opener truly has a 5% chance of receiving a **star**, a 15% chance of receiving a **square**, and an 80% chance of receiving a **circle**, as long as the random number used in the selection of the reward unit(s) to distribute is truly random. +> + +In the event of a pack opening, the random number used in the process affects what unit of reward is selected by the `Pack` contract to be distributed to the pack owner. + +If a pack owner can predict, at any moment, what random number will be used in this process of the contract selecting what unit of reward to distribute on opening a pack at that moment, the pack owner can selectively open their pack at a moment where they’ll receive the reward they want from the pack. + +This is a **possible** **critical vulnerability** since a core feature of the `Pack` product offering is the guarantee that each reward unit in a pack has a % probability of being distributed on opening a pack, and that this probability has some integrity (in the common sense way). Being able to predict the random numbers, as described above, overturns this guarantee. + +### Sourcing random numbers — solution + +The `Pack` contract requires a design where a pack owner *cannot possibly* predict the random number that will be used in the process of their pack opening. + +To ensure the above, we make a simple check in the `openPack` function: + +```solidity +require(msg.sender == tx.origin, "opener cannot be smart contract"); + +require(_msgSender() == tx.origin, "opener cannot be smart contract"); +``` + +`tx.origin` returns the address of the external account that initiated the transaction, of which the `openPack` function call is a part of. + +The above check essentially means that only an external account i.e. an end user wallet, and no smart contract, can open packs. This lets us generate a pseudo random number using block variables, for the purpose of `openPack`: + +```solidity +uint256 random = uint(keccak256(abi.encodePacked(msg.sender, blockhash(block.number), block.difficulty))); +``` + +Since only end user wallets can open packs, a pack owner *cannot possibly* predict the random number that will be used in the process of their pack opening. That is because a pack opener cannot query the result of the random number calculation during a given block, and call `openPack` within that same block. + +We now list the single most important advantage, and consequent trade-off of using this solution: + +| Advantage | Trade-off | +| --- | --- | +| A pack owner cannot possibly predict the random number that will be used in the process of their pack opening. | Only external accounts / EOAs can open packs. Smart contracts cannot open packs. | + +### Sourcing random numbers — discarded solutions + + We’ll now discuss some possible solutions for this design problem along with their trade-offs / why we do not use these solutions: + +- **Using an oracle (e.g. Chainlink VRF)** + + Using an oracle like Chainlink VRF enables the original design for the `Pack` contract: a pack owner can open *n* number of packs, whenever they want, independent of when the other pack owners choose to open their own packs. All in all — opening *n* packs becomes a closed isolated event performed by a single pack owner. + + ![pack-diag-3.png](/assets/pack-diag-3.png) + + **Why we’re not using this solution:** + + - Chainlink VRF v1 is only on Ethereum and Polygon, and Chainlink VRF v2 (current version) is only on Ethereum and Binance. As a result, this solution cannot be used by itself across all the chains thirdweb supports (and wants to support). + - Each random number request costs an end user Chainlink’s LINK token — it is costly, and seems like a random requirement for using a thirdweb offering. + +- **Delayed-reveal randomness: rewards for all packs in a set of packs visible all at once** + + By ‘delayed-reveal’ randomness, we mean the following — + + - When creating a set of packs, the creator provides (1) an encrypted seed i.e. integer (see the [encryption pattern used in thirdweb’s delayed-reveal NFTs](https://blog.thirdweb.com/delayed-reveal-nfts#step-1-encryption)), and (2) a future block number. + - The created packs are *non-transferrable* by any address except the (1) pack creator, or (2) addresses manually approved by the pack creator. This is to let the creator distribute packs as they desire, *and* is essential for the next step. + - After the specified future block number passes, the creator submits the unencrypted seed to the `Pack` contract. Whenever a pack owner now opens a pack, we calculate the random number to be used in the opening process as follows: + + ```solidity + uint256 random = uint(keccak256(seed, msg.sender, blockhash(storedBlockNumber))); + ``` + + - No one can predict the block hash of the stored future block unless the pack creator is the miner of the block with that block number (highly unlikely). + - The seed is controlled by the creator, submitted at the time of pack creation, and cannot be changed after submission. + - Since packs are non-transferrable in the way described above, as long as the pack opener is not approved to transfer packs, the opener cannot manipulate the value of `random` by transferring packs to a desirable address and then opening the pack from that address. + + **Why we’re not using this solution:** + + - Active involvement from the pack creator. They’re trusted to reveal the unencrypted seed once packs are eligible to be opened. + - Packs *must* be non-transferrable in the way described above, which means they can’t be purchased on a marketplace, etc. Lack of a built-in distribution mechanism for the packs. \ No newline at end of file diff --git a/contracts/package.json b/contracts/package.json index ca152e9a5..88e77f5f2 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.14", + "version": "2.3.15-0", "license": "Apache-2.0", "repository": { "type": "git", diff --git a/docs/IDropSinglePhase.md b/docs/IDropSinglePhase.md deleted file mode 100644 index 360ec69a8..000000000 --- a/docs/IDropSinglePhase.md +++ /dev/null @@ -1,92 +0,0 @@ -# IDropSinglePhase - - - - - - - - - -## Methods - -### claim - -```solidity -function claim(address receiver, uint256 quantity, address currency, uint256 pricePerToken, IDropSinglePhase.AllowlistProof allowlistProof, bytes data) external payable -``` - -Lets an account claim a given quantity of NFTs. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| receiver | address | The receiver of the NFTs to claim. -| quantity | uint256 | The quantity of NFTs to claim. -| currency | address | The currency in which to pay for the claim. -| pricePerToken | uint256 | The price per token to pay for the claim. -| allowlistProof | IDropSinglePhase.AllowlistProof | The proof of the claimer's inclusion in the merkle root allowlist of the claim conditions that apply. -| data | bytes | Arbitrary bytes data that can be leveraged in the implementation of this interface. - -### setClaimConditions - -```solidity -function setClaimConditions(IClaimCondition.ClaimCondition phase, bool resetClaimEligibility) external nonpayable -``` - -Lets a contract admin (account with `DEFAULT_ADMIN_ROLE`) set claim conditions. - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| phase | IClaimCondition.ClaimCondition | Claim condition to set. -| resetClaimEligibility | bool | Whether to reset `limitLastClaimTimestamp` and `limitMerkleProofClaim` values when setting new claim conditions. - - - -## Events - -### ClaimConditionUpdated - -```solidity -event ClaimConditionUpdated(IClaimCondition.ClaimCondition condition, bool resetEligibility) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| condition | IClaimCondition.ClaimCondition | undefined | -| resetEligibility | bool | undefined | - -### TokensClaimed - -```solidity -event TokensClaimed(address indexed claimer, address indexed receiver, uint256 startTokenId, uint256 quantityClaimed) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| claimer `indexed` | address | undefined | -| receiver `indexed` | address | undefined | -| startTokenId | uint256 | undefined | -| quantityClaimed | uint256 | undefined | - - - diff --git a/docs/IPack.md b/docs/IPack.md index 4bbe435e3..32df02d77 100644 --- a/docs/IPack.md +++ b/docs/IPack.md @@ -4,67 +4,16 @@ - +The thirdweb `Pack` contract is a lootbox mechanism. An account can bundle up arbitrary ERC20, ERC721 and ERC1155 tokens into a set of packs. A pack can then be opened in return for a selection of the tokens in the pack. The selection of tokens distributed on opening a pack depends on the relative supply of all tokens in the packs. ## Methods -### contractType - -```solidity -function contractType() external pure returns (bytes32) -``` - - - -*Returns the module type of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -### contractURI - -```solidity -function contractURI() external view returns (string) -``` - - - -*Returns the metadata URI of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | string | undefined - -### contractVersion - -```solidity -function contractVersion() external pure returns (uint8) -``` - - - -*Returns the version of the contract.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | uint8 | undefined - ### createPack ```solidity -function createPack(IPack.PackContents contents, string uri, uint128 openStartTimestamp, uint128 nftsPerOpen) external nonpayable +function createPack(ITokenBundle.Token[] contents, uint256[] numOfRewardUnits, string packUri, uint128 openStartTimestamp, uint128 amountDistributedPerOpen, address recipient) external payable returns (uint256 packId, uint256 packTotalSupply) ``` Creates a pack with the stated contents. @@ -75,59 +24,27 @@ Creates a pack with the stated contents. | Name | Type | Description | |---|---|---| -| contents | IPack.PackContents | The contents of the packs to be created. -| uri | string | The (metadata) URI assigned to the packs created. -| openStartTimestamp | uint128 | The timestamp after which a pack is opened. -| nftsPerOpen | uint128 | The number of NFTs received on opening one pack. - -### getDefaultRoyaltyInfo - -```solidity -function getDefaultRoyaltyInfo() external view returns (address, uint16) -``` - - - -*Returns the royalty recipient and fee bps.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined - -### getRoyaltyInfoForToken - -```solidity -function getRoyaltyInfoForToken(uint256 tokenId) external view returns (address, uint16) -``` - - - -*Returns the royalty recipient for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined +| contents | ITokenBundle.Token[] | The reward units to pack in the packs. +| numOfRewardUnits | uint256[] | The number of reward units to create, for each asset specified in `contents`. +| packUri | string | The (metadata) URI assigned to the packs created. +| openStartTimestamp | uint128 | The timestamp after which packs can be opened. +| amountDistributedPerOpen | uint128 | The number of reward units distributed per open. +| recipient | address | The recipient of the packs created. #### Returns | Name | Type | Description | |---|---|---| -| _0 | address | undefined -| _1 | uint16 | undefined +| packId | uint256 | The unique identifer of the created set of packs. +| packTotalSupply | uint256 | The total number of packs created. ### openPack ```solidity -function openPack(uint256 packId, uint256 amountToOpen) external nonpayable +function openPack(uint256 packId, uint256 amountToOpen) external nonpayable returns (struct ITokenBundle.Token[]) ``` -Lets a pack owner open a pack and receive the pack's NFTs. +Lets a pack owner open a pack and receive the pack's reward unit. @@ -138,125 +55,23 @@ Lets a pack owner open a pack and receive the pack's NFTs. | packId | uint256 | The identifier of the pack to open. | amountToOpen | uint256 | The number of packs to open at once. -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the owner of the contract.* - - #### Returns | Name | Type | Description | |---|---|---| -| _0 | address | undefined - -### royaltyInfo - -```solidity -function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns (address receiver, uint256 royaltyAmount) -``` - - - -*Returns how much royalty is owed and to whom, based on a sale price that may be denominated in any unit of exchange. The royalty amount is denominated and should be payed in that same unit of exchange.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| salePrice | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| receiver | address | undefined -| royaltyAmount | uint256 | undefined - -### setContractURI - -```solidity -function setContractURI(string _uri) external nonpayable -``` - - - -*Sets contract URI for the storefront-level metadata of the contract. Only module admin can call this function.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _uri | string | undefined - -### setDefaultRoyaltyInfo - -```solidity -function setDefaultRoyaltyInfo(address _royaltyRecipient, uint256 _royaltyBps) external nonpayable -``` - - - -*Lets a module admin update the royalty bps and recipient.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _royaltyRecipient | address | undefined -| _royaltyBps | uint256 | undefined - -### setOwner - -```solidity -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.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newOwner | address | undefined - -### setRoyaltyInfoForToken - -```solidity -function setRoyaltyInfoForToken(uint256 tokenId, address recipient, uint256 bps) external nonpayable -``` - - - -*Lets a module admin set the royalty recipient for a particular token Id.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| tokenId | uint256 | undefined -| recipient | address | undefined -| bps | uint256 | undefined +| _0 | ITokenBundle.Token[] | undefined ## Events -### DefaultRoyalty +### PackCreated ```solidity -event DefaultRoyalty(address newRoyaltyRecipient, uint256 newRoyaltyBps) +event PackCreated(uint256 indexed packId, address indexed packCreator, address recipient, uint256 totalPacksCreated) ``` - +Emitted when a set of packs is created. @@ -264,33 +79,18 @@ event DefaultRoyalty(address newRoyaltyRecipient, uint256 newRoyaltyBps) | Name | Type | Description | |---|---|---| -| newRoyaltyRecipient | address | undefined | -| newRoyaltyBps | uint256 | undefined | +| packId `indexed` | uint256 | undefined | +| packCreator `indexed` | address | undefined | +| recipient | address | undefined | +| totalPacksCreated | uint256 | undefined | -### OwnerUpdated +### PackOpened ```solidity -event OwnerUpdated(address prevOwner, address newOwner) +event PackOpened(uint256 indexed packId, address indexed opener, uint256 numOfPacksOpened, ITokenBundle.Token[] rewardUnitsDistributed) ``` - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| prevOwner | address | undefined | -| newOwner | address | undefined | - -### RoyaltyForToken - -```solidity -event RoyaltyForToken(uint256 indexed tokenId, address royaltyRecipient, uint256 royaltyBps) -``` - - +Emitted when a pack is opened. @@ -298,9 +98,10 @@ event RoyaltyForToken(uint256 indexed tokenId, address royaltyRecipient, uint256 | Name | Type | Description | |---|---|---| -| tokenId `indexed` | uint256 | undefined | -| royaltyRecipient | address | undefined | -| royaltyBps | uint256 | undefined | +| packId `indexed` | uint256 | undefined | +| opener `indexed` | address | undefined | +| numOfPacksOpened | uint256 | undefined | +| rewardUnitsDistributed | ITokenBundle.Token[] | undefined | diff --git a/docs/LinkTokenInterface.md b/docs/LinkTokenInterface.md deleted file mode 100644 index 5fa953678..000000000 --- a/docs/LinkTokenInterface.md +++ /dev/null @@ -1,262 +0,0 @@ -# LinkTokenInterface - - - - - - - - - -## Methods - -### allowance - -```solidity -function allowance(address owner, address spender) external view returns (uint256 remaining) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined -| spender | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| remaining | uint256 | undefined - -### approve - -```solidity -function approve(address spender, uint256 value) external nonpayable returns (bool success) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| value | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined - -### balanceOf - -```solidity -function balanceOf(address owner) external view returns (uint256 balance) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| owner | address | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| balance | uint256 | undefined - -### decimals - -```solidity -function decimals() external view returns (uint8 decimalPlaces) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| decimalPlaces | uint8 | undefined - -### decreaseApproval - -```solidity -function decreaseApproval(address spender, uint256 addedValue) external nonpayable returns (bool success) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| addedValue | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined - -### increaseApproval - -```solidity -function increaseApproval(address spender, uint256 subtractedValue) external nonpayable -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| spender | address | undefined -| subtractedValue | uint256 | undefined - -### name - -```solidity -function name() external view returns (string tokenName) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| tokenName | string | undefined - -### symbol - -```solidity -function symbol() external view returns (string tokenSymbol) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| tokenSymbol | string | undefined - -### totalSupply - -```solidity -function totalSupply() external view returns (uint256 totalTokensIssued) -``` - - - - - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| totalTokensIssued | uint256 | undefined - -### transfer - -```solidity -function transfer(address to, uint256 value) external nonpayable returns (bool success) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| value | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined - -### transferAndCall - -```solidity -function transferAndCall(address to, uint256 value, bytes data) external nonpayable returns (bool success) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| to | address | undefined -| value | uint256 | undefined -| data | bytes | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined - -### transferFrom - -```solidity -function transferFrom(address from, address to, uint256 value) external nonpayable returns (bool success) -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| from | address | undefined -| to | address | undefined -| value | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| success | bool | undefined - - - - diff --git a/docs/Multiwrap.md b/docs/Multiwrap.md index 332f7930c..5a47e3747 100644 --- a/docs/Multiwrap.md +++ b/docs/Multiwrap.md @@ -27,6 +27,23 @@ function DEFAULT_ADMIN_ROLE() external view returns (bytes32) |---|---|---| | _0 | bytes32 | undefined +### NATIVE_TOKEN + +```solidity +function NATIVE_TOKEN() external view returns (address) +``` + + + + + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined + ### approve ```solidity diff --git a/docs/Pack.md b/docs/Pack.md index feb8f59c7..c682d8297 100644 --- a/docs/Pack.md +++ b/docs/Pack.md @@ -27,87 +27,68 @@ function DEFAULT_ADMIN_ROLE() external view returns (bytes32) |---|---|---| | _0 | bytes32 | undefined -### balanceOf +### NATIVE_TOKEN ```solidity -function balanceOf(address account, uint256 id) external view returns (uint256) +function NATIVE_TOKEN() external view returns (address) ``` -*See {IERC1155-balanceOf}. Requirements: - `account` cannot be the zero address.* -#### Parameters -| Name | Type | Description | -|---|---|---| -| account | address | undefined -| id | uint256 | undefined #### Returns | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined +| _0 | address | undefined -### balanceOfBatch +### balanceOf ```solidity -function balanceOfBatch(address[] accounts, uint256[] ids) external view returns (uint256[]) +function balanceOf(address account, uint256 id) external view returns (uint256) ``` -*See {IERC1155-balanceOfBatch}. Requirements: - `accounts` and `ids` must have the same length.* +*See {IERC1155-balanceOf}. Requirements: - `account` cannot be the zero address.* #### Parameters | Name | Type | Description | |---|---|---| -| accounts | address[] | undefined -| ids | uint256[] | undefined +| account | address | undefined +| id | uint256 | undefined #### Returns | Name | Type | Description | |---|---|---| -| _0 | uint256[] | undefined +| _0 | uint256 | undefined -### burn +### balanceOfBatch ```solidity -function burn(address account, uint256 id, uint256 value) external nonpayable +function balanceOfBatch(address[] accounts, uint256[] ids) external view returns (uint256[]) ``` - +*See {IERC1155-balanceOfBatch}. Requirements: - `accounts` and `ids` must have the same length.* #### Parameters | Name | Type | Description | |---|---|---| -| account | address | undefined -| id | uint256 | undefined -| value | uint256 | undefined - -### burnBatch - -```solidity -function burnBatch(address account, uint256[] ids, uint256[] values) external nonpayable -``` - - - - +| accounts | address[] | undefined +| ids | uint256[] | undefined -#### Parameters +#### Returns | Name | Type | Description | |---|---|---| -| account | address | undefined -| ids | uint256[] | undefined -| values | uint256[] | undefined +| _0 | uint256[] | undefined ### contractType @@ -117,7 +98,7 @@ function contractType() external pure returns (bytes32) -*Returns the module type of the contract.* +*Returns the type of the contract.* #### Returns @@ -134,7 +115,7 @@ function contractURI() external view returns (string) -*Collection level metadata.* + #### Returns @@ -160,28 +141,33 @@ function contractVersion() external pure returns (uint8) |---|---|---| | _0 | uint8 | undefined -### currentRequestId +### createPack ```solidity -function currentRequestId(uint256, address) external view returns (bytes32) +function createPack(ITokenBundle.Token[] _contents, uint256[] _numOfRewardUnits, string _packUri, uint128 _openStartTimestamp, uint128 _amountDistributedPerOpen, address _recipient) external payable returns (uint256 packId, uint256 packTotalSupply) ``` -*pack tokenId => pack opener => Chainlink VRF request ID if there is an incomplete pack opening process.* +*Creates a pack with the stated contents.* #### Parameters | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined -| _1 | address | undefined +| _contents | ITokenBundle.Token[] | undefined +| _numOfRewardUnits | uint256[] | undefined +| _packUri | string | undefined +| _openStartTimestamp | uint128 | undefined +| _amountDistributedPerOpen | uint128 | undefined +| _recipient | address | undefined #### Returns | Name | Type | Description | |---|---|---| -| _0 | bytes32 | undefined +| packId | uint256 | undefined +| packTotalSupply | uint256 | undefined ### getDefaultRoyaltyInfo @@ -191,7 +177,7 @@ function getDefaultRoyaltyInfo() external view returns (address, uint16) -*Returns the platform fee bps and recipient.* +*Returns the default royalty recipient and bps.* #### Returns @@ -201,15 +187,15 @@ function getDefaultRoyaltyInfo() external view returns (address, uint16) | _0 | address | undefined | _1 | uint16 | undefined -### getPackWithRewards +### getPackContents ```solidity -function getPackWithRewards(uint256 _packId) external view returns (struct Pack.PackState pack, uint256 packTotalSupply, address source, uint256[] tokenIds, uint256[] amountsPacked) +function getPackContents(uint256 _packId) external view returns (struct ITokenBundle.Token[] contents, uint256[] perUnitAmounts) ``` -*Returns a pack with its underlying rewards* +*Returns the underlying contents of a pack.* #### Parameters @@ -221,11 +207,8 @@ function getPackWithRewards(uint256 _packId) external view returns (struct Pack. | Name | Type | Description | |---|---|---| -| pack | Pack.PackState | undefined -| packTotalSupply | uint256 | undefined -| source | address | undefined -| tokenIds | uint256[] | undefined -| amountsPacked | uint256[] | undefined +| contents | ITokenBundle.Token[] | undefined +| perUnitAmounts | uint256[] | undefined ### getRoleAdmin @@ -235,7 +218,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 @@ -252,7 +235,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) ``` @@ -270,12 +253,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) ``` @@ -292,7 +275,7 @@ function getRoleMemberCount(bytes32 role) external view returns (uint256) | Name | Type | Description | |---|---|---| -| _0 | uint256 | undefined +| count | uint256 | undefined ### getRoyaltyInfoForToken @@ -302,7 +285,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 @@ -317,6 +300,73 @@ 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 + ### grantRole ```solidity @@ -325,7 +375,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 @@ -344,6 +394,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 | @@ -360,7 +433,7 @@ function hasRole(bytes32 role, address account) external view returns (bool) ### initialize ```solidity -function initialize(address _defaultAdmin, string _name, string _symbol, string _contractURI, address[] _trustedForwarders, address _royaltyRecipient, uint128 _royaltyBps, uint128 _fees, bytes32 _keyHash) external nonpayable +function initialize(address _defaultAdmin, string _name, string _symbol, string _contractURI, address[] _trustedForwarders, address _royaltyRecipient, uint256 _royaltyBps) external nonpayable ``` @@ -377,9 +450,7 @@ function initialize(address _defaultAdmin, string _name, string _symbol, string | _contractURI | string | undefined | _trustedForwarders | address[] | undefined | _royaltyRecipient | address | undefined -| _royaltyBps | uint128 | undefined -| _fees | uint128 | undefined -| _keyHash | bytes32 | undefined +| _royaltyBps | uint256 | undefined ### isApprovedForAll @@ -426,44 +497,6 @@ function isTrustedForwarder(address forwarder) external view returns (bool) |---|---|---| | _0 | bool | undefined -### mint - -```solidity -function mint(address, uint256, uint256, bytes) external nonpayable -``` - - - -*See {ERC1155-_mint}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint256 | undefined -| _2 | uint256 | undefined -| _3 | bytes | undefined - -### mintBatch - -```solidity -function mintBatch(address, uint256[], uint256[], bytes) external nonpayable -``` - - - -*See {ERC1155-_mintBatch}.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | address | undefined -| _1 | uint256[] | undefined -| _2 | uint256[] | undefined -| _3 | bytes | undefined - ### multicall ```solidity @@ -503,15 +536,15 @@ function name() external view returns (string) |---|---|---| | _0 | string | undefined -### nextTokenId +### nextTokenIdToMint ```solidity -function nextTokenId() external view returns (uint256) +function nextTokenIdToMint() external view returns (uint256) ``` -*The token Id of the next token to be minted.* +*The token Id of the next set of packs to be minted.* #### Returns @@ -523,22 +556,22 @@ function nextTokenId() external view returns (uint256) ### onERC1155BatchReceived ```solidity -function onERC1155BatchReceived(address _operator, address, uint256[] _ids, uint256[] _values, bytes _data) external nonpayable returns (bytes4) +function onERC1155BatchReceived(address, address, uint256[], uint256[], bytes) external nonpayable returns (bytes4) ``` -*Creates pack on receiving ERC 1155 reward tokens* + #### Parameters | Name | Type | Description | |---|---|---| -| _operator | address | undefined +| _0 | address | undefined | _1 | address | undefined -| _ids | uint256[] | undefined -| _values | uint256[] | undefined -| _data | bytes | undefined +| _2 | uint256[] | undefined +| _3 | uint256[] | undefined +| _4 | bytes | undefined #### Returns @@ -600,70 +633,42 @@ function onERC721Received(address, address, uint256, bytes) external nonpayable ### openPack ```solidity -function openPack(uint256 _packId) external nonpayable +function openPack(uint256 _packId, uint256 _amountToOpen) external nonpayable returns (struct ITokenBundle.Token[]) ``` +Lets a pack owner open packs and receive the packs' reward units. -*Lets a pack owner request to open a single pack.* #### Parameters | Name | Type | Description | |---|---|---| | _packId | uint256 | undefined - -### owner - -```solidity -function owner() external view returns (address) -``` - - - -*Returns the address of the current owner.* - +| _amountToOpen | uint256 | undefined #### Returns | Name | Type | Description | |---|---|---| -| _0 | address | undefined +| _0 | ITokenBundle.Token[] | undefined -### packs +### owner ```solidity -function packs(uint256) external view returns (string uri, address creator, uint256 openStart) +function owner() external view returns (address) ``` -*pack tokenId => The state of packs with id `tokenId`.* -#### Parameters -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined #### Returns | Name | Type | Description | |---|---|---| -| uri | string | undefined -| creator | address | undefined -| openStart | uint256 | undefined - -### pause - -```solidity -function pause() external nonpayable -``` - - - -*Pauses all token transfers. See {ERC1155Pausable} and {Pausable-_pause}. Requirements: - the caller must have the `PAUSER_ROLE`.* - +| _0 | address | undefined ### paused @@ -682,46 +687,6 @@ function paused() external view returns (bool) |---|---|---| | _0 | bool | undefined -### randomnessRequests - -```solidity -function randomnessRequests(bytes32) external view returns (uint256 packId, address opener) -``` - - - -*Chainlink VRF requestId => Chainlink VRF request state with id `requestId`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | bytes32 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| packId | uint256 | undefined -| opener | address | undefined - -### rawFulfillRandomness - -```solidity -function rawFulfillRandomness(bytes32 requestId, uint256 randomness) external nonpayable -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| requestId | bytes32 | undefined -| randomness | uint256 | undefined - ### renounceRole ```solidity @@ -730,7 +695,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 @@ -747,7 +712,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 @@ -756,29 +721,6 @@ function revokeRole(bytes32 role, address account) external nonpayable | role | bytes32 | undefined | account | address | undefined -### rewards - -```solidity -function rewards(uint256) external view returns (address source, uint256 rewardsPerOpen) -``` - - - -*pack tokenId => rewards in pack with id `tokenId`.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _0 | uint256 | undefined - -#### Returns - -| Name | Type | Description | -|---|---|---| -| source | address | undefined -| rewardsPerOpen | uint256 | undefined - ### royaltyInfo ```solidity @@ -787,7 +729,7 @@ function royaltyInfo(uint256 tokenId, uint256 salePrice) external view returns ( -*See EIP-2981* +*Returns the royalty recipient and amount, given a tokenId and sale price.* #### Parameters @@ -860,22 +802,6 @@ function setApprovalForAll(address operator, bool approved) external nonpayable | operator | address | undefined | approved | bool | undefined -### setChainlinkFees - -```solidity -function setChainlinkFees(uint256 _newFees) external nonpayable -``` - - - -*Lets a module admin change the Chainlink VRF fee.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _newFees | uint256 | undefined - ### setContractURI ```solidity @@ -884,7 +810,7 @@ function setContractURI(string _uri) external nonpayable -*Sets contract URI for the storefront-level metadata of the contract.* +*Lets a contract admin set the URI for contract-level metadata.* #### Parameters @@ -900,7 +826,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 @@ -917,7 +843,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 @@ -933,7 +859,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 @@ -951,7 +877,7 @@ function supportsInterface(bytes4 interfaceId) external view returns (bool) -*See EIP 165* +*See ERC 165* #### Parameters @@ -982,38 +908,21 @@ function symbol() external view returns (string) |---|---|---| | _0 | string | undefined -### thirdwebFee - -```solidity -function thirdwebFee() external view returns (contract ITWFee) -``` - - - -*The thirdweb contract with fee related information.* - - -#### Returns - -| Name | Type | Description | -|---|---|---| -| _0 | contract ITWFee | undefined - ### totalSupply ```solidity -function totalSupply(uint256 id) external view returns (uint256) +function totalSupply(uint256) external view returns (uint256) ``` -*Total amount of tokens in with a given id.* +*Mapping from token ID => total circulating supply of token with that ID.* #### Parameters | Name | Type | Description | |---|---|---| -| id | uint256 | undefined +| _0 | uint256 | undefined #### Returns @@ -1021,32 +930,21 @@ function totalSupply(uint256 id) external view returns (uint256) |---|---|---| | _0 | uint256 | undefined -### unpause - -```solidity -function unpause() external nonpayable -``` - - - -*Unpauses all token transfers. See {ERC1155Pausable} and {Pausable-_unpause}. Requirements: - the caller must have the `PAUSER_ROLE`.* - - ### uri ```solidity -function uri(uint256 _id) external view returns (string) +function uri(uint256 _tokenId) external view returns (string) ``` -*See EIP 1155* +*Returns the URI for a given tokenId.* #### Parameters | Name | Type | Description | |---|---|---| -| _id | uint256 | undefined +| _tokenId | uint256 | undefined #### Returns @@ -1054,23 +952,6 @@ function uri(uint256 _id) external view returns (string) |---|---|---| | _0 | string | undefined -### withdrawLink - -```solidity -function withdrawLink(address _to, uint256 _amount) external nonpayable -``` - - - -*Lets a module admin withdraw link from the contract.* - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| _to | address | undefined -| _amount | uint256 | undefined - ## Events @@ -1093,10 +974,10 @@ event ApprovalForAll(address indexed account, address indexed operator, bool app | operator `indexed` | address | undefined | | approved | bool | undefined | -### DefaultRoyalty +### ContractURIUpdated ```solidity -event DefaultRoyalty(address newRoyaltyRecipient, uint256 newRoyaltyBps) +event ContractURIUpdated(string prevURI, string newURI) ``` @@ -1107,13 +988,13 @@ event DefaultRoyalty(address newRoyaltyRecipient, uint256 newRoyaltyBps) | Name | Type | Description | |---|---|---| -| newRoyaltyRecipient | address | undefined | -| newRoyaltyBps | uint256 | undefined | +| prevURI | string | undefined | +| newURI | string | undefined | -### OwnerUpdated +### DefaultRoyalty ```solidity -event OwnerUpdated(address prevOwner, address newOwner) +event DefaultRoyalty(address newRoyaltyRecipient, uint256 newRoyaltyBps) ``` @@ -1124,59 +1005,54 @@ event OwnerUpdated(address prevOwner, address newOwner) | Name | Type | Description | |---|---|---| -| prevOwner | address | undefined | -| newOwner | address | undefined | +| newRoyaltyRecipient | address | undefined | +| newRoyaltyBps | uint256 | undefined | -### PackAdded +### OwnerUpdated ```solidity -event PackAdded(uint256 indexed packId, address indexed rewardContract, address indexed creator, uint256 packTotalSupply, Pack.PackState packState, Pack.Rewards rewards) +event OwnerUpdated(address prevOwner, address newOwner) ``` -*Emitted when a set of packs is created.* + #### Parameters | Name | Type | Description | |---|---|---| -| packId `indexed` | uint256 | undefined | -| rewardContract `indexed` | address | undefined | -| creator `indexed` | address | undefined | -| packTotalSupply | uint256 | undefined | -| packState | Pack.PackState | undefined | -| rewards | Pack.Rewards | undefined | +| prevOwner | address | undefined | +| newOwner | address | undefined | -### PackOpenFulfilled +### PackCreated ```solidity -event PackOpenFulfilled(uint256 indexed packId, address indexed opener, bytes32 requestId, address indexed rewardContract, uint256[] rewardIds) +event PackCreated(uint256 indexed packId, address indexed packCreator, address recipient, uint256 totalPacksCreated) ``` +Emitted when a set of packs is created. -*Emitted when a request to open a pack is fulfilled.* #### Parameters | Name | Type | Description | |---|---|---| | packId `indexed` | uint256 | undefined | -| opener `indexed` | address | undefined | -| requestId | bytes32 | undefined | -| rewardContract `indexed` | address | undefined | -| rewardIds | uint256[] | undefined | +| packCreator `indexed` | address | undefined | +| recipient | address | undefined | +| totalPacksCreated | uint256 | undefined | -### PackOpenRequested +### PackOpened ```solidity -event PackOpenRequested(uint256 indexed packId, address indexed opener, bytes32 requestId) +event PackOpened(uint256 indexed packId, address indexed opener, uint256 numOfPacksOpened, ITokenBundle.Token[] rewardUnitsDistributed) ``` +Emitted when a pack is opened. -*Emitted on a request to open a pack.* #### Parameters @@ -1184,7 +1060,8 @@ event PackOpenRequested(uint256 indexed packId, address indexed opener, bytes32 |---|---|---| | packId `indexed` | uint256 | undefined | | opener `indexed` | address | undefined | -| requestId | bytes32 | undefined | +| numOfPacksOpened | uint256 | undefined | +| rewardUnitsDistributed | ITokenBundle.Token[] | undefined | ### Paused diff --git a/docs/TokenStore.md b/docs/TokenStore.md index c0b95d538..f2b69f79d 100644 --- a/docs/TokenStore.md +++ b/docs/TokenStore.md @@ -10,6 +10,23 @@ ## Methods +### NATIVE_TOKEN + +```solidity +function NATIVE_TOKEN() external view returns (address) +``` + + + +*The address interpreted as native token of the chain.* + + +#### Returns + +| Name | Type | Description | +|---|---|---| +| _0 | address | undefined + ### getTokenCountOfBundle ```solidity diff --git a/docs/VRFConsumerBase.md b/docs/VRFConsumerBase.md deleted file mode 100644 index 5dd6dd4c8..000000000 --- a/docs/VRFConsumerBase.md +++ /dev/null @@ -1,32 +0,0 @@ -# VRFConsumerBase - - - - - -****************************************************************************Interface for contracts using VRF randomness ***************************************************************************** - -*PURPOSEReggie the Random Oracle (not his real job) wants to provide randomnessto Vera the verifier in such a way that Vera can be sure he's notmaking his output up to suit himself. Reggie provides Vera a public keyto which he knows the secret key. Each time Vera provides a seed toReggie, he gives back a value which is computed completelydeterministically from the seed and the secret key.Reggie provides a proof by which Vera can verify that the output wascorrectly computed once Reggie tells it to her, but without that proof,the output is indistinguishable to her from a uniform random samplefrom the output space.The purpose of this contract is to make it easy for unrelated contractsto talk to Vera the verifier about the work Reggie is doing, to providesimple access to a verifiable source of randomness. *****************************************************************************USAGECalling contracts must inherit from VRFConsumerBase, and caninitialize VRFConsumerBase's attributes in their constructor asshown:contract VRFConsumer {constructor(<other arguments>, address _vrfCoordinator, address _link)VRFConsumerBase(_vrfCoordinator, _link) public {<initialization with other arguments goes here>}}The oracle will have given you an ID for the VRF keypair they havecommitted to (let's call it keyHash), and have told you the minimum LINKprice for VRF service. Make sure your contract has sufficient LINK, andcall requestRandomness(keyHash, fee, seed), where seed is the input youwant to generate randomness from.Once the VRFCoordinator has received and validated the oracle's responseto your request, it will call your contract's fulfillRandomness method.The randomness argument to fulfillRandomness is the actual random valuegenerated from your seed.The requestId argument is generated from the keyHash and the seed bymakeRequestId(keyHash, seed). If your contract could have concurrentrequests open, you can use the requestId to track which seed isassociated with which randomness. See VRFRequestIDBase.sol for moredetails. (See "SECURITY CONSIDERATIONS" for principles to keep in mind,if your contract could have multiple requests in flight simultaneously.)Colliding `requestId`s are cryptographically impossible as long as seedsdiffer. (Which is critical to making unpredictable randomness! See thenext section.) *****************************************************************************SECURITY CONSIDERATIONSA method with the ability to call your fulfillRandomness method directlycould spoof a VRF response with any random value, so it's critical thatit cannot be directly called by anything other than this base contract(specifically, by the VRFConsumerBase.rawFulfillRandomness method).For your users to trust that your contract's random behavior is freefrom malicious interference, it's best if you can write it so that allbehaviors implied by a VRF response are executed *during* yourfulfillRandomness method. If your contract must store the response (oranything derived from it) and use it later, you must ensure that anyuser-significant behavior which depends on that stored value cannot bemanipulated by a subsequent VRF request.Similarly, both miners and the VRF oracle itself have some influenceover the order in which VRF responses appear on the blockchain, so ifyour contract could have multiple VRF requests in flight simultaneously,you must ensure that the order in which the VRF responses arrive cannotbe used to manipulate your contract's user-significant behavior.Since the ultimate input to the VRF is mixed with the block hash of theblock in which the request is made, user-provided seeds have no impacton its economic security properties. They are only included for APIcompatability with previous versions of this contract.Since the block hash of the block which contains the requestRandomnesscall is mixed into the input to the VRF *last*, a sufficiently powerfulminer could, in principle, fork the blockchain to evict the blockcontaining the request, forcing the request to be included in adifferent block with a different hash, and therefore a different inputto the VRF. However, such an attack would incur a substantial economiccost. This cost scales with the number of blocks the VRF oracle waitsuntil it calls responds to a request.* - -## Methods - -### rawFulfillRandomness - -```solidity -function rawFulfillRandomness(bytes32 requestId, uint256 randomness) external nonpayable -``` - - - - - -#### Parameters - -| Name | Type | Description | -|---|---|---| -| requestId | bytes32 | undefined -| randomness | uint256 | undefined - - - - diff --git a/docs/VRFRequestIDBase.md b/docs/VRFRequestIDBase.md deleted file mode 100644 index bf83d747f..000000000 --- a/docs/VRFRequestIDBase.md +++ /dev/null @@ -1,12 +0,0 @@ -# VRFRequestIDBase - - - - - - - - - - - diff --git a/scripts/deploy/pack.ts b/scripts/deploy/pack.ts new file mode 100644 index 000000000..535888a61 --- /dev/null +++ b/scripts/deploy/pack.ts @@ -0,0 +1,35 @@ +import hre, { ethers } from "hardhat"; +import { Pack } from "typechain"; +import { nativeTokenWrapper } from "../../utils/nativeTokenWrapper"; + +async function main() { + + const chainId: number = hre.network.config.chainId as number; + const nativeTokenWrapperAddress: string = nativeTokenWrapper[chainId]; + + const pack: Pack = await ethers.getContractFactory("Pack").then(f => f.deploy(nativeTokenWrapperAddress)); + console.log("Deploying Pack \ntransaction: ", pack.deployTransaction.hash, "\naddress: ", pack.address); + await pack.deployTransaction.wait(); + console.log("\n"); + + console.log("Verifying contract"); + await verify(pack.address, [nativeTokenWrapperAddress]); +} + +async function verify(address: string, args: any[]) { + try { + return await hre.run("verify:verify", { + address: address, + constructorArguments: args, + }); + } catch (e) { + console.log(address, args, e); + } +} + +main() +.then(() => process.exit(0)) +.catch((e) => { + console.error(e); + process.exit(1); +}) \ No newline at end of file diff --git a/src/test/Pack.t.sol b/src/test/Pack.t.sol new file mode 100644 index 000000000..cc3385a4f --- /dev/null +++ b/src/test/Pack.t.sol @@ -0,0 +1,997 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { Pack } from "contracts/pack/Pack.sol"; +import { IPack } from "contracts/interfaces/IPack.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"; + +contract PackTest is BaseTest { + /// @notice Emitted when a set of packs is created. + event PackCreated( + uint256 indexed packId, + address indexed packCreator, + address recipient, + uint256 totalPacksCreated + ); + + /// @notice Emitted when a pack is opened. + event PackOpened( + uint256 indexed packId, + address indexed opener, + uint256 numOfPacksOpened, + ITokenBundle.Token[] rewardUnitsDistributed + ); + + Pack internal pack; + + Wallet internal tokenOwner; + string internal packUri; + ITokenBundle.Token[] internal packContents; + uint256[] internal numOfRewardUnits; + + function setUp() public override { + super.setUp(); + + pack = Pack(payable(getContract("Pack"))); + + tokenOwner = getWallet(); + packUri = "ipfs://"; + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 0, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 0, + totalAmount: 100 + }) + ); + numOfRewardUnits.push(20); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + numOfRewardUnits.push(50); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 1, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 2, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + numOfRewardUnits.push(100); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 3, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 4, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 5, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 1, + totalAmount: 500 + }) + ); + numOfRewardUnits.push(50); + + erc20.mint(address(tokenOwner), 2000 ether); + erc721.mint(address(tokenOwner), 6); + erc1155.mint(address(tokenOwner), 0, 100); + erc1155.mint(address(tokenOwner), 1, 500); + + tokenOwner.setAllowanceERC20(address(erc20), address(pack), type(uint256).max); + tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), true); + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), true); + + vm.prank(deployer); + pack.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `createPack` + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing state changes; token owner calls `createPack` to pack owned tokens. + */ + function test_state_createPack() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + assertEq(packId + 1, pack.nextTokenIdToMint()); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, packContents.length); + for (uint256 i = 0; i < packed.length; i += 1) { + assertEq(packed[i].assetContract, packContents[i].assetContract); + assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); + assertEq(packed[i].tokenId, packContents[i].tokenId); + assertEq(packed[i].totalAmount, packContents[i].totalAmount); + } + + assertEq(packUri, pack.uri(packId)); + } + + /* + * note: Testing state changes; token owner calls `createPack` to pack native tokens. + */ + function test_state_createPack_nativeTokens() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.deal(address(tokenOwner), 100 ether); + packContents.push( + ITokenBundle.Token({ + assetContract: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 20 ether + }) + ); + numOfRewardUnits.push(20); + + vm.prank(address(tokenOwner)); + pack.createPack{ value: 20 ether }(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + assertEq(packId + 1, pack.nextTokenIdToMint()); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, packContents.length); + for (uint256 i = 0; i < packed.length; i += 1) { + assertEq(packed[i].assetContract, packContents[i].assetContract); + assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); + assertEq(packed[i].tokenId, packContents[i].tokenId); + assertEq(packed[i].totalAmount, packContents[i].totalAmount); + } + + assertEq(packUri, pack.uri(packId)); + } + + /** + * note: Testing state changes; token owner calls `createPack` to pack owned tokens. + * Only assets with ASSET_ROLE can be packed. + */ + function test_state_createPack_withAssetRoleRestriction() public { + vm.startPrank(deployer); + pack.revokeRole(keccak256("ASSET_ROLE"), address(0)); + for (uint256 i = 0; i < packContents.length; i += 1) { + pack.grantRole(keccak256("ASSET_ROLE"), packContents[i].assetContract); + } + vm.stopPrank(); + + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + assertEq(packId + 1, pack.nextTokenIdToMint()); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, packContents.length); + for (uint256 i = 0; i < packed.length; i += 1) { + assertEq(packed[i].assetContract, packContents[i].assetContract); + assertEq(uint256(packed[i].tokenType), uint256(packContents[i].tokenType)); + assertEq(packed[i].tokenId, packContents[i].tokenId); + assertEq(packed[i].totalAmount, packContents[i].totalAmount); + } + + assertEq(packUri, pack.uri(packId)); + } + + /** + * note: Testing event emission; token owner calls `createPack` to pack owned tokens. + */ + function test_event_createPack_PackCreated() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectEmit(true, true, true, true); + emit PackCreated(packId, address(tokenOwner), recipient, 226); + + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + vm.stopPrank(); + } + + /** + * note: Testing token balances; token owner calls `createPack` to pack owned tokens. + */ + function test_balances_createPack() public { + // ERC20 balance + assertEq(erc20.balanceOf(address(tokenOwner)), 2000 ether); + assertEq(erc20.balanceOf(address(pack)), 0); + + // ERC721 balance + assertEq(erc721.ownerOf(0), address(tokenOwner)); + assertEq(erc721.ownerOf(1), address(tokenOwner)); + assertEq(erc721.ownerOf(2), address(tokenOwner)); + assertEq(erc721.ownerOf(3), address(tokenOwner)); + assertEq(erc721.ownerOf(4), address(tokenOwner)); + assertEq(erc721.ownerOf(5), address(tokenOwner)); + + // ERC1155 balance + assertEq(erc1155.balanceOf(address(tokenOwner), 0), 100); + assertEq(erc1155.balanceOf(address(pack), 0), 0); + + assertEq(erc1155.balanceOf(address(tokenOwner), 1), 500); + assertEq(erc1155.balanceOf(address(pack), 1), 0); + + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(1); + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + // ERC20 balance + assertEq(erc20.balanceOf(address(tokenOwner)), 0); + assertEq(erc20.balanceOf(address(pack)), 2000 ether); + + // ERC721 balance + assertEq(erc721.ownerOf(0), address(pack)); + assertEq(erc721.ownerOf(1), address(pack)); + assertEq(erc721.ownerOf(2), address(pack)); + assertEq(erc721.ownerOf(3), address(pack)); + assertEq(erc721.ownerOf(4), address(pack)); + assertEq(erc721.ownerOf(5), address(pack)); + + // ERC1155 balance + assertEq(erc1155.balanceOf(address(tokenOwner), 0), 0); + assertEq(erc1155.balanceOf(address(pack), 0), 100); + + assertEq(erc1155.balanceOf(address(tokenOwner), 1), 0); + assertEq(erc1155.balanceOf(address(pack), 1), 500); + + // Pack wrapped token balance + assertEq(pack.balanceOf(address(recipient), packId), totalSupply); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack owned tokens. + * Only assets with ASSET_ROLE can be packed, but assets being packed don't have that role. + */ + function test_revert_createPack_access_ASSET_ROLE() public { + vm.prank(deployer); + pack.revokeRole(keccak256("ASSET_ROLE"), address(0)); + + address recipient = address(0x123); + + string memory errorMsg = string( + abi.encodePacked( + "Permissions: account ", + Strings.toHexString(uint160(packContents[0].assetContract), 20), + " is missing role ", + Strings.toHexString(uint256(keccak256("ASSET_ROLE")), 32) + ) + ); + + vm.prank(address(tokenOwner)); + vm.expectRevert(bytes(errorMsg)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack owned tokens, without MINTER_ROLE. + */ + function test_revert_createPack_access_MINTER_ROLE() public { + vm.prank(address(tokenOwner)); + pack.renounceRole(keccak256("MINTER_ROLE"), address(tokenOwner)); + + 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)); + vm.expectRevert(bytes(errorMsg)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` with insufficient value when packing native tokens. + */ + function test_revert_createPack_nativeTokens_insufficientValue() public { + address recipient = address(0x123); + + vm.deal(address(tokenOwner), 100 ether); + + packContents.push( + ITokenBundle.Token({ + assetContract: 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE, + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 20 ether + }) + ); + numOfRewardUnits.push(1 ether); + + vm.prank(address(tokenOwner)); + vm.expectRevert("msg.value != amount"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC20 tokens. + */ + function test_revert_createPack_notOwner_ERC20() public { + tokenOwner.transferERC20(address(erc20), address(0x12), 1000 ether); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC20: transfer amount exceeds balance"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC721 tokens. + */ + function test_revert_createPack_notOwner_ERC721() public { + tokenOwner.transferERC721(address(erc721), address(0x12), 0); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC721: transfer caller is not owner nor approved"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-owned ERC1155 tokens. + */ + function test_revert_createPack_notOwner_ERC1155() public { + tokenOwner.transferERC1155(address(erc1155), address(0x12), 0, 100, ""); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC1155: insufficient balance for transfer"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC20 tokens. + */ + function test_revert_createPack_notApprovedTransfer_ERC20() public { + tokenOwner.setAllowanceERC20(address(erc20), address(pack), 0); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC20: insufficient allowance"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC721 tokens. + */ + function test_revert_createPack_notApprovedTransfer_ERC721() public { + tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), false); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC721: transfer caller is not owner nor approved"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` to pack un-approved ERC1155 tokens. + */ + function test_revert_createPack_notApprovedTransfer_ERC1155() public { + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), false); + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ERC1155: caller is not owner nor approved"); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` with no tokens to pack. + */ + function test_revert_createPack_noTokensToPack() public { + ITokenBundle.Token[] memory emptyContent; + uint256[] memory rewardUnits; + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("nothing to pack"); + pack.createPack(emptyContent, rewardUnits, packUri, 0, 1, recipient); + } + + /** + * note: Testing revert condition; token owner calls `createPack` with unequal length of contents and rewardUnits. + */ + function test_revert_createPack_invalidRewardUnits() public { + uint256[] memory rewardUnits; + + address recipient = address(0x123); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("invalid reward units"); + pack.createPack(packContents, rewardUnits, packUri, 0, 1, recipient); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `openPack` + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing state changes; pack owner calls `openPack` to redeem underlying rewards. + */ + function test_state_openPack() public { + vm.warp(1000); + uint256 packId = pack.nextTokenIdToMint(); + uint256 packsToOpen = 3; + address recipient = address(1); + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); + + vm.prank(recipient, recipient); + ITokenBundle.Token[] memory rewardUnits = pack.openPack(packId, packsToOpen); + console2.log("total reward units: ", rewardUnits.length); + + for (uint256 i = 0; i < rewardUnits.length; i++) { + console2.log("----- reward unit number: ", i, "------"); + console2.log("asset contract: ", rewardUnits[i].assetContract); + console2.log("token type: ", uint256(rewardUnits[i].tokenType)); + console2.log("tokenId: ", rewardUnits[i].tokenId); + if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { + console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); + } else { + console2.log("total amount: ", rewardUnits[i].totalAmount); + } + console2.log(""); + } + + assertEq(packUri, pack.uri(packId)); + assertEq(pack.totalSupply(packId), totalSupply - packsToOpen); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, packContents.length); + } + + /** + * note: Testing event emission; pack owner calls `openPack` to open owned packs. + */ + function test_event_openPack_PackOpened() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + ITokenBundle.Token[] memory emptyRewardUnitsForTestingEvent; + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + vm.expectEmit(true, true, false, false); + emit PackOpened(packId, recipient, 1, emptyRewardUnitsForTestingEvent); + + vm.prank(recipient, recipient); + pack.openPack(packId, 1); + } + + function test_balances_openPack() public { + uint256 packId = pack.nextTokenIdToMint(); + uint256 packsToOpen = 3; + address recipient = address(1); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 2, recipient); + + // ERC20 balance + assertEq(erc20.balanceOf(address(recipient)), 0); + assertEq(erc20.balanceOf(address(pack)), 2000 ether); + + // ERC721 balance + assertEq(erc721.ownerOf(0), address(pack)); + assertEq(erc721.ownerOf(1), address(pack)); + assertEq(erc721.ownerOf(2), address(pack)); + assertEq(erc721.ownerOf(3), address(pack)); + assertEq(erc721.ownerOf(4), address(pack)); + assertEq(erc721.ownerOf(5), address(pack)); + + // ERC1155 balance + assertEq(erc1155.balanceOf(address(recipient), 0), 0); + assertEq(erc1155.balanceOf(address(pack), 0), 100); + + assertEq(erc1155.balanceOf(address(recipient), 1), 0); + assertEq(erc1155.balanceOf(address(pack), 1), 500); + + vm.prank(recipient, recipient); + ITokenBundle.Token[] memory rewardUnits = pack.openPack(packId, packsToOpen); + console2.log("total reward units: ", rewardUnits.length); + + uint256 erc20Amount; + uint256[] memory erc1155Amounts = new uint256[](2); + uint256 erc721Amount; + + for (uint256 i = 0; i < rewardUnits.length; i++) { + console2.log("----- reward unit number: ", i, "------"); + console2.log("asset contract: ", rewardUnits[i].assetContract); + console2.log("token type: ", uint256(rewardUnits[i].tokenType)); + console2.log("tokenId: ", rewardUnits[i].tokenId); + if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { + console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); + console.log("balance of recipient: ", erc20.balanceOf(address(recipient)) / 1 ether, "ether"); + erc20Amount += rewardUnits[i].totalAmount; + } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC1155) { + console2.log("total amount: ", rewardUnits[i].totalAmount); + console.log("balance of recipient: ", erc1155.balanceOf(address(recipient), rewardUnits[i].tokenId)); + erc1155Amounts[rewardUnits[i].tokenId] += rewardUnits[i].totalAmount; + } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC721) { + console2.log("total amount: ", rewardUnits[i].totalAmount); + console.log("balance of recipient: ", erc721.balanceOf(address(recipient))); + erc721Amount += rewardUnits[i].totalAmount; + } + console2.log(""); + } + + assertEq(erc20.balanceOf(address(recipient)), erc20Amount); + assertEq(erc721.balanceOf(address(recipient)), erc721Amount); + + for (uint256 i = 0; i < erc1155Amounts.length; i += 1) { + assertEq(erc1155.balanceOf(address(recipient), i), erc1155Amounts[i]); + } + } + + /** + * note: Testing revert condition; caller of `openPack` is not EOA. + */ + function test_revert_openPack_notEOA() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + vm.startPrank(recipient, address(27)); + vm.expectRevert("opener must be eoa"); + pack.openPack(packId, 1); + } + + /** + * note: Testing revert condition; pack owner calls `openPack` to open more than owned packs. + */ + function test_revert_openPack_openMoreThanOwned() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + vm.startPrank(recipient, recipient); + vm.expectRevert("opening more than owned"); + pack.openPack(packId, totalSupply + 1); + } + + /** + * note: Testing revert condition; pack owner calls `openPack` before start timestamp. + */ + function test_revert_openPack_openBeforeStart() public { + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 1000, 1, recipient); + + vm.startPrank(recipient, recipient); + vm.expectRevert("cannot open yet"); + pack.openPack(packId, 1); + } + + /** + * note: Testing revert condition; pack owner calls `openPack` with pack-id non-existent or not owned. + */ + function test_revert_openPack_invalidPackId() public { + address recipient = address(0x123); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, recipient); + + vm.startPrank(recipient, recipient); + vm.expectRevert("opening more than owned"); + pack.openPack(2, 1); + } + + /*/////////////////////////////////////////////////////////////// + Fuzz testing + //////////////////////////////////////////////////////////////*/ + + uint256 internal constant MAX_TOKENS = 2000; + + function getTokensToPack(uint256 len) + internal + returns (ITokenBundle.Token[] memory tokensToPack, uint256[] memory rewardUnits) + { + vm.assume(len < MAX_TOKENS); + tokensToPack = new ITokenBundle.Token[](len); + rewardUnits = new uint256[](len); + + for (uint256 i = 0; i < len; i += 1) { + uint256 random = uint256(keccak256(abi.encodePacked(len + i))) % MAX_TOKENS; + uint256 selector = random % 4; + + if (selector == 0) { + tokensToPack[i] = ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: (random + 1) * 10 ether + }); + rewardUnits[i] = random + 1; + + erc20.mint(address(tokenOwner), tokensToPack[i].totalAmount); + } else if (selector == 1) { + uint256 tokenId = erc721.nextTokenIdToMint(); + + tokensToPack[i] = ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: tokenId, + totalAmount: 1 + }); + rewardUnits[i] = 1; + + erc721.mint(address(tokenOwner), 1); + } else if (selector == 2) { + tokensToPack[i] = ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: random, + totalAmount: (random + 1) * 10 + }); + rewardUnits[i] = random + 1; + + erc1155.mint(address(tokenOwner), tokensToPack[i].tokenId, tokensToPack[i].totalAmount); + } else if (selector == 3) { + tokensToPack[i] = ITokenBundle.Token({ + assetContract: address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 5 ether + }); + rewardUnits[i] = 5; + } + } + } + + function checkBalances(ITokenBundle.Token[] memory rewardUnits, address recipient) + internal + returns ( + uint256 nativeTokenAmount, + uint256 erc20Amount, + uint256[] memory erc1155Amounts, + uint256 erc721Amount + ) + { + erc1155Amounts = new uint256[](MAX_TOKENS); + + for (uint256 i = 0; i < rewardUnits.length; i++) { + console2.log("----- reward unit number: ", i, "------"); + console2.log("asset contract: ", rewardUnits[i].assetContract); + console2.log("token type: ", uint256(rewardUnits[i].tokenType)); + console2.log("tokenId: ", rewardUnits[i].tokenId); + if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC20) { + if (rewardUnits[i].assetContract == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { + console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); + console.log("balance of recipient: ", address(recipient).balance); + nativeTokenAmount += rewardUnits[i].totalAmount; + } else { + console2.log("total amount: ", rewardUnits[i].totalAmount / 1 ether, "ether"); + console.log("balance of recipient: ", erc20.balanceOf(address(recipient)) / 1 ether, "ether"); + erc20Amount += rewardUnits[i].totalAmount; + } + } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC1155) { + console2.log("total amount: ", rewardUnits[i].totalAmount); + console.log("balance of recipient: ", erc1155.balanceOf(address(recipient), rewardUnits[i].tokenId)); + erc1155Amounts[rewardUnits[i].tokenId] += rewardUnits[i].totalAmount; + } else if (rewardUnits[i].tokenType == ITokenBundle.TokenType.ERC721) { + console2.log("total amount: ", rewardUnits[i].totalAmount); + console.log("balance of recipient: ", erc721.balanceOf(address(recipient))); + erc721Amount += rewardUnits[i].totalAmount; + } + console2.log(""); + } + } + + function test_fuzz_state_createPack(uint256 x, uint128 y) public { + (ITokenBundle.Token[] memory tokensToPack, uint256[] memory rewardUnits) = getTokensToPack(x); + if (tokensToPack.length == 0) { + return; + } + + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + uint256 totalRewardUnits; + uint256 nativeTokenPacked; + + for (uint256 i = 0; i < tokensToPack.length; i += 1) { + totalRewardUnits += rewardUnits[i]; + if (tokensToPack[i].assetContract == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { + nativeTokenPacked += tokensToPack[i].totalAmount; + } + } + vm.deal(address(tokenOwner), nativeTokenPacked); + vm.assume(y > 0 && totalRewardUnits % y == 0); + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack{ value: nativeTokenPacked }( + tokensToPack, + rewardUnits, + packUri, + 0, + y, + recipient + ); + console2.log("total supply: ", totalSupply); + console2.log("total reward units: ", totalRewardUnits); + + assertEq(packId + 1, pack.nextTokenIdToMint()); + + (ITokenBundle.Token[] memory packed, ) = pack.getPackContents(packId); + assertEq(packed.length, tokensToPack.length); + for (uint256 i = 0; i < packed.length; i += 1) { + assertEq(packed[i].assetContract, tokensToPack[i].assetContract); + assertEq(uint256(packed[i].tokenType), uint256(tokensToPack[i].tokenType)); + assertEq(packed[i].tokenId, tokensToPack[i].tokenId); + assertEq(packed[i].totalAmount, tokensToPack[i].totalAmount); + } + + assertEq(packUri, pack.uri(packId)); + } + + function test_fuzz_state_openPack( + uint256 x, + uint128 y, + uint256 z + ) public { + // vm.assume(x == 1574 && y == 22 && z == 392); + (ITokenBundle.Token[] memory tokensToPack, uint256[] memory rewardUnits) = getTokensToPack(x); + if (tokensToPack.length == 0) { + return; + } + + uint256 packId = pack.nextTokenIdToMint(); + address recipient = address(0x123); + uint256 totalRewardUnits; + uint256 nativeTokenPacked; + + for (uint256 i = 0; i < tokensToPack.length; i += 1) { + totalRewardUnits += rewardUnits[i]; + if (tokensToPack[i].assetContract == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { + nativeTokenPacked += tokensToPack[i].totalAmount; + } + } + vm.assume(y > 0 && totalRewardUnits % y == 0); + vm.deal(address(tokenOwner), nativeTokenPacked); + + vm.prank(address(tokenOwner)); + (, uint256 totalSupply) = pack.createPack{ value: nativeTokenPacked }( + tokensToPack, + rewardUnits, + packUri, + 0, + y, + recipient + ); + console2.log("total supply: ", totalSupply); + console2.log("total reward units: ", totalRewardUnits); + + vm.assume(z <= totalSupply); + vm.prank(recipient, recipient); + ITokenBundle.Token[] memory rewardsReceived = pack.openPack(packId, z); + console2.log("received reward units: ", rewardsReceived.length); + + assertEq(packUri, pack.uri(packId)); + + ( + uint256 nativeTokenAmount, + uint256 erc20Amount, + uint256[] memory erc1155Amounts, + uint256 erc721Amount + ) = checkBalances(rewardsReceived, recipient); + + assertEq(address(recipient).balance, nativeTokenAmount); + assertEq(erc20.balanceOf(address(recipient)), erc20Amount); + assertEq(erc721.balanceOf(address(recipient)), erc721Amount); + + for (uint256 i = 0; i < erc1155Amounts.length; i += 1) { + assertEq(erc1155.balanceOf(address(recipient), i), erc1155Amounts[i]); + } + } + + // function test_fuzz_failing_state_openPack() public { + // // vm.assume(x == 1574 && y == 22 && z == 392); + + // uint256 x = 1574; + // uint128 y = 22; + // uint256 z = 392; + + // (ITokenBundle.Token[] memory tokensToPack, uint256[] memory rewardUnits) = getTokensToPack(x); + // if (tokensToPack.length == 0) { + // return; + // } + + // uint256 packId = pack.nextTokenIdToMint(); + // address recipient = address(0x123); + // uint256 totalRewardUnits; + // uint256 nativeTokenPacked; + + // for (uint256 i = 0; i < tokensToPack.length; i += 1) { + // totalRewardUnits += tokensToPack[i].totalAmount / amounts[i]; + // if (tokensToPack[i].assetContract == address(0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE)) { + // nativeTokenPacked += tokensToPack[i].totalAmount; + // } + // } + // vm.assume(y > 0 && totalRewardUnits % y == 0); + // vm.deal(address(tokenOwner), nativeTokenPacked); + + // vm.prank(address(tokenOwner)); + // (, uint256 totalSupply) = pack.createPack{ value: nativeTokenPacked }( + // tokensToPack, + // amounts, + // packUri, + // 0, + // y, + // recipient + // ); + // console2.log("total supply: ", totalSupply); + // console2.log("total reward units: ", totalRewardUnits); + + // vm.assume(z <= totalSupply); + // vm.prank(recipient, recipient); + // ITokenBundle.Token[] memory rewardUnits = pack.openPack(packId, z); + // console2.log("received reward units: ", rewardUnits.length); + + // assertEq(packUri, pack.uri(packId)); + + // ( + // uint256 nativeTokenAmount, + // uint256 erc20Amount, + // uint256[] memory erc1155Amounts, + // uint256 erc721Amount + // ) = checkBalances(rewardUnits, recipient); + + // assertEq(address(recipient).balance, nativeTokenAmount); + // assertEq(erc20.balanceOf(address(recipient)), erc20Amount); + // assertEq(erc721.balanceOf(address(recipient)), erc721Amount); + + // for (uint256 i = 0; i < erc1155Amounts.length; i += 1) { + // assertEq(erc1155.balanceOf(address(recipient), i), erc1155Amounts[i]); + // } + // } + + /*/////////////////////////////////////////////////////////////// + Scenario/Exploit tests + //////////////////////////////////////////////////////////////*/ + /** + * note: Testing revert condition; token owner calls `createPack` to pack owned tokens. + */ + function test_revert_createPack_reentrancy() public { + MaliciousERC20 malERC20 = new MaliciousERC20(payable(address(pack))); + ITokenBundle.Token[] memory content = new ITokenBundle.Token[](1); + uint256[] memory rewards = new uint256[](1); + + malERC20.mint(address(tokenOwner), 10 ether); + content[0] = ITokenBundle.Token({ + assetContract: address(malERC20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 10 ether + }); + rewards[0] = 10; + + tokenOwner.setAllowanceERC20(address(malERC20), address(pack), 10 ether); + + address recipient = address(0x123); + + vm.prank(address(deployer)); + pack.grantRole(keccak256("MINTER_ROLE"), address(malERC20)); + + vm.startPrank(address(tokenOwner)); + vm.expectRevert("ReentrancyGuard: reentrant call"); + pack.createPack(content, rewards, packUri, 0, 1, recipient); + } +} + +contract MaliciousERC20 is MockERC20, ITokenBundle { + Pack public pack; + + constructor(address payable _pack) { + pack = Pack(_pack); + } + + function transferFrom( + address from, + address to, + uint256 amount + ) public override returns (bool) { + ITokenBundle.Token[] memory content = new ITokenBundle.Token[](1); + uint256[] memory rewards = new uint256[](1); + + address recipient = address(0x123); + pack.createPack(content, rewards, "", 0, 1, recipient); + return super.transferFrom(from, to, amount); + } +} diff --git a/src/test/PackBenchmark.t.sol b/src/test/PackBenchmark.t.sol new file mode 100644 index 000000000..b867ed68b --- /dev/null +++ b/src/test/PackBenchmark.t.sol @@ -0,0 +1,258 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.0; + +import { Pack } from "contracts/pack/Pack.sol"; +import { IPack } from "contracts/interfaces/IPack.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"; + +contract CreatePackBenchmarkTest is BaseTest { + Pack internal pack; + + Wallet internal tokenOwner; + string internal packUri; + ITokenBundle.Token[] internal packContents; + uint256[] internal numOfRewardUnits; + + function setUp() public override { + super.setUp(); + + pack = Pack(payable(getContract("Pack"))); + + tokenOwner = getWallet(); + packUri = "ipfs://"; + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 0, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 0, + totalAmount: 100 + }) + ); + numOfRewardUnits.push(20); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + numOfRewardUnits.push(50); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 1, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + numOfRewardUnits.push(100); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 2, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 3, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 4, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + erc20.mint(address(tokenOwner), 2000 ether); + erc721.mint(address(tokenOwner), 5); + erc1155.mint(address(tokenOwner), 0, 100); + + tokenOwner.setAllowanceERC20(address(erc20), address(pack), type(uint256).max); + tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), true); + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), true); + + vm.prank(deployer); + pack.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); + + vm.startPrank(address(tokenOwner)); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `createPack` + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing state changes; token owner calls `createPack` to pack owned tokens. + */ + function test_benchmark_createPack() public { + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, address(0x123)); + } +} + +contract OpenPackBenchmarkTest is BaseTest { + Pack internal pack; + + Wallet internal tokenOwner; + string internal packUri; + ITokenBundle.Token[] internal packContents; + uint256[] internal numOfRewardUnits; + + function setUp() public override { + super.setUp(); + + pack = Pack(payable(getContract("Pack"))); + + tokenOwner = getWallet(); + packUri = "ipfs://"; + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc721), + tokenType: ITokenBundle.TokenType.ERC721, + tokenId: 0, + totalAmount: 1 + }) + ); + numOfRewardUnits.push(1); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc1155), + tokenType: ITokenBundle.TokenType.ERC1155, + tokenId: 0, + totalAmount: 100 + }) + ); + numOfRewardUnits.push(20); + + packContents.push( + ITokenBundle.Token({ + assetContract: address(erc20), + tokenType: ITokenBundle.TokenType.ERC20, + tokenId: 0, + totalAmount: 1000 ether + }) + ); + numOfRewardUnits.push(50); + + // packContents.push( + // ITokenBundle.Token({ + // assetContract: address(erc721), + // tokenType: ITokenBundle.TokenType.ERC721, + // tokenId: 1, + // totalAmount: 1 + // }) + // ); + // amountsPerUnit.push(1); + + // packContents.push( + // ITokenBundle.Token({ + // assetContract: address(erc20), + // tokenType: ITokenBundle.TokenType.ERC20, + // tokenId: 0, + // totalAmount: 1000 ether + // }) + // ); + // amountsPerUnit.push(10 ether); + + // packContents.push( + // ITokenBundle.Token({ + // assetContract: address(erc721), + // tokenType: ITokenBundle.TokenType.ERC721, + // tokenId: 2, + // totalAmount: 1 + // }) + // ); + // amountsPerUnit.push(1); + + // packContents.push( + // ITokenBundle.Token({ + // assetContract: address(erc721), + // tokenType: ITokenBundle.TokenType.ERC721, + // tokenId: 3, + // totalAmount: 1 + // }) + // ); + // amountsPerUnit.push(1); + + // packContents.push( + // ITokenBundle.Token({ + // assetContract: address(erc721), + // tokenType: ITokenBundle.TokenType.ERC721, + // tokenId: 4, + // totalAmount: 1 + // }) + // ); + // amountsPerUnit.push(1); + + erc20.mint(address(tokenOwner), 2000 ether); + erc721.mint(address(tokenOwner), 5); + erc1155.mint(address(tokenOwner), 0, 100); + + tokenOwner.setAllowanceERC20(address(erc20), address(pack), type(uint256).max); + tokenOwner.setApprovalForAllERC721(address(erc721), address(pack), true); + tokenOwner.setApprovalForAllERC1155(address(erc1155), address(pack), true); + + vm.prank(deployer); + pack.grantRole(keccak256("MINTER_ROLE"), address(tokenOwner)); + + vm.prank(address(tokenOwner)); + pack.createPack(packContents, numOfRewardUnits, packUri, 0, 1, address(0x123)); + + vm.startPrank(address(0x123), address(0x123)); + } + + /*/////////////////////////////////////////////////////////////// + Unit tests: `openPack` + //////////////////////////////////////////////////////////////*/ + + /** + * note: Testing state changes; pack owner calls `openPack` to redeem underlying rewards. + */ + function test_benchmark_openPack() public { + pack.openPack(0, 1); + } +} diff --git a/src/test/utils/BaseTest.sol b/src/test/utils/BaseTest.sol index f1ffee35d..b2facfe52 100644 --- a/src/test/utils/BaseTest.sol +++ b/src/test/utils/BaseTest.sol @@ -14,7 +14,7 @@ import "contracts/TWFee.sol"; import "contracts/TWRegistry.sol"; import "contracts/TWFactory.sol"; import { Multiwrap } from "contracts/multiwrap/Multiwrap.sol"; -import "contracts/Pack.sol"; +import { Pack } from "contracts/pack/Pack.sol"; import "contracts/Split.sol"; import "contracts/drop/DropERC20.sol"; import "contracts/drop/DropERC721.sol"; @@ -90,8 +90,9 @@ abstract contract BaseTest is DSTest, Test { TWFactory(factory).addImplementation(address(new MockContract(bytes32("Marketplace"), 1))); TWFactory(factory).addImplementation(address(new Marketplace(address(weth), fee))); TWFactory(factory).addImplementation(address(new Split(fee))); - // TWFactory(factory).addImplementation(address(new Pack(address(0), address(0), fee))); TWFactory(factory).addImplementation(address(new Multiwrap(address(weth)))); + TWFactory(factory).addImplementation(address(new MockContract(bytes32("Pack"), 1))); + TWFactory(factory).addImplementation(address(new Pack(address(weth)))); TWFactory(factory).addImplementation(address(new VoteERC20())); vm.stopPrank(); @@ -232,6 +233,13 @@ abstract contract BaseTest is DSTest, Test { (deployer, NAME, SYMBOL, CONTRACT_URI, forwarders(), royaltyRecipient, royaltyBps) ) ); + deployContractProxy( + "Pack", + abi.encodeCall( + Pack.initialize, + (deployer, NAME, SYMBOL, CONTRACT_URI, forwarders(), royaltyRecipient, royaltyBps) + ) + ); } function deployContractProxy(string memory _contractType, bytes memory _initializer)