diff --git a/contracts/DODOMysteryBox/BaseMysteryBox.sol b/contracts/DODOMysteryBox/BaseMysteryBox.sol new file mode 100644 index 0000000..a1f20fd --- /dev/null +++ b/contracts/DODOMysteryBox/BaseMysteryBox.sol @@ -0,0 +1,177 @@ +/* + Copyright 2021 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 +*/ +pragma solidity 0.6.9; +pragma experimental ABIEncoderV2; + +import {IERC20} from "../intf/IERC20.sol"; +import {SafeERC20} from "../lib/SafeERC20.sol"; +import {SafeMath} from "../lib/SafeMath.sol"; +import {InitializableOwnable} from "../lib/InitializableOwnable.sol"; +import {IDODOApproveProxy} from "../SmartRoute/DODOApproveProxy.sol"; + +interface IPrice { + function getUserPrice(address mysteryBox, address user, uint256 originalPrice) external view returns (uint256); +} + + +contract BaseMysteryBox is InitializableOwnable { + using SafeMath for uint256; + using SafeERC20 for IERC20; + + // ============ Storage ============ + address constant _BASE_COIN_ = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE; + address public _BUY_TICKET_TOKEN_; + address public _FEE_MODEL_; + address public _DODO_APPROVE_PROXY_; + uint256 [] public _PRICE_TIME_INTERVAL_; + uint256 [] public _PRICE_SET_; + uint256 [] public _SELLING_AMOUNT_SET_; + + uint256 public _REDEEM_ALLOWED_TIME_; + + mapping(address => uint256) public _USER_TICKETS_; + uint256 public _TOTAL_TICKETS_; + uint256 public _TICKET_UNIT_ = 1; // ticket consumed in a single lottery + + bool public _IS_REVEAL_MODE_; + uint256 public _REVEAL_RNG_ = 0; + address public _RANDOM_GENERATOR_; + + + // ============ Modifiers ============ + + modifier notStart() { + require(block.timestamp < _PRICE_TIME_INTERVAL_[0] || _PRICE_TIME_INTERVAL_[0] == 0, "ALREADY_START"); + _; + } + + // ============ Event ============= + event BuyTicket(address account, uint256 value, uint256 tickets); + event Withdraw(address account, uint256 amount); + event ChangeRandomGenerator(address randomGenerator); + event ChangeRedeemTime(uint256 redeemTime); + event ChangeTicketUnit(uint256 newTicketUnit); + event SetPriceInterval(); + event Transfer(address indexed from, address indexed to, uint256 amount); + + + function _baseInit( + address[] memory contractList, //0 owner, 1 buyTicketToken, 2 feeModel, 3 dodoApproveProxy 4 randomGenerator + uint256[][] memory priceList, //0 priceTimeInterval, 1 priceSet, 2 sellAmount + uint256 redeemAllowedTime, + bool isRevealMode + ) public { + initOwner(contractList[0]); + _BUY_TICKET_TOKEN_ = contractList[1]; + _FEE_MODEL_ = contractList[2]; + _DODO_APPROVE_PROXY_ = contractList[3]; + _RANDOM_GENERATOR_ = contractList[4]; + + _REDEEM_ALLOWED_TIME_ = redeemAllowedTime; + _IS_REVEAL_MODE_ = isRevealMode; + if(priceList[0].length > 0) _setPrice(priceList[0], priceList[1], priceList[2]); + } + + function buyTickets(uint256 amount) payable external { + uint256 curBlockTime = block.timestamp; + require(curBlockTime >= _PRICE_TIME_INTERVAL_[0] && _PRICE_TIME_INTERVAL_[0] != 0, "NOT_START"); + uint256 i; + for (i = 1; i < _PRICE_TIME_INTERVAL_.length; i++) { + if (curBlockTime <= _PRICE_TIME_INTERVAL_[i]) { + break; + } + } + uint256 curSellAmount = _SELLING_AMOUNT_SET_[i-1]; + require(amount <= curSellAmount, "TICKETS_NOT_ENOUGH"); + uint256 buyPrice = IPrice(_FEE_MODEL_).getUserPrice(address(this), msg.sender, _PRICE_SET_[i-1]); + require(buyPrice > 0, "UnQualified"); + uint256 payAmount = buyPrice.mul(amount); + if(_BUY_TICKET_TOKEN_ == _BASE_COIN_) { + require(msg.value >= payAmount,"BUY_TOKEN_NOT_ENOUGH"); + }else { + IDODOApproveProxy(_DODO_APPROVE_PROXY_).claimTokens(_BUY_TICKET_TOKEN_, msg.sender, address(this), payAmount); + } + + _USER_TICKETS_[msg.sender] = _USER_TICKETS_[msg.sender].add(amount); + _TOTAL_TICKETS_ = _TOTAL_TICKETS_.add(amount); + _SELLING_AMOUNT_SET_[i - 1] = curSellAmount.sub(amount); + + uint256 leftOver = msg.value - payAmount; + if(leftOver > 0) + msg.sender.transfer(leftOver); + + emit BuyTicket(msg.sender, payAmount, amount); + } + + + function transferTickets(address to, uint256 amount) public returns (bool) { + require(amount <= _USER_TICKETS_[msg.sender], "TICKET_NOT_ENOUGH"); + + _USER_TICKETS_[msg.sender] = _USER_TICKETS_[msg.sender].sub(amount); + _USER_TICKETS_[to] = _USER_TICKETS_[to].add(amount); + emit Transfer(msg.sender, to, amount); + return true; + } + + // ================= View =================== + function getTickets(address account) view external returns(uint256) { + return _USER_TICKETS_[account]; + } + + // ============ Internal ============ + + function _setPrice(uint256[] memory priceIntervals, uint256[] memory prices, uint256[] memory amounts) internal { + require(priceIntervals.length == prices.length && prices.length == amounts.length, "PARAM_NOT_INVALID"); + for (uint256 i = 0; i < priceIntervals.length - 1; i++) { + require(priceIntervals[i] < priceIntervals[i + 1], "INTERVAL_INVALID"); + require(prices[i] != 0, "INTERVAL_INVALID"); + require(amounts[i] != 0, "INTERVAL_INVALID"); + } + _PRICE_TIME_INTERVAL_ = priceIntervals; + _PRICE_SET_ = prices; + _SELLING_AMOUNT_SET_ = amounts; + emit SetPriceInterval(); + } + + // ================= Owner =================== + function withdraw() external onlyOwner { + uint256 amount; + if(_BASE_COIN_ == _BUY_TICKET_TOKEN_) { + amount = address(this).balance; + msg.sender.transfer(amount); + }else { + amount = IERC20(_BUY_TICKET_TOKEN_).balanceOf(address(this)); + IERC20(_BUY_TICKET_TOKEN_).safeTransfer(msg.sender, amount); + } + emit Withdraw(msg.sender, amount); + } + + function setRevealRng() external onlyOwner { + require(_REVEAL_RNG_ == 0, "ALREADY_SET"); + _REVEAL_RNG_ = uint256(keccak256(abi.encodePacked(blockhash(block.number - 1)))); + } + + function setPrice(uint256[] memory priceIntervals, uint256[] memory prices, uint256[] memory amounts) external notStart() onlyOwner { + _setPrice(priceIntervals, prices, amounts); + } + + function updateRandomGenerator(address newRandomGenerator) external onlyOwner { + require(newRandomGenerator != address(0)); + _RANDOM_GENERATOR_ = newRandomGenerator; + emit ChangeRandomGenerator(newRandomGenerator); + } + + function updateTicketUnit(uint256 newTicketUnit) external onlyOwner { + require(newTicketUnit != 0); + _TICKET_UNIT_ = newTicketUnit; + emit ChangeTicketUnit(newTicketUnit); + } + + function updateRedeemTime(uint256 newRedeemTime) external onlyOwner { + require(newRedeemTime > block.timestamp || newRedeemTime == 0, "PARAM_NOT_INVALID"); + _REDEEM_ALLOWED_TIME_ = newRedeemTime; + emit ChangeRedeemTime(newRedeemTime); + } +} diff --git a/contracts/DODOMysteryBox/DODOMysteryBox.sol b/contracts/DODOMysteryBox/DODOMysteryBox.sol deleted file mode 100644 index 646d18c..0000000 --- a/contracts/DODOMysteryBox/DODOMysteryBox.sol +++ /dev/null @@ -1,192 +0,0 @@ -/* - Copyright 2020 DODO ZOO. - SPDX-License-Identifier: Apache-2.0 -*/ -pragma solidity 0.6.9; -pragma experimental ABIEncoderV2; - -import {IERC20} from "../intf/IERC20.sol"; -import {SafeERC20} from "../lib/SafeERC20.sol"; -import {SafeMath} from "../lib/SafeMath.sol"; -import {IRandomGenerator} from "../lib/RandomGenerator.sol"; -import {InitializableOwnable} from "../lib/InitializableOwnable.sol"; -import {ERC1155} from "../external/ERC1155/ERC1155.sol"; - -contract DODOMysteryBox is ERC1155, InitializableOwnable { - using SafeMath for uint256; - using SafeERC20 for IERC20; - - // ============ Storage ============ - - address public _TICKET_; - uint256 public _TICKET_RESERVE_; - uint256 public _TICKET_UNIT_; // ticket consumed in a single lottery - - address public _RANDOM_GENERATOR_; - address public _DODO_MYSTERY_BOX_PROXY_; - uint256[] public _PROB_INTERVAL_; // index => Interval probability - uint256[][] public _PRIZE_SET_; // Interval index => tokenIds - mapping(uint256 => bool) _TOKEN_ID_FLAG_; - - // ============ Event ============= - event ChangeRandomGenerator(address randomGenerator); - event ChangeTicketUnit(uint256 newTicketUnit); - event ChangeMysteryBoxProxy(address mysteryBoxProxy); - event RetriveTicket(address to, uint256 amount); - event BurnTicket(uint256 amount); - event RedeemPrize(address to, uint256 ticketInput, uint256 ticketNum); - event SetProbInterval(); - event SetPrizeSet(); - event SetPrizeSetByIndex(uint256 index); - - function init( - address owner, - string memory baseUri, - address randomGenerator, - address dodoMysteryBoxProxy, - address ticket, - uint256 ticketUnit, - uint256[] memory probIntervals, - uint256[][] memory prizeSet - ) public { - require( - probIntervals.length == prizeSet.length && probIntervals.length > 0, - "DODOMysteryBox: PARAM_NOT_INVALID" - ); - initOwner(owner); - _setURI(baseUri); - - _RANDOM_GENERATOR_ = randomGenerator; - _DODO_MYSTERY_BOX_PROXY_ = dodoMysteryBoxProxy; - _TICKET_ = ticket; - _TICKET_UNIT_ = ticketUnit; - - _setProbInterval(probIntervals); - _setPrizeSet(prizeSet); - } - - function redeemPrize(address to) external { - require(msg.sender == _DODO_MYSTERY_BOX_PROXY_, "DODOMysteryBox: ACCESS_DENIED"); - uint256 ticketBalance = IERC20(_TICKET_).balanceOf(address(this)); - uint256 ticketInput = ticketBalance.sub(_TICKET_RESERVE_); - uint256 ticketNum = ticketInput.div(_TICKET_UNIT_); - require(ticketNum >= 1, "DODOMysteryBox: TICKET_NOT_ENOUGH"); - for (uint256 i = 0; i < ticketNum; i++) { - _redeemSinglePrize(to); - } - _TICKET_RESERVE_ = _TICKET_RESERVE_.add(ticketInput); - emit RedeemPrize(to, ticketInput, ticketNum); - } - - // =============== View ================ - function getRarityByTokenId(uint256 tokenId) external view returns (uint256) { - require(_TOKEN_ID_FLAG_[tokenId], "DODOMysteryBox: TOKEN_ID_NOT_FOUND"); - for (uint256 i = 0; i < _PRIZE_SET_.length; i++) { - uint256[] memory curPrizes = _PRIZE_SET_[i]; - for (uint256 j = 0; j < curPrizes.length; j++) { - if(tokenId == curPrizes[j]) { - return i; - } - } - } - } - - // ============ Internal ============ - - function _redeemSinglePrize(address to) internal { - uint256 range = _PROB_INTERVAL_[_PROB_INTERVAL_.length - 1]; - uint256 random = IRandomGenerator(_RANDOM_GENERATOR_).random(gasleft()) % range; - uint256 i; - for (i = 0; i < _PROB_INTERVAL_.length; i++) { - if (random <= _PROB_INTERVAL_[i]) { - break; - } - } - require(_PRIZE_SET_[i].length > 0, "EMPTY_PRIZE_SET"); - uint256 prize = _PRIZE_SET_[i][random % _PRIZE_SET_[i].length]; - _mint(to, prize, 1, ""); - } - - function _setProbInterval(uint256[] memory probIntervals) internal { - for (uint256 i = 1; i < probIntervals.length; i++) { - require(probIntervals[i] > probIntervals[i - 1], "DODOMysteryBox: INTERVAL_INVALID"); - } - _PROB_INTERVAL_ = probIntervals; - emit SetProbInterval(); - } - - function _setPrizeSet(uint256[][] memory prizeSet) internal { - for (uint256 i = 0; i < prizeSet.length; i++) { - uint256[] memory curPrizes = prizeSet[i]; - require(curPrizes.length > 0, "DODOMysteryBox: PRIZES_INVALID"); - for (uint256 j = 0; j < curPrizes.length; j++) { - uint256 curTokenId = prizeSet[i][j]; - if (_TOKEN_ID_FLAG_[curTokenId]) { - require(false, "DODOMysteryBox: TOKEN_ID_INVALID"); - } else { - _TOKEN_ID_FLAG_[curTokenId] = true; - } - } - } - _PRIZE_SET_ = prizeSet; - emit SetPrizeSet(); - } - - // ================= Owner =================== - - function setProbInterval(uint256[] memory probIntervals) external onlyOwner { - require(probIntervals.length > 0, "DODOMysteryBox: PARAM_NOT_INVALID"); - _setProbInterval(probIntervals); - } - - function setPrzieSet(uint256[][] memory prizeSet) external onlyOwner { - require(prizeSet.length == _PROB_INTERVAL_.length, "DODOMysteryBox: PARAM_NOT_INVALID"); - _setPrizeSet(prizeSet); - } - - function setPrizeSetByIndex(uint256 index, uint256[] memory prizes) external onlyOwner { - require( - prizes.length > 0 && index < _PRIZE_SET_.length, - "DODOMysteryBox: PARAM_NOT_INVALID" - ); - for (uint256 i = 0; i < prizes.length; i++) { - if (_TOKEN_ID_FLAG_[prizes[i]]) { - require(false, "DODOMysteryBox: TOKEN_ID_INVALID"); - } else { - _TOKEN_ID_FLAG_[prizes[i]] = true; - } - } - _PRIZE_SET_[index] = prizes; - emit SetPrizeSetByIndex(index); - } - - function updateRandomGenerator(address newRandomGenerator) external onlyOwner { - require(newRandomGenerator != address(0)); - _RANDOM_GENERATOR_ = newRandomGenerator; - emit ChangeRandomGenerator(newRandomGenerator); - } - - function updateMysteryBoxProxy(address newMysteryBoxProxy) external onlyOwner { - require(newMysteryBoxProxy != address(0)); - _DODO_MYSTERY_BOX_PROXY_ = newMysteryBoxProxy; - emit ChangeMysteryBoxProxy(newMysteryBoxProxy); - } - - function updateTicketUnit(uint256 newTicketUnit) external onlyOwner { - require(newTicketUnit != 0); - _TICKET_UNIT_ = newTicketUnit; - emit ChangeTicketUnit(newTicketUnit); - } - - function retriveTicket(uint256 amount) external onlyOwner { - _TICKET_RESERVE_ = _TICKET_RESERVE_.sub(amount); - IERC20(_TICKET_).safeTransfer(_OWNER_, amount); - emit RetriveTicket(_OWNER_, amount); - } - - function burnTicket(uint256 amount) external onlyOwner { - _TICKET_RESERVE_ = _TICKET_RESERVE_.sub(amount); - IERC20(_TICKET_).safeTransfer(address(0), amount); - emit BurnTicket(amount); - } -} diff --git a/contracts/DODOMysteryBox/MysteryBoxV2.sol b/contracts/DODOMysteryBox/MysteryBoxV2.sol new file mode 100644 index 0000000..b50e50c --- /dev/null +++ b/contracts/DODOMysteryBox/MysteryBoxV2.sol @@ -0,0 +1,122 @@ +/* + Copyright 2021 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 +*/ +pragma solidity 0.6.9; +pragma experimental ABIEncoderV2; + +import {IERC20} from "../intf/IERC20.sol"; +import {SafeERC20} from "../lib/SafeERC20.sol"; +import {SafeMath} from "../lib/SafeMath.sol"; +import {Address} from "../external/utils/Address.sol"; +import {IRandomGenerator} from "../lib/RandomGenerator.sol"; +import {ERC1155} from "../external/ERC1155/ERC1155.sol"; +import {BaseMysteryBox} from "./BaseMysteryBox.sol"; + +//讨论:tokenId 与 ipfs 的 uri 关联关系 +contract MysteryBoxV2 is BaseMysteryBox, ERC1155 { + using SafeMath for uint256; + using SafeERC20 for IERC20; + using Address for address; + + // ============ Storage ============ + + uint256[] public _PROB_INTERVAL_; // index => Interval probability + uint256[][] public _TOKEN_ID_SET_; // Interval index => tokenIds + + // ============ Event ============= + event RedeemPrize(address account, uint256 tokenId); + + event SetProbInterval(); + event SetTokenIds(); + event SetTokenIdByIndex(uint256 index); + + + function init( + address[] memory contractList, //0 owner, 1 buyTicketToken, 2 feeModel, 3 dodoApproveProxy 4 randomGenerator + uint256[][] memory priceList, //0 priceTimeInterval, 1 priceSet, 2 sellAmount + uint256 redeemAllowedTime, + bool isRevealMode, + string memory baseUri, + uint256[] memory probIntervals, + uint256[][] memory tokenIds + ) public { + super._baseInit(contractList,priceList,redeemAllowedTime,isRevealMode); + _setURI(baseUri); + if(probIntervals.length > 0) _setProbInterval(probIntervals); + if(tokenIds.length > 0) _setTokenIds(tokenIds); + } + + function redeemTicket(uint256 ticketNum) external { + require(!address(msg.sender).isContract(), "ONLY_ALLOW_EOA"); + require(ticketNum >= 1 && ticketNum <= _USER_TICKETS_[msg.sender], "TICKET_NUM_INVALID"); + _USER_TICKETS_[msg.sender] = _USER_TICKETS_[msg.sender].sub(ticketNum); + _TOTAL_TICKETS_ = _TOTAL_TICKETS_.sub(ticketNum); + for (uint256 i = 0; i < ticketNum; i++) { + _redeemSinglePrize(msg.sender, i); + } + } + + // ============ Internal ============ + + function _redeemSinglePrize(address to, uint256 curNo) internal { + require(block.timestamp >= _REDEEM_ALLOWED_TIME_ && _REDEEM_ALLOWED_TIME_ != 0, "REDEEM_CLOSE"); + uint256 range = _PROB_INTERVAL_[_PROB_INTERVAL_.length - 1]; + uint256 random; + if(_IS_REVEAL_MODE_) { + require(_REVEAL_RNG_ != 0, "REVEAL_NOT_SET"); + random = uint256(keccak256(abi.encodePacked(_REVEAL_RNG_, msg.sender, _USER_TICKETS_[msg.sender].add(curNo + 1)))) % range; + }else { + random = IRandomGenerator(_RANDOM_GENERATOR_).random(gasleft()) % range; + } + uint256 i; + for (i = 0; i < _PROB_INTERVAL_.length; i++) { + if (random <= _PROB_INTERVAL_[i]) { + break; + } + } + require(_TOKEN_ID_SET_[i].length > 0, "EMPTY_TOKEN_ID_SET"); + uint256 tokenId = _TOKEN_ID_SET_[i][random % _TOKEN_ID_SET_[i].length]; + _mint(to, tokenId, 1, ""); + emit RedeemPrize(to, tokenId); + } + + function _setProbInterval(uint256[] memory probIntervals) internal { + require(probIntervals.length > 0, "PARAM_NOT_INVALID"); + for (uint256 i = 1; i < probIntervals.length; i++) { + require(probIntervals[i] > probIntervals[i - 1], "INTERVAL_INVALID"); + } + _PROB_INTERVAL_ = probIntervals; + emit SetProbInterval(); + } + + function _setTokenIds(uint256[][] memory tokenIds) internal { + require(tokenIds.length == _PROB_INTERVAL_.length, "PARAM_NOT_INVALID"); + for (uint256 i = 0; i < tokenIds.length; i++) { + require(tokenIds[i].length > 0, "INVALID"); + } + _TOKEN_ID_SET_ = tokenIds; + emit SetTokenIds(); + } + + // ================= Owner =================== + function redeemByOwner(uint256 ticketNum) external onlyOwner { + for (uint256 i = 0; i < ticketNum; i++) { + _redeemSinglePrize(msg.sender, i); + } + } + + function setProbInterval(uint256[] memory probIntervals) external notStart() onlyOwner { + _setProbInterval(probIntervals); + } + + function setTokenIds(uint256[][] memory tokenIds) external notStart() onlyOwner { + _setTokenIds(tokenIds); + } + + function setTokenIdByIndex(uint256 index, uint256[] memory tokenIds) external notStart() onlyOwner { + require(tokenIds.length > 0 && index < _TOKEN_ID_SET_.length,"PARAM_NOT_INVALID"); + _TOKEN_ID_SET_[index] = tokenIds; + emit SetTokenIdByIndex(index); + } +} diff --git a/contracts/DODOMysteryBox/MysteryBoxV3.sol b/contracts/DODOMysteryBox/MysteryBoxV3.sol new file mode 100644 index 0000000..7dcb033 --- /dev/null +++ b/contracts/DODOMysteryBox/MysteryBoxV3.sol @@ -0,0 +1,93 @@ +/* + Copyright 2021 DODO ZOO. + SPDX-License-Identifier: Apache-2.0 +*/ +pragma solidity 0.6.9; +pragma experimental ABIEncoderV2; + +import {IERC20} from "../intf/IERC20.sol"; +import {SafeERC20} from "../lib/SafeERC20.sol"; +import {SafeMath} from "../lib/SafeMath.sol"; +import {IRandomGenerator} from "../lib/RandomGenerator.sol"; +import {InitializableOwnable} from "../lib/InitializableOwnable.sol"; +import {Address} from "../external/utils/Address.sol"; +import {ERC721} from "../external/ERC721/ERC721.sol"; +import {BaseMysteryBox} from "./BaseMysteryBox.sol"; + +contract MysteryBoxV3 is BaseMysteryBox, ERC721 { + using SafeMath for uint256; + using SafeERC20 for IERC20; + using Address for address; + + // ============ Storage ============ + uint256[] public _TOKEN_IDS_; + + // ============ Event ============= + event RedeemPrize(address account, uint256 tokenId); + event SetTokenIds(); + + function init( + address[] memory contractList, //0 owner, 1 buyTicketToken, 2 feeModel, 3 dodoApproveProxy 4 randomGenerator + uint256[][] memory priceList, //0 priceTimeInterval, 1 priceSet, 2 sellAmount + uint256 redeemAllowedTime, + bool isRevealMode, + string memory name, + string memory symbol, + string memory baseUri, + uint256[] memory tokenIds + ) public { + super._baseInit(contractList,priceList,redeemAllowedTime,isRevealMode); + _name = name; + _symbol = symbol; + _baseUri = baseUri; + if(tokenIds.length > 0) _setTokenIds(tokenIds); + } + + function redeemPrize(uint256 ticketNum) external { + require(!address(msg.sender).isContract(), "ONLY_ALLOW_EOA"); + require(ticketNum >= 1 && ticketNum <= _USER_TICKETS_[msg.sender], "TICKET_NUM_INVALID"); + _USER_TICKETS_[msg.sender] = _USER_TICKETS_[msg.sender].sub(ticketNum); + _TOTAL_TICKETS_ = _TOTAL_TICKETS_.sub(ticketNum); + for (uint256 i = 0; i < ticketNum; i++) { + _redeemSinglePrize(msg.sender, i); + } + } + + // =============== Internal ================ + function _redeemSinglePrize(address to, uint256 curNo) internal { + require(block.timestamp >= _REDEEM_ALLOWED_TIME_ && _REDEEM_ALLOWED_TIME_ != 0, "REDEEM_CLOSE"); + uint256 range = _TOKEN_IDS_.length; + uint256 random; + if(_IS_REVEAL_MODE_) { + require(_REVEAL_RNG_ != 0, "REVEAL_NOT_SET"); + random = uint256(keccak256(abi.encodePacked(_REVEAL_RNG_, msg.sender, _USER_TICKETS_[msg.sender].add(curNo + 1)))) % range; + }else { + random = IRandomGenerator(_RANDOM_GENERATOR_).random(gasleft()) % range; + } + uint256 tokenId = _TOKEN_IDS_[random]; + + if(random != range - 1) { + _TOKEN_IDS_[random] = _TOKEN_IDS_[range - 1]; + } + _TOKEN_IDS_.pop(); + _mint(to, tokenId); + emit RedeemPrize(to, tokenId); + } + + function _setTokenIds(uint256[] memory ids) internal { + require(ids.length > 0, "PARAM_NOT_INVALID"); + _TOKEN_IDS_ = ids; + } + + // ================= Owner =================== + function redeemByOwner(uint256 ticketNum) external onlyOwner { + for (uint256 i = 0; i < ticketNum; i++) { + _redeemSinglePrize(msg.sender, i); + } + } + + function setTokenIds(uint256[] memory ids) external notStart() onlyOwner { + _setTokenIds(ids); + emit SetTokenIds(); + } +}