├── .gitignore ├── LICENSE ├── README.md ├── audit-reports ├── BEOSIN-2021-04-10.pdf ├── BEOSIN-2021-04-30.pdf └── BLES-Token.pdf ├── contracts ├── LinkAccessor.sol ├── Migrations.sol ├── NFTMaster.sol ├── NFTMasterProxy.sol ├── Staking.sol ├── Timelock.sol ├── VoteStaking.sol ├── interfaces │ ├── ILinkAccessor.sol │ ├── INFTMaster.sol │ ├── IUniswapV2Router01.sol │ ├── IUniswapV2Router02.sol │ └── IVoteStaking.sol ├── mock │ ├── MockERC20.sol │ ├── MockLinkAccessor.sol │ ├── MockNFT.sol │ └── MockNFTMaster.sol └── tokens │ └── BLES.sol ├── migrations ├── 1_initial_migration.js └── 2_deploy_contracts.js ├── package-lock.json ├── package.json ├── test ├── NFTMaster.test.js └── Staking.test.js └── truffle-config.js /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /build/ 3 | *.swp 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2010-2020 Google LLC. http://angularjs.org 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BlindBoxes.io 2 | 3 | # Testnet Smart Contracts (Rinkeby) 4 | 5 | - NFTMaster [0x48EDFef4b3f956136bCD84888793f6bc115a4675](https://rinkeby.etherscan.io/address/0x48EDFef4b3f956136bCD84888793f6bc115a4675) 6 | 7 | - LinkAccessor [0x2C8Cd318f2f79C86E686aced7Cd418b48c45BaDF](https://rinkeby.etherscan.io/address/0x2C8Cd318f2f79C86E686aced7Cd418b48c45BaDF) 8 | 9 | - MockUSDC [0x138DC345fdCa4898D772b0a65F479AcE20bb6E79](https://rinkeby.etherscan.io/address/0x138DC345fdCa4898D772b0a65F479AcE20bb6E79) 10 | 11 | - MockBLES [0x5B415B4d751a5274455DA472DDed0721058fD404](https://rinkeby.etherscan.io/address/0x5B415B4d751a5274455DA472DDed0721058fD404) 12 | 13 | 14 | # BCS Smart Contracts (Binance Smart Chain) 15 | 16 | - NFTMaster [0x22A1236Ad555cDb956148F64c7aD079fB112a6d6](https://bscscan.com/address/0x22A1236Ad555cDb956148F64c7aD079fB112a6d6) 17 | 18 | - BLES [0xe9e7cea3dedca5984780bafc599bd69add087d56](https://bscscan.com/address/0xe9e7cea3dedca5984780bafc599bd69add087d56) 19 | -------------------------------------------------------------------------------- /audit-reports/BEOSIN-2021-04-10.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlindBoxesNFT/blindboxes-contracts/60f2283be26f8803058f522f62ff7bcdf7865b85/audit-reports/BEOSIN-2021-04-10.pdf -------------------------------------------------------------------------------- /audit-reports/BEOSIN-2021-04-30.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlindBoxesNFT/blindboxes-contracts/60f2283be26f8803058f522f62ff7bcdf7865b85/audit-reports/BEOSIN-2021-04-30.pdf -------------------------------------------------------------------------------- /audit-reports/BLES-Token.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BlindBoxesNFT/blindboxes-contracts/60f2283be26f8803058f522f62ff7bcdf7865b85/audit-reports/BLES-Token.pdf -------------------------------------------------------------------------------- /contracts/LinkAccessor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.12; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 5 | import "@openzeppelin/contracts/utils/Context.sol"; 6 | import "@chainlink/contracts/src/v0.6/VRFConsumerBase.sol"; 7 | 8 | import "./interfaces/INFTMaster.sol"; 9 | import "./interfaces/ILinkAccessor.sol"; 10 | 11 | contract LinkAccessor is Context, VRFConsumerBase, ILinkAccessor { 12 | 13 | uint256 constant FEE = 10 ** 17; // 0.1 LINK 14 | 15 | bytes32 public linkKeyHash; 16 | 17 | address public link; 18 | INFTMaster public nftMaster; 19 | 20 | constructor( 21 | address vrfCoordinator_, 22 | address link_, 23 | bytes32 linkKeyHash_, 24 | INFTMaster nftMaster_ 25 | ) VRFConsumerBase(vrfCoordinator_, link_) public { 26 | link = link_; 27 | linkKeyHash = linkKeyHash_; 28 | nftMaster = nftMaster_; 29 | } 30 | 31 | function requestRandomness(uint256 userProvidedSeed_) public override returns(bytes32) { 32 | require(_msgSender() == address(nftMaster), "Not the right caller"); 33 | require(IERC20(link).balanceOf(address(this)) >= FEE, "Not enough LINK"); 34 | 35 | bytes32 requestId = requestRandomness(linkKeyHash, FEE, userProvidedSeed_); 36 | return requestId; 37 | } 38 | 39 | /** 40 | * Callback function used by VRF Coordinator 41 | */ 42 | function fulfillRandomness(bytes32 requestId, uint256 randomness) internal override { 43 | nftMaster.fulfillRandomness(requestId, randomness); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /contracts/Migrations.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity >=0.4.25 <0.7.0; 3 | 4 | contract Migrations { 5 | address public owner; 6 | uint public last_completed_migration; 7 | 8 | modifier restricted() { 9 | if (msg.sender == owner) _; 10 | } 11 | 12 | constructor() public { 13 | owner = msg.sender; 14 | } 15 | 16 | function setCompleted(uint completed) public restricted { 17 | last_completed_migration = completed; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/NFTMaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.12; 3 | 4 | import "@openzeppelin/contracts/access/Ownable.sol"; 5 | import "@openzeppelin/contracts/math/SafeMath.sol"; 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 8 | import "@openzeppelin/contracts/token/ERC721/IERC721.sol"; 9 | import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol"; 10 | 11 | import "./interfaces/ILinkAccessor.sol"; 12 | import "./interfaces/IUniswapV2Router02.sol"; 13 | 14 | // This contract is owned by Timelock. 15 | contract NFTMaster is Ownable, IERC721Receiver { 16 | 17 | using SafeERC20 for IERC20; 18 | using SafeMath for uint256; 19 | 20 | event CreateCollection(address _who, uint256 _collectionId); 21 | event PublishCollection(address _who, uint256 _collectionId); 22 | event UnpublishCollection(address _who, uint256 _collectionId); 23 | event NFTDeposit(address _who, address _tokenAddress, uint256 _tokenId); 24 | event NFTWithdraw(address _who, address _tokenAddress, uint256 _tokenId); 25 | event NFTClaim(address _who, address _tokenAddress, uint256 _tokenId); 26 | 27 | IERC20 public wETH; 28 | IERC20 public baseToken; 29 | IERC20 public blesToken; 30 | IERC20 public linkToken; 31 | 32 | uint256 public linkCost = 1e17; // 0.1 LINK 33 | ILinkAccessor public linkAccessor; 34 | 35 | bool public canDrawMultiple = true; 36 | 37 | // Platform fee. 38 | uint256 constant FEE_BASE = 10000; 39 | uint256 public feeRate = 500; // 5% 40 | 41 | address public feeTo; 42 | 43 | // Collection creating fee. 44 | uint256 public creatingFee = 0; // By default, 0 45 | 46 | IUniswapV2Router02 public router; 47 | 48 | uint256 public nextNFTId = 1e4; 49 | uint256 public nextCollectionId = 1e4; 50 | 51 | struct NFT { 52 | address tokenAddress; 53 | uint256 tokenId; 54 | address owner; 55 | uint256 price; 56 | uint256 paid; 57 | uint256 collectionId; 58 | uint256 indexInCollection; 59 | } 60 | 61 | // nftId => NFT 62 | mapping(uint256 => NFT) public allNFTs; 63 | 64 | // owner => nftId[] 65 | mapping(address => uint256[]) public nftsByOwner; 66 | 67 | // tokenAddress => tokenId => nftId 68 | mapping(address => mapping(uint256 => uint256)) public nftIdMap; 69 | 70 | struct Collection { 71 | address owner; 72 | string name; 73 | uint256 size; 74 | uint256 commissionRate; // for curator (owner) 75 | bool willAcceptBLES; 76 | 77 | // The following are runtime variables before publish 78 | uint256 totalPrice; 79 | uint256 averagePrice; 80 | uint256 fee; 81 | uint256 commission; 82 | 83 | // The following are runtime variables after publish 84 | uint256 publishedAt; // time that published. 85 | uint256 timesToCall; 86 | uint256 soldCount; 87 | } 88 | 89 | // collectionId => Collection 90 | mapping(uint256 => Collection) public allCollections; 91 | 92 | // owner => collectionId[] 93 | mapping(address => uint256[]) public collectionsByOwner; 94 | 95 | // collectionId => who => true/false 96 | mapping(uint256 => mapping(address => bool)) public isCollaborator; 97 | 98 | // collectionId => collaborators 99 | mapping(uint256 => address[]) public collaborators; 100 | 101 | // collectionId => nftId[] 102 | mapping(uint256 => uint256[]) public nftsByCollectionId; 103 | 104 | struct RequestInfo { 105 | uint256 collectionId; 106 | } 107 | 108 | mapping(bytes32 => RequestInfo) public requestInfoMap; 109 | 110 | struct Slot { 111 | address owner; 112 | uint256 size; 113 | } 114 | 115 | // collectionId => Slot[] 116 | mapping(uint256 => Slot[]) public slotMap; 117 | 118 | // collectionId => r[] 119 | mapping(uint256 => uint256[]) public nftMapping; 120 | 121 | uint256 public nftPriceFloor = 1e18; // 1 USDC 122 | uint256 public nftPriceCeil = 1e24; // 1M USDC 123 | uint256 public minimumCollectionSize = 3; // 3 blind boxes 124 | uint256 public maximumDuration = 14 days; // Refund if not sold out in 14 days. 125 | 126 | constructor() public { } 127 | 128 | function onERC721Received(address, address, uint256, bytes memory) public virtual override returns (bytes4) { 129 | return this.onERC721Received.selector; 130 | } 131 | 132 | function setWETH(IERC20 wETH_) external onlyOwner { 133 | wETH = wETH_; 134 | } 135 | 136 | function setLinkToken(IERC20 linkToken_) external onlyOwner { 137 | linkToken = linkToken_; 138 | } 139 | 140 | function setBaseToken(IERC20 baseToken_) external onlyOwner { 141 | baseToken = baseToken_; 142 | } 143 | 144 | function setBlesToken(IERC20 blesToken_) external onlyOwner { 145 | blesToken = blesToken_; 146 | } 147 | 148 | function setLinkAccessor(ILinkAccessor linkAccessor_) external onlyOwner { 149 | linkAccessor = linkAccessor_; 150 | } 151 | 152 | function setLinkCost(uint256 linkCost_) external onlyOwner { 153 | linkCost = linkCost_; 154 | } 155 | 156 | function setCanDrawMultiple(bool value_) external onlyOwner { 157 | canDrawMultiple = value_; 158 | } 159 | 160 | function setFeeRate(uint256 feeRate_) external onlyOwner { 161 | feeRate = feeRate_; 162 | } 163 | 164 | function setFeeTo(address feeTo_) external onlyOwner { 165 | feeTo = feeTo_; 166 | } 167 | 168 | function setCreatingFee(uint256 creatingFee_) external onlyOwner { 169 | creatingFee = creatingFee_; 170 | } 171 | 172 | function setUniswapV2Router(IUniswapV2Router02 router_) external onlyOwner { 173 | router = router_; 174 | } 175 | 176 | function setNFTPriceFloor(uint256 value_) external onlyOwner { 177 | require(value_ < nftPriceCeil, "should be higher than floor"); 178 | nftPriceFloor = value_; 179 | } 180 | 181 | function setNFTPriceCeil(uint256 value_) external onlyOwner { 182 | require(value_ > nftPriceFloor, "should be higher than floor"); 183 | nftPriceCeil = value_; 184 | } 185 | 186 | function setMinimumCollectionSize(uint256 size_) external onlyOwner { 187 | minimumCollectionSize = size_; 188 | } 189 | 190 | function setMaximumDuration(uint256 maximumDuration_) external onlyOwner { 191 | maximumDuration = maximumDuration_; 192 | } 193 | 194 | function _generateNextNFTId() private returns(uint256) { 195 | return ++nextNFTId; 196 | } 197 | 198 | function _generateNextCollectionId() private returns(uint256) { 199 | return ++nextCollectionId; 200 | } 201 | 202 | function _depositNFT(address tokenAddress_, uint256 tokenId_) private returns(uint256) { 203 | IERC721(tokenAddress_).safeTransferFrom(_msgSender(), address(this), tokenId_); 204 | 205 | NFT memory nft; 206 | nft.tokenAddress = tokenAddress_; 207 | nft.tokenId = tokenId_; 208 | nft.owner = _msgSender(); 209 | nft.collectionId = 0; 210 | nft.indexInCollection = 0; 211 | 212 | uint256 nftId; 213 | 214 | if (nftIdMap[tokenAddress_][tokenId_] > 0) { 215 | nftId = nftIdMap[tokenAddress_][tokenId_]; 216 | } else { 217 | nftId = _generateNextNFTId(); 218 | nftIdMap[tokenAddress_][tokenId_] = nftId; 219 | } 220 | 221 | allNFTs[nftId] = nft; 222 | nftsByOwner[_msgSender()].push(nftId); 223 | 224 | emit NFTDeposit(_msgSender(), tokenAddress_, tokenId_); 225 | return nftId; 226 | } 227 | 228 | function _withdrawNFT(address who_, uint256 nftId_, bool isClaim_) private { 229 | allNFTs[nftId_].owner = address(0); 230 | allNFTs[nftId_].collectionId = 0; 231 | 232 | address tokenAddress = allNFTs[nftId_].tokenAddress; 233 | uint256 tokenId = allNFTs[nftId_].tokenId; 234 | 235 | IERC721(tokenAddress).safeTransferFrom(address(this), who_, tokenId); 236 | 237 | if (isClaim_) { 238 | emit NFTClaim(who_, tokenAddress, tokenId); 239 | } else { 240 | emit NFTWithdraw(who_, tokenAddress, tokenId); 241 | } 242 | } 243 | 244 | function claimNFT(uint256 collectionId_, uint256 index_) external { 245 | Collection storage collection = allCollections[collectionId_]; 246 | 247 | require(collection.soldCount == collection.size, "Not finished"); 248 | 249 | address winner = getWinner(collectionId_, index_); 250 | 251 | require(winner == _msgSender(), "Only winner can claim"); 252 | 253 | uint256 nftId = nftsByCollectionId[collectionId_][index_]; 254 | 255 | require(allNFTs[nftId].collectionId == collectionId_, "Already claimed"); 256 | 257 | if (allNFTs[nftId].paid == 0) { 258 | if (collection.willAcceptBLES) { 259 | allNFTs[nftId].paid = allNFTs[nftId].price.mul( 260 | FEE_BASE.sub(collection.commissionRate)).div(FEE_BASE); 261 | IERC20(blesToken).safeTransfer(allNFTs[nftId].owner, allNFTs[nftId].paid); 262 | } else { 263 | allNFTs[nftId].paid = allNFTs[nftId].price.mul( 264 | FEE_BASE.sub(feeRate).sub(collection.commissionRate)).div(FEE_BASE); 265 | IERC20(baseToken).safeTransfer(allNFTs[nftId].owner, allNFTs[nftId].paid); 266 | } 267 | } 268 | 269 | _withdrawNFT(_msgSender(), nftId, true); 270 | } 271 | 272 | function claimRevenue(uint256 collectionId_, uint256 index_) external { 273 | Collection storage collection = allCollections[collectionId_]; 274 | 275 | require(collection.soldCount == collection.size, "Not finished"); 276 | 277 | uint256 nftId = nftsByCollectionId[collectionId_][index_]; 278 | 279 | require(allNFTs[nftId].owner == _msgSender() && allNFTs[nftId].collectionId > 0, "NFT not claimed"); 280 | 281 | if (allNFTs[nftId].paid == 0) { 282 | if (collection.willAcceptBLES) { 283 | allNFTs[nftId].paid = allNFTs[nftId].price.mul( 284 | FEE_BASE.sub(collection.commissionRate)).div(FEE_BASE); 285 | IERC20(blesToken).safeTransfer(allNFTs[nftId].owner, allNFTs[nftId].paid); 286 | } else { 287 | allNFTs[nftId].paid = allNFTs[nftId].price.mul( 288 | FEE_BASE.sub(feeRate).sub(collection.commissionRate)).div(FEE_BASE); 289 | IERC20(baseToken).safeTransfer(allNFTs[nftId].owner, allNFTs[nftId].paid); 290 | } 291 | } 292 | } 293 | 294 | function claimCommission(uint256 collectionId_) external { 295 | Collection storage collection = allCollections[collectionId_]; 296 | 297 | require(_msgSender() == collection.owner, "Only curator can claim"); 298 | require(collection.soldCount == collection.size, "Not finished"); 299 | 300 | if (collection.willAcceptBLES) { 301 | IERC20(blesToken).safeTransfer(collection.owner, collection.commission); 302 | } else { 303 | IERC20(baseToken).safeTransfer(collection.owner, collection.commission); 304 | } 305 | 306 | // Mark it claimed. 307 | collection.commission = 0; 308 | } 309 | 310 | function claimFee(uint256 collectionId_) external { 311 | require(feeTo != address(0), "Please set feeTo first"); 312 | 313 | Collection storage collection = allCollections[collectionId_]; 314 | 315 | require(collection.soldCount == collection.size, "Not finished"); 316 | require(!collection.willAcceptBLES, "No fee if the curator accepts BLES"); 317 | 318 | IERC20(baseToken).safeTransfer(feeTo, collection.fee); 319 | 320 | // Mark it claimed. 321 | collection.fee = 0; 322 | } 323 | 324 | function createCollection( 325 | string calldata name_, 326 | uint256 size_, 327 | uint256 commissionRate_, 328 | bool willAcceptBLES_, 329 | address[] calldata collaborators_ 330 | ) external { 331 | require(size_ >= minimumCollectionSize, "Size too small"); 332 | require(commissionRate_.add(feeRate) < FEE_BASE, "Too much commission"); 333 | 334 | if (creatingFee > 0) { 335 | // Charges BLES for creating the collection. 336 | IERC20(blesToken).safeTransfer(feeTo, creatingFee); 337 | } 338 | 339 | Collection memory collection; 340 | collection.owner = _msgSender(); 341 | collection.name = name_; 342 | collection.size = size_; 343 | collection.commissionRate = commissionRate_; 344 | collection.totalPrice = 0; 345 | collection.averagePrice = 0; 346 | collection.willAcceptBLES = willAcceptBLES_; 347 | collection.publishedAt = 0; 348 | 349 | uint256 collectionId = _generateNextCollectionId(); 350 | 351 | allCollections[collectionId] = collection; 352 | collectionsByOwner[_msgSender()].push(collectionId); 353 | collaborators[collectionId] = collaborators_; 354 | 355 | for (uint256 i = 0; i < collaborators_.length; ++i) { 356 | isCollaborator[collectionId][collaborators_[i]] = true; 357 | } 358 | 359 | emit CreateCollection(_msgSender(), collectionId); 360 | } 361 | 362 | function changeCollaborators(uint256 collectionId_, address[] calldata collaborators_) external { 363 | Collection storage collection = allCollections[collectionId_]; 364 | 365 | require(collection.owner == _msgSender(), "Needs collection owner"); 366 | require(!isPublished(collectionId_), "Collection already published"); 367 | 368 | uint256 i; 369 | 370 | for (i = 0; i < collaborators_.length; ++i) { 371 | isCollaborator[collectionId_][collaborators_[i]] = true; 372 | } 373 | 374 | for (i = 0; i < collaborators[collectionId_].length; ++i) { 375 | uint256 j; 376 | for (j = 0; j < collaborators_.length; ++j) { 377 | if (collaborators[collectionId_][i] == collaborators_[j]) { 378 | break; 379 | } 380 | } 381 | 382 | // If not found. 383 | if (j == collaborators_.length) { 384 | isCollaborator[collectionId_][collaborators[collectionId_][i]] = false; 385 | } 386 | } 387 | 388 | collaborators[collectionId_] = collaborators_; 389 | } 390 | 391 | function isPublished(uint256 collectionId_) public view returns(bool) { 392 | return allCollections[collectionId_].publishedAt > 0; 393 | } 394 | 395 | function _addNFTToCollection(uint256 nftId_, uint256 collectionId_, uint256 price_) private { 396 | Collection storage collection = allCollections[collectionId_]; 397 | 398 | require(allNFTs[nftId_].owner == _msgSender(), "Only NFT owner can add"); 399 | require(collection.owner == _msgSender() || 400 | isCollaborator[collectionId_][_msgSender()], "Needs collection owner or collaborator"); 401 | 402 | require(price_ >= nftPriceFloor && price_ <= nftPriceCeil, "Price not in range"); 403 | 404 | require(allNFTs[nftId_].collectionId == 0, "Already added"); 405 | require(!isPublished(collectionId_), "Collection already published"); 406 | require(nftsByCollectionId[collectionId_].length < collection.size, 407 | "collection full"); 408 | 409 | allNFTs[nftId_].price = price_; 410 | allNFTs[nftId_].collectionId = collectionId_; 411 | allNFTs[nftId_].indexInCollection = nftsByCollectionId[collectionId_].length; 412 | 413 | // Push to nftsByCollectionId. 414 | nftsByCollectionId[collectionId_].push(nftId_); 415 | 416 | collection.totalPrice = collection.totalPrice.add(price_); 417 | 418 | if (!collection.willAcceptBLES) { 419 | collection.fee = collection.fee.add(price_.mul(feeRate).div(FEE_BASE)); 420 | } 421 | 422 | collection.commission = collection.commission.add(price_.mul(collection.commissionRate).div(FEE_BASE)); 423 | } 424 | 425 | function addNFTToCollection(address tokenAddress_, uint256 tokenId_, uint256 collectionId_, uint256 price_) external { 426 | uint256 nftId = _depositNFT(tokenAddress_, tokenId_); 427 | _addNFTToCollection(nftId, collectionId_, price_); 428 | } 429 | 430 | function editNFTInCollection(uint256 nftId_, uint256 collectionId_, uint256 price_) external { 431 | Collection storage collection = allCollections[collectionId_]; 432 | 433 | require(collection.owner == _msgSender() || 434 | allNFTs[nftId_].owner == _msgSender(), "Needs collection owner or NFT owner"); 435 | 436 | require(price_ >= nftPriceFloor && price_ <= nftPriceCeil, "Price not in range"); 437 | 438 | require(allNFTs[nftId_].collectionId == collectionId_, "NFT not in collection"); 439 | require(!isPublished(collectionId_), "Collection already published"); 440 | 441 | collection.totalPrice = collection.totalPrice.add(price_).sub(allNFTs[nftId_].price); 442 | 443 | if (!collection.willAcceptBLES) { 444 | collection.fee = collection.fee.add( 445 | price_.mul(feeRate).div(FEE_BASE)).sub( 446 | allNFTs[nftId_].price.mul(feeRate).div(FEE_BASE)); 447 | } 448 | 449 | collection.commission = collection.commission.add( 450 | price_.mul(collection.commissionRate).div(FEE_BASE)).sub( 451 | allNFTs[nftId_].price.mul(collection.commissionRate).div(FEE_BASE)); 452 | 453 | allNFTs[nftId_].price = price_; // Change price. 454 | } 455 | 456 | function _removeNFTFromCollection(uint256 nftId_, uint256 collectionId_) private { 457 | Collection storage collection = allCollections[collectionId_]; 458 | 459 | require(allNFTs[nftId_].owner == _msgSender() || 460 | collection.owner == _msgSender(), 461 | "Only NFT owner or collection owner can remove"); 462 | require(allNFTs[nftId_].collectionId == collectionId_, "NFT not in collection"); 463 | require(!isPublished(collectionId_), "Collection already published"); 464 | 465 | collection.totalPrice = collection.totalPrice.sub(allNFTs[nftId_].price); 466 | 467 | if (!collection.willAcceptBLES) { 468 | collection.fee = collection.fee.sub( 469 | allNFTs[nftId_].price.mul(feeRate).div(FEE_BASE)); 470 | } 471 | 472 | collection.commission = collection.commission.sub( 473 | allNFTs[nftId_].price.mul(collection.commissionRate).div(FEE_BASE)); 474 | 475 | 476 | allNFTs[nftId_].collectionId = 0; 477 | 478 | // Removes from nftsByCollectionId 479 | uint256 index = allNFTs[nftId_].indexInCollection; 480 | uint256 lastNFTId = nftsByCollectionId[collectionId_][nftsByCollectionId[collectionId_].length - 1]; 481 | 482 | nftsByCollectionId[collectionId_][index] = lastNFTId; 483 | allNFTs[lastNFTId].indexInCollection = index; 484 | nftsByCollectionId[collectionId_].pop(); 485 | } 486 | 487 | function removeNFTFromCollection(uint256 nftId_, uint256 collectionId_) external { 488 | address nftOwner = allNFTs[nftId_].owner; 489 | _removeNFTFromCollection(nftId_, collectionId_); 490 | _withdrawNFT(nftOwner, nftId_, false); 491 | } 492 | 493 | function randomnessCount(uint256 size_) public pure returns(uint256){ 494 | uint256 i; 495 | for (i = 0; size_** i <= type(uint256).max / size_; i++) {} 496 | return i; 497 | } 498 | 499 | function publishCollection(uint256 collectionId_, address[] calldata path, uint256 amountInMax_, uint256 deadline_) external { 500 | Collection storage collection = allCollections[collectionId_]; 501 | 502 | require(collection.owner == _msgSender(), "Only owner can publish"); 503 | 504 | uint256 actualSize = nftsByCollectionId[collectionId_].length; 505 | require(actualSize >= minimumCollectionSize, "Not enough boxes"); 506 | 507 | collection.size = actualSize; // Fit the size. 508 | 509 | // Math.ceil(totalPrice / actualSize); 510 | collection.averagePrice = collection.totalPrice.add(actualSize.sub(1)).div(actualSize); 511 | collection.publishedAt = now; 512 | 513 | // Now buy LINK. Here is some math for calculating the time of calls needed from ChainLink. 514 | uint256 count = randomnessCount(actualSize); 515 | uint256 times = actualSize.add(count).sub(1).div(count); // Math.ceil 516 | 517 | if (linkCost > 0 && address(linkAccessor) != address(0)) { 518 | buyLink(times, path, amountInMax_, deadline_); 519 | } 520 | 521 | collection.timesToCall = times; 522 | 523 | emit PublishCollection(_msgSender(), collectionId_); 524 | } 525 | 526 | function unpublishCollection(uint256 collectionId_) external { 527 | // Anyone can call if the collection expires and not sold out. 528 | // Owner can unpublish a collection if nothing is sold out. 529 | 530 | Collection storage collection = allCollections[collectionId_]; 531 | 532 | if (_msgSender() != collection.owner || collection.soldCount > 0) { 533 | require(now > collection.publishedAt + maximumDuration, "Not expired yet"); 534 | require(collection.soldCount < collection.size, "Sold out"); 535 | } 536 | 537 | collection.publishedAt = 0; 538 | collection.soldCount = 0; 539 | 540 | // Now refund to the buyers. 541 | uint256 length = slotMap[collectionId_].length; 542 | for (uint256 i = 0; i < length; ++i) { 543 | Slot memory slot = slotMap[collectionId_][length.sub(i + 1)]; 544 | slotMap[collectionId_].pop(); 545 | 546 | if (collection.willAcceptBLES) { 547 | IERC20(blesToken).transfer(slot.owner, collection.averagePrice.mul(slot.size)); 548 | } else { 549 | IERC20(baseToken).transfer(slot.owner, collection.averagePrice.mul(slot.size)); 550 | } 551 | } 552 | 553 | emit UnpublishCollection(_msgSender(), collectionId_); 554 | } 555 | 556 | function buyLink(uint256 times_, address[] calldata path, uint256 amountInMax_, uint256 deadline_) internal virtual { 557 | require(path[path.length.sub(1)] == address(linkToken), "Last token must be LINK"); 558 | 559 | uint256 amountToBuy = linkCost.mul(times_); 560 | 561 | if (path.length == 1) { 562 | // Pay with LINK. 563 | linkToken.transferFrom(_msgSender(), address(linkAccessor), amountToBuy); 564 | } else { 565 | if (IERC20(path[0]).allowance(address(this), address(router)) < amountInMax_) { 566 | IERC20(path[0]).approve(address(router), amountInMax_); 567 | } 568 | 569 | uint256[] memory amounts = router.getAmountsIn(amountToBuy, path); 570 | IERC20(path[0]).transferFrom(_msgSender(), address(this), amounts[0]); 571 | 572 | // Pay with other token. 573 | router.swapTokensForExactTokens( 574 | amountToBuy, 575 | amountInMax_, 576 | path, 577 | address(linkAccessor), 578 | deadline_); 579 | } 580 | } 581 | 582 | function drawBoxes(uint256 collectionId_, uint256 times_) external { 583 | if (!canDrawMultiple) { 584 | require(times_ == 1, "Can draw only 1"); 585 | } 586 | 587 | require(isPublished(collectionId_), "Not published"); 588 | 589 | Collection storage collection = allCollections[collectionId_]; 590 | 591 | require(collection.soldCount.add(times_) <= collection.size, "Not enough left"); 592 | 593 | uint256 cost = collection.averagePrice.mul(times_); 594 | 595 | if (collection.willAcceptBLES) { 596 | IERC20(blesToken).safeTransferFrom(_msgSender(), address(this), cost); 597 | } else { 598 | IERC20(baseToken).safeTransferFrom(_msgSender(), address(this), cost); 599 | } 600 | 601 | Slot memory slot; 602 | slot.owner = _msgSender(); 603 | slot.size = times_; 604 | slotMap[collectionId_].push(slot); 605 | 606 | uint256 startFromIndex = collection.size.sub(collection.timesToCall); 607 | if (startFromIndex < collection.soldCount) { 608 | startFromIndex = collection.soldCount; 609 | } 610 | 611 | collection.soldCount = collection.soldCount.add(times_); 612 | 613 | for (uint256 i = startFromIndex; 614 | i < collection.soldCount; 615 | ++i) { 616 | requestRandomNumber(collectionId_, i.sub(startFromIndex)); 617 | } 618 | } 619 | 620 | function getWinner(uint256 collectionId_, uint256 nftIndex_) public view returns(address) { 621 | Collection storage collection = allCollections[collectionId_]; 622 | 623 | if (collection.soldCount < collection.size) { 624 | // Not sold all yet. 625 | return address(0); 626 | } 627 | 628 | uint256 size = collection.size; 629 | uint256 count = randomnessCount(size); 630 | 631 | uint256 lastRandomnessIndex = nftMapping[collectionId_].length.sub(1); 632 | uint256 lastR = nftMapping[collectionId_][lastRandomnessIndex]; 633 | 634 | // Use lastR as an offset for rotating the sequence, to make sure that 635 | // we need to wait for all boxes being sold. 636 | nftIndex_ = nftIndex_.add(lastR).mod(size); 637 | 638 | uint256 randomnessIndex = nftIndex_.div(count); 639 | 640 | uint256 r = nftMapping[collectionId_][randomnessIndex]; 641 | 642 | uint256 i; 643 | 644 | for (i = 0; i < nftIndex_.mod(count); ++i) { 645 | r /= size; 646 | } 647 | 648 | r %= size; 649 | 650 | // Iterate through all slots. 651 | for (i = 0; i < slotMap[collectionId_].length; ++i) { 652 | if (r >= slotMap[collectionId_][i].size) { 653 | r -= slotMap[collectionId_][i].size; 654 | } else { 655 | return slotMap[collectionId_][i].owner; 656 | } 657 | } 658 | 659 | require(false, "r overflow"); 660 | } 661 | 662 | function psuedoRandomness() public view returns(uint256) { 663 | return uint256(keccak256(abi.encodePacked( 664 | block.timestamp + block.difficulty + 665 | ((uint256(keccak256(abi.encodePacked(block.coinbase)))) / (now)) + 666 | block.gaslimit + 667 | ((uint256(keccak256(abi.encodePacked(_msgSender())))) / (now)) + 668 | block.number 669 | ))); 670 | } 671 | 672 | function requestRandomNumber(uint256 collectionId_, uint256 index_) private { 673 | if (address(linkAccessor) != address(0)) { 674 | bytes32 requestId = linkAccessor.requestRandomness(index_); 675 | requestInfoMap[requestId].collectionId = collectionId_; 676 | } else { 677 | // Uses psuedo random number instead, and doesn't involve request / callback. 678 | useRandomness(collectionId_, psuedoRandomness()); 679 | } 680 | } 681 | 682 | /** 683 | * Callback function used by VRF Coordinator 684 | */ 685 | function fulfillRandomness(bytes32 requestId, uint256 randomness) public { 686 | require(_msgSender() == address(linkAccessor), "Only linkAccessor can call"); 687 | 688 | uint256 collectionId = requestInfoMap[requestId].collectionId; 689 | useRandomness(collectionId, randomness); 690 | } 691 | 692 | function useRandomness( 693 | uint256 collectionId_, 694 | uint256 randomness_ 695 | ) private { 696 | uint256 size = allCollections[collectionId_].size; 697 | bool[] memory filled = new bool[](size); 698 | 699 | uint256 r; 700 | uint256 i; 701 | uint256 j; 702 | uint256 count = randomnessCount(size); 703 | 704 | for (i = 0; i < nftMapping[collectionId_].length; ++i) { 705 | r = nftMapping[collectionId_][i]; 706 | for (j = 0; j < count; ++j) { 707 | filled[r.mod(size)] = true; 708 | r = r.div(size); 709 | } 710 | } 711 | 712 | r = 0; 713 | 714 | uint256 t; 715 | uint256 remaining = size.sub(count.mul(nftMapping[collectionId_].length)); 716 | 717 | for (i = 0; i < count; ++i) { 718 | if (remaining == 0) { 719 | break; 720 | } 721 | 722 | t = randomness_.mod(remaining); 723 | randomness_ = randomness_.div(remaining); 724 | 725 | t = t.add(1); 726 | 727 | // Skips filled mappings. 728 | for (j = 0; j < size; ++j) { 729 | if (!filled[j]) { 730 | t = t.sub(1); 731 | } 732 | 733 | if (t == 0) { 734 | break; 735 | } 736 | } 737 | 738 | filled[j] = true; 739 | r = r.mul(size).add(j); 740 | remaining = remaining.sub(1); 741 | } 742 | 743 | nftMapping[collectionId_].push(r); 744 | } 745 | } 746 | -------------------------------------------------------------------------------- /contracts/NFTMasterProxy.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.12; 3 | 4 | interface INFTMaster { 5 | function allNFTs(uint256 collectionId_) external view returns( 6 | address, 7 | uint256, 8 | address, 9 | uint256, 10 | uint256, 11 | uint256, 12 | uint256 13 | ); 14 | 15 | function nftIdMap(address token_, uint256 id_) external view returns(uint256); 16 | 17 | function allCollections(uint256 collectionId_) external view returns( 18 | address, 19 | string memory, 20 | uint256, 21 | uint256, 22 | bool, 23 | uint256, 24 | uint256, 25 | uint256, 26 | uint256, 27 | uint256, 28 | uint256, 29 | uint256 30 | ); 31 | 32 | function isCollaborator(uint256 collectionId_, address who_) external view returns(bool); 33 | 34 | function collaborators(uint256 collectionId_, uint256 index_) external view returns(address); 35 | 36 | function nftsByCollectionId(uint256 collectionId_, uint256 index_) external view returns(uint256); 37 | 38 | function slotMap(uint256 collectionId_, uint256 index_) external view returns(address, uint256); 39 | 40 | function isPublished(uint256 collectionId_) external view returns(bool); 41 | 42 | function getWinner(uint256 collectionId_, uint256 nftIndex_) external view returns(address); 43 | } 44 | 45 | contract NFTMasterProxy { 46 | 47 | bytes32 public constant OLD_SLOT = keccak256("com.blindboxes.old"); 48 | bytes32 public constant NEW_SLOT = keccak256("com.blindboxes.new"); 49 | 50 | constructor(address oldMaster_, address newMaster_) public { 51 | setOldMaster(oldMaster_); 52 | setNewMaster(newMaster_); 53 | } 54 | 55 | function oldMaster() external view returns (address) { 56 | return loadOldMaster(); 57 | } 58 | 59 | function loadOldMaster() internal view returns (address) { 60 | address _impl; 61 | bytes32 position = OLD_SLOT; 62 | assembly { 63 | _impl := sload(position) 64 | } 65 | return _impl; 66 | } 67 | 68 | function setOldMaster(address oldMaster_) private { 69 | bytes32 position = OLD_SLOT; 70 | assembly { 71 | sstore(position, oldMaster_) 72 | } 73 | } 74 | 75 | function newMaster() external view returns (address) { 76 | return loadNewMaster(); 77 | } 78 | 79 | function loadNewMaster() internal view returns (address) { 80 | address _impl; 81 | bytes32 position = NEW_SLOT; 82 | assembly { 83 | _impl := sload(position) 84 | } 85 | return _impl; 86 | } 87 | 88 | function setNewMaster(address newMaster_) private { 89 | bytes32 position = NEW_SLOT; 90 | assembly { 91 | sstore(position, newMaster_) 92 | } 93 | } 94 | 95 | function delegatedFwd(address _dst, bytes memory _calldata) internal { 96 | // solium-disable-next-line security/no-inline-assembly 97 | assembly { 98 | let result := delegatecall( 99 | sub(gas(), 10000), 100 | _dst, 101 | add(_calldata, 0x20), 102 | mload(_calldata), 103 | 0, 104 | 0 105 | ) 106 | let size := returndatasize() 107 | 108 | let ptr := mload(0x40) 109 | returndatacopy(ptr, 0, size) 110 | 111 | // revert instead of invalid() bc if the underlying call failed with invalid() it already wasted gas. 112 | // if the call returned error data, forward it 113 | switch result 114 | case 0 { 115 | revert(ptr, size) 116 | } 117 | default { 118 | return(ptr, size) 119 | } 120 | } 121 | } 122 | 123 | function allNFTs(uint256 collectionId_) external view returns( 124 | address, 125 | uint256, 126 | address, 127 | uint256, 128 | uint256, 129 | uint256, 130 | uint256 131 | ) { 132 | if (collectionId_ < 1e4) { 133 | return INFTMaster(loadOldMaster()).allNFTs(collectionId_); 134 | } else { 135 | return INFTMaster(loadNewMaster()).allNFTs(collectionId_); 136 | } 137 | } 138 | 139 | function nftIdMap(address token_, uint256 id_) external view returns(uint256) { 140 | uint256 result = INFTMaster(loadOldMaster()).nftIdMap(token_, id_); 141 | if (result > 0) { 142 | return result; 143 | } 144 | 145 | return INFTMaster(loadNewMaster()).nftIdMap(token_, id_); 146 | } 147 | 148 | function allCollections(uint256 collectionId_) external view returns( 149 | address, 150 | string memory, 151 | uint256, 152 | uint256, 153 | bool, 154 | uint256, 155 | uint256, 156 | uint256, 157 | uint256, 158 | uint256, 159 | uint256, 160 | uint256 161 | ) { 162 | if (collectionId_ < 1e4) { 163 | return INFTMaster(loadOldMaster()).allCollections(collectionId_); 164 | } else { 165 | return INFTMaster(loadNewMaster()).allCollections(collectionId_); 166 | } 167 | } 168 | 169 | function isCollaborator(uint256 collectionId_, address who_) external view returns(bool) { 170 | if (collectionId_ < 1e4) { 171 | return INFTMaster(loadOldMaster()).isCollaborator(collectionId_, who_); 172 | } else { 173 | return INFTMaster(loadNewMaster()).isCollaborator(collectionId_, who_); 174 | } 175 | } 176 | 177 | function collaborators(uint256 collectionId_, uint256 index_) external view returns(address) { 178 | if (collectionId_ < 1e4) { 179 | return INFTMaster(loadOldMaster()).collaborators(collectionId_, index_); 180 | } else { 181 | return INFTMaster(loadNewMaster()).collaborators(collectionId_, index_); 182 | } 183 | } 184 | 185 | function nftsByCollectionId(uint256 collectionId_, uint256 index_) external view returns(uint256) { 186 | if (collectionId_ < 1e4) { 187 | return INFTMaster(loadOldMaster()).nftsByCollectionId(collectionId_, index_); 188 | } else { 189 | return INFTMaster(loadNewMaster()).nftsByCollectionId(collectionId_, index_); 190 | } 191 | } 192 | 193 | function slotMap(uint256 collectionId_, uint256 index_) external view returns(address, uint256) { 194 | if (collectionId_ < 1e4) { 195 | return INFTMaster(loadOldMaster()).slotMap(collectionId_, index_); 196 | } else { 197 | return INFTMaster(loadNewMaster()).slotMap(collectionId_, index_); 198 | } 199 | } 200 | 201 | function isPublished(uint256 collectionId_) external view returns(bool) { 202 | if (collectionId_ < 1e4) { 203 | return INFTMaster(loadOldMaster()).isPublished(collectionId_); 204 | } else { 205 | return INFTMaster(loadNewMaster()).isPublished(collectionId_); 206 | } 207 | } 208 | 209 | function getWinner(uint256 collectionId_, uint256 nftIndex_) public view returns(address) { 210 | if (collectionId_ < 1e4) { 211 | return INFTMaster(loadOldMaster()).getWinner(collectionId_, nftIndex_); 212 | } else { 213 | return INFTMaster(loadNewMaster()).getWinner(collectionId_, nftIndex_); 214 | } 215 | } 216 | 217 | function delegatedFwdByCollectionId(uint256 collectionId_) internal { 218 | if (collectionId_ < 1e4) { 219 | delegatedFwd(loadOldMaster(), msg.data); 220 | } else { 221 | delegatedFwd(loadNewMaster(), msg.data); 222 | } 223 | } 224 | 225 | function claimNFT(uint256 collectionId_, uint256 index_) external { 226 | delegatedFwdByCollectionId(collectionId_); 227 | } 228 | 229 | function claimRevenue(uint256 collectionId_, uint256 index_) external { 230 | delegatedFwdByCollectionId(collectionId_); 231 | } 232 | 233 | function claimCommission(uint256 collectionId_) external { 234 | delegatedFwdByCollectionId(collectionId_); 235 | } 236 | 237 | function claimFee(uint256 collectionId_) external { 238 | delegatedFwdByCollectionId(collectionId_); 239 | } 240 | 241 | function createCollection( 242 | string calldata name_, 243 | uint256 size_, 244 | uint256 commissionRate_, 245 | bool willAcceptBLES_, 246 | address[] calldata collaborators_ 247 | ) external { 248 | delegatedFwd(loadNewMaster(), msg.data); 249 | } 250 | 251 | function changeCollaborators(uint256 collectionId_, address[] calldata collaborators_) external { 252 | delegatedFwdByCollectionId(collectionId_); 253 | } 254 | 255 | function addNFTToCollection(address tokenAddress_, uint256 tokenId_, uint256 collectionId_, uint256 price_) external { 256 | delegatedFwdByCollectionId(collectionId_); 257 | } 258 | 259 | function editNFTInCollection(uint256 nftId_, uint256 collectionId_, uint256 price_) external { 260 | delegatedFwdByCollectionId(collectionId_); 261 | } 262 | 263 | function removeNFTFromCollection(uint256 nftId_, uint256 collectionId_) external { 264 | delegatedFwdByCollectionId(collectionId_); 265 | } 266 | 267 | function publishCollection(uint256 collectionId_, address[] calldata path, uint256 amountInMax_, uint256 deadline_) external { 268 | delegatedFwdByCollectionId(collectionId_); 269 | } 270 | 271 | function unpublishCollection(uint256 collectionId_) external { 272 | delegatedFwdByCollectionId(collectionId_); 273 | } 274 | 275 | function drawBoxes(uint256 collectionId_, uint256 times_) external { 276 | delegatedFwdByCollectionId(collectionId_); 277 | } 278 | } 279 | -------------------------------------------------------------------------------- /contracts/Staking.sol: -------------------------------------------------------------------------------- 1 | 2 | // SPDX-License-Identifier: MIT 3 | 4 | pragma solidity 0.6.12; 5 | pragma experimental ABIEncoderV2; 6 | 7 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 8 | import "@openzeppelin/contracts/token/ERC20/SafeERC20.sol"; 9 | import "@openzeppelin/contracts/utils/EnumerableSet.sol"; 10 | import "@openzeppelin/contracts/math/SafeMath.sol"; 11 | import "@openzeppelin/contracts/access/Ownable.sol"; 12 | 13 | import "./interfaces/IVoteStaking.sol"; 14 | import "./tokens/BLES.sol"; 15 | 16 | contract Staking is Ownable { 17 | 18 | using SafeMath for uint256; 19 | using SafeERC20 for IERC20; 20 | 21 | uint256 constant PER_SHARE_SIZE = 1e12; 22 | 23 | // Info of each user. 24 | struct UserInfo { 25 | uint256 amount; // How many tokens the user has provided. 26 | uint256 rewardAmount; 27 | uint256 rewardDebt; // Reward debt. 28 | } 29 | 30 | // Info of each user that stakes tokens. 31 | mapping(uint256 => mapping(address => UserInfo)) public userInfo; 32 | 33 | // Info of each pool. 34 | struct PoolInfo { 35 | IERC20 token; // Address of token contract. 36 | uint256 totalBalance; 37 | uint256 rewardPerBlock; 38 | uint256 startBlock; 39 | uint256 endBlock; 40 | uint256 lastRewardBlock; 41 | uint256 accRewardPerShare; // Accumulated BLES per share, times PER_SHARE_SIZE. 42 | uint256 lockForDays; 43 | } 44 | 45 | // Info of each pool. 46 | PoolInfo[] public poolInfo; 47 | 48 | struct LockInfo { 49 | uint256 pointer; 50 | uint256 amount; 51 | } 52 | 53 | // who => poolId => LockInfo 54 | mapping(address => mapping(uint256 => LockInfo)) public userLockInfo; 55 | 56 | // who => poolId => pointer => amount 57 | mapping(address => mapping(uint256 => mapping(uint256 => uint256))) public lockedAmountMap; 58 | 59 | 60 | // Claim request. 61 | struct ClaimRequest { 62 | uint256 time; 63 | uint256 amount; 64 | bool executed; 65 | } 66 | 67 | // who => ClaimRequest[] 68 | mapping(address => ClaimRequest[]) public claimRequestMap; 69 | 70 | uint256 public claimWaitTime = 7 days; 71 | 72 | // The bles token 73 | BLES public blesToken; 74 | uint256 public blesPrincipal; 75 | 76 | event Deposit(address indexed user, uint256 indexed pid, uint256 amount); 77 | event Withdraw(address indexed user, uint256 indexed pid, uint256 amount); 78 | event ClaimNow(address indexed user, uint256 indexed pid, uint256 amount); 79 | event ClaimLater(address indexed user, uint256 indexed pid, uint256 amount, uint256 requestIndex); 80 | event ClaimLaterExecution(address indexed user, uint256 amount, uint256 requestIndex); 81 | 82 | // VoteStaking for extra mining rewards. 83 | IVoteStaking public voteStaking; 84 | 85 | uint256 public maximumVotingBlocks = 86400; // Approximately 3 days in BSC. 86 | 87 | struct Proposal { 88 | 89 | string description; 90 | uint256 startBlock; 91 | uint256 endBlock; 92 | uint256 optionCount; 93 | 94 | // optionIndex => count 95 | mapping (uint256 => uint256) optionVotes; 96 | 97 | // who => optionIndex => count 98 | mapping (address => mapping (uint256 => uint256)) userOptionVotes; 99 | } 100 | 101 | Proposal[] public proposals; 102 | 103 | event Vote(address indexed user, uint256 indexed _pid, uint256 indexed proposalIndex, uint256 optionIndex, uint256 votes); 104 | 105 | // who => block number 106 | mapping(address => uint256) public userVoteEndBlock; 107 | 108 | constructor( 109 | BLES _bles 110 | ) public { 111 | blesToken = _bles; 112 | } 113 | 114 | function setClaimWaitTime(uint256 _time) external onlyOwner { 115 | claimWaitTime = _time; 116 | } 117 | 118 | function setVoteStaking(IVoteStaking _voteStaking) external onlyOwner { 119 | voteStaking = _voteStaking; 120 | } 121 | 122 | function setMaximumVotingBlocks(uint256 _maximumVotingBlocks) external onlyOwner { 123 | maximumVotingBlocks = _maximumVotingBlocks; 124 | } 125 | 126 | function poolLength() external view returns (uint256) { 127 | return poolInfo.length; 128 | } 129 | 130 | function isBles(IERC20 _token) public view returns(bool) { 131 | return address(_token) == address(blesToken); 132 | } 133 | 134 | function hasVoteStaking() public view returns(bool) { 135 | return address(voteStaking) != address(0); 136 | } 137 | 138 | // Add a new lp to the pool. Can only be called by the owner. 139 | // XXX DO NOT add the same LP token more than once. Reward will be messed up if you do. 140 | function add( 141 | IERC20 _token, 142 | uint256 _rewardPerBlock, 143 | uint256 _startBlock, 144 | uint256 _endBlock, 145 | uint256 _lockForDays, 146 | bool _withUpdate 147 | ) external onlyOwner { 148 | if (_withUpdate) { 149 | massUpdatePools(); 150 | } 151 | 152 | poolInfo.push( 153 | PoolInfo({ 154 | token: _token, 155 | totalBalance: 0, 156 | rewardPerBlock: _rewardPerBlock, 157 | startBlock: _startBlock, 158 | endBlock: _endBlock, 159 | lastRewardBlock: 0, 160 | accRewardPerShare: 0, 161 | lockForDays: _lockForDays 162 | }) 163 | ); 164 | } 165 | 166 | // Update the given pool's SUSHI allocation point. Can only be called by the owner. 167 | function set( 168 | uint256 _pid, 169 | uint256 _rewardPerBlock, 170 | uint256 _startBlock, 171 | uint256 _endBlock, 172 | uint256 _lockForDays, 173 | bool _withUpdate 174 | ) external onlyOwner { 175 | if (_withUpdate) { 176 | massUpdatePools(); 177 | } 178 | 179 | poolInfo[_pid].rewardPerBlock = _rewardPerBlock; 180 | poolInfo[_pid].startBlock = _startBlock; 181 | poolInfo[_pid].endBlock = _endBlock; 182 | poolInfo[_pid].lockForDays = _lockForDays; 183 | } 184 | 185 | // Return reward multiplier over the given _from to _to block. 186 | function getReward(uint256 _pid, uint256 _from, uint256 _to) 187 | public 188 | view 189 | returns (uint256) 190 | { 191 | if (_to <= _from || _from > poolInfo[_pid].endBlock || _to < poolInfo[_pid].startBlock) { 192 | return 0; 193 | } 194 | 195 | uint256 startBlock = _from < poolInfo[_pid].startBlock ? poolInfo[_pid].startBlock : _from; 196 | uint256 endBlock = _to < poolInfo[_pid].endBlock ? _to : poolInfo[_pid].endBlock; 197 | return endBlock.sub(startBlock).mul(poolInfo[_pid].rewardPerBlock); 198 | } 199 | 200 | // View function to see pending BLES on frontend. 201 | function pendingReward(uint256 _pid, address _user) 202 | external 203 | view 204 | returns (uint256) 205 | { 206 | PoolInfo storage pool = poolInfo[_pid]; 207 | UserInfo storage user = userInfo[_pid][_user]; 208 | 209 | uint256 accRewardPerShare = pool.accRewardPerShare; 210 | 211 | if (block.number > pool.lastRewardBlock && pool.totalBalance > 0) { 212 | uint256 reward = getReward(_pid, pool.lastRewardBlock, block.number); 213 | accRewardPerShare = accRewardPerShare.add( 214 | reward.mul(PER_SHARE_SIZE).div(pool.totalBalance) 215 | ); 216 | } 217 | 218 | uint256 extra = 0; 219 | if (isBles(pool.token) && hasVoteStaking()) { 220 | extra = voteStaking.pendingReward(_pid, _user); 221 | } 222 | 223 | return user.amount.mul(accRewardPerShare).div( 224 | PER_SHARE_SIZE).sub(user.rewardDebt).add(user.rewardAmount).add(extra); 225 | } 226 | 227 | // Update reward vairables for all pools. Be careful of gas spending! 228 | function massUpdatePools() public { 229 | uint256 length = poolInfo.length; 230 | for (uint256 pid = 0; pid < length; ++pid) { 231 | updatePool(pid); 232 | } 233 | } 234 | 235 | // Update reward variables of the given pool to be up-to-date. 236 | function updatePool(uint256 _pid) public { 237 | PoolInfo storage pool = poolInfo[_pid]; 238 | if (block.number <= pool.lastRewardBlock) { 239 | return; 240 | } 241 | if (pool.totalBalance == 0) { 242 | pool.lastRewardBlock = block.number; 243 | return; 244 | } 245 | 246 | uint256 reward = getReward(_pid, pool.lastRewardBlock, block.number); 247 | 248 | pool.accRewardPerShare = pool.accRewardPerShare.add( 249 | reward.mul(PER_SHARE_SIZE).div(pool.totalBalance) 250 | ); 251 | 252 | pool.lastRewardBlock = block.number; 253 | 254 | if (isBles(pool.token) && hasVoteStaking()) { 255 | voteStaking.updatePool(_pid); 256 | } 257 | } 258 | 259 | function unlock(address _who, uint256 _pid) public { 260 | uint256 lockForDays = poolInfo[_pid].lockForDays; 261 | uint256 stopAtPointer = (now / 86400).sub(lockForDays); 262 | LockInfo storage lockInfo = userLockInfo[_who][_pid]; 263 | 264 | if (lockForDays == 0 || lockInfo.pointer == 0) { 265 | return; 266 | } 267 | 268 | uint256 pointer; 269 | for (pointer = lockInfo.pointer; pointer <= stopAtPointer; ++pointer) { 270 | if (lockedAmountMap[_who][_pid][pointer] > 0) { 271 | lockInfo.amount = lockInfo.amount.add(lockedAmountMap[_who][_pid][pointer]); 272 | delete lockedAmountMap[_who][_pid][pointer]; 273 | } 274 | } 275 | 276 | lockInfo.pointer = pointer; 277 | } 278 | 279 | function getUnlockAmount(address _who, uint256 _pid) external view returns(uint256) { 280 | uint256 lockForDays = poolInfo[_pid].lockForDays; 281 | uint256 stopAtPointer = (now / 86400).sub(lockForDays); 282 | LockInfo storage lockInfo = userLockInfo[_who][_pid]; 283 | 284 | if (lockForDays == 0 || lockInfo.pointer == 0) { 285 | return 0; 286 | } 287 | 288 | uint256 result = lockInfo.amount; 289 | for (uint256 pointer = lockInfo.pointer; pointer <= stopAtPointer; ++pointer) { 290 | if (lockedAmountMap[_who][_pid][pointer] > 0) { 291 | result = result.add(lockedAmountMap[_who][_pid][pointer]); 292 | } 293 | } 294 | 295 | return result; 296 | } 297 | 298 | function getUnlockArray(address _who, uint256 _pid, uint256 _offset, uint256 _limit) external view returns(LockInfo[] memory) { 299 | uint256 lockForDays = poolInfo[_pid].lockForDays; 300 | LockInfo storage lockInfo = userLockInfo[_who][_pid]; 301 | uint256 today = (now / 86400); 302 | 303 | if (lockForDays == 0 || lockInfo.pointer == 0) { 304 | return new LockInfo[](0); 305 | } 306 | 307 | uint256 count = 0; 308 | uint256 pointer; 309 | for (pointer = lockInfo.pointer + _offset; 310 | pointer <= today && pointer < lockInfo.pointer + _offset + _limit; 311 | ++pointer) { 312 | if (lockedAmountMap[_who][_pid][pointer] > 0) { 313 | ++count; 314 | } 315 | } 316 | 317 | LockInfo[] memory results = new LockInfo[](count); 318 | 319 | count = 0; 320 | for (pointer = lockInfo.pointer + _offset; 321 | pointer <= today && pointer < lockInfo.pointer + _offset + _limit; 322 | ++pointer) { 323 | if (lockedAmountMap[_who][_pid][pointer] > 0) { 324 | results[count].pointer = pointer; 325 | results[count].amount = lockedAmountMap[_who][_pid][pointer]; 326 | ++count; 327 | } 328 | } 329 | 330 | return results; 331 | } 332 | 333 | // Deposit tokens for BLES allocation. 334 | function deposit(uint256 _pid, uint256 _amount) external { 335 | PoolInfo storage pool = poolInfo[_pid]; 336 | UserInfo storage user = userInfo[_pid][msg.sender]; 337 | updatePool(_pid); 338 | 339 | if (user.amount > 0) { 340 | uint256 pending = 341 | user.amount.mul(pool.accRewardPerShare).div(PER_SHARE_SIZE).sub( 342 | user.rewardDebt 343 | ); 344 | 345 | user.rewardAmount = user.rewardAmount.add(pending); 346 | } 347 | 348 | pool.token.safeTransferFrom( 349 | address(msg.sender), 350 | address(this), 351 | _amount 352 | ); 353 | 354 | pool.totalBalance = pool.totalBalance.add(_amount); 355 | 356 | user.amount = user.amount.add(_amount); 357 | user.rewardDebt = user.amount.mul(pool.accRewardPerShare).div(PER_SHARE_SIZE); 358 | 359 | emit Deposit(msg.sender, _pid, _amount); 360 | 361 | if (isBles(pool.token)) { 362 | blesPrincipal = blesPrincipal.add(_amount); 363 | } 364 | 365 | if (pool.lockForDays > 0) { 366 | LockInfo storage lockInfo = userLockInfo[msg.sender][_pid]; 367 | 368 | uint256 currentPointer = now / 86400; 369 | lockedAmountMap[msg.sender][_pid][currentPointer] = _amount; 370 | if (lockInfo.pointer == 0) { 371 | lockInfo.pointer = currentPointer; 372 | } 373 | 374 | unlock(msg.sender, _pid); 375 | } 376 | } 377 | 378 | // Withdraw tokens. 379 | function withdraw(uint256 _pid, uint256 _amount) external { 380 | PoolInfo storage pool = poolInfo[_pid]; 381 | UserInfo storage user = userInfo[_pid][msg.sender]; 382 | 383 | if (isBles(pool.token) && hasVoteStaking()) { 384 | require(block.number >= userVoteEndBlock[msg.sender] || 385 | user.amount.sub(voteStaking.getUserStakedAmount(_pid, msg.sender)) >= _amount, 386 | "Withdraw more than staked - locked"); 387 | 388 | if (block.number >= userVoteEndBlock[msg.sender]) { 389 | voteStaking.withdraw(_pid, msg.sender); 390 | } 391 | } else { 392 | require(user.amount >= _amount, "Withdraw more than staked"); 393 | } 394 | 395 | if (pool.lockForDays > 0) { 396 | unlock(msg.sender, _pid); 397 | 398 | LockInfo storage lockInfo = userLockInfo[msg.sender][_pid]; 399 | require(lockInfo.amount >= _amount, "Please wait for unlock"); 400 | lockInfo.amount = lockInfo.amount.sub(_amount); 401 | } 402 | 403 | updatePool(_pid); 404 | 405 | if (user.amount > 0) { 406 | uint256 pending = 407 | user.amount.mul(pool.accRewardPerShare).div(PER_SHARE_SIZE).sub( 408 | user.rewardDebt 409 | ); 410 | user.rewardAmount = user.rewardAmount.add(pending); 411 | } 412 | 413 | user.amount = user.amount.sub(_amount); 414 | user.rewardDebt = user.amount.mul(pool.accRewardPerShare).div(PER_SHARE_SIZE); 415 | 416 | pool.token.safeTransfer(address(msg.sender), _amount); 417 | pool.totalBalance = pool.totalBalance.sub(_amount); 418 | 419 | emit Withdraw(msg.sender, _pid, _amount); 420 | 421 | if (isBles(pool.token)) { 422 | blesPrincipal = blesPrincipal.sub(_amount); 423 | } 424 | } 425 | 426 | // Withdraw tokens. 427 | function withdrawEarly(uint256 _pid, uint256 _pointer, uint256 _amount) external { 428 | PoolInfo storage pool = poolInfo[_pid]; 429 | UserInfo storage user = userInfo[_pid][msg.sender]; 430 | 431 | uint256 currentPointer = now / 86400; 432 | 433 | require(isBles(pool.token), "Must be BLES"); 434 | require(pool.lockForDays > 0, "Must have lockForDays"); 435 | require(currentPointer >= _pointer, "Point must be old"); 436 | require(_pointer + pool.lockForDays >= currentPointer, "Not unlocked yet"); 437 | 438 | if (hasVoteStaking()) { 439 | require(block.number >= userVoteEndBlock[msg.sender] || 440 | user.amount.sub(voteStaking.getUserStakedAmount(_pid, msg.sender)) >= _amount, 441 | "Withdraw more than staked - locked"); 442 | 443 | if (block.number >= userVoteEndBlock[msg.sender]) { 444 | voteStaking.withdraw(_pid, msg.sender); 445 | } 446 | } 447 | 448 | unlock(msg.sender, _pid); 449 | require(lockedAmountMap[msg.sender][_pid][_pointer] >= _amount, "_amount too large"); 450 | lockedAmountMap[msg.sender][_pid][_pointer] = lockedAmountMap[msg.sender][_pid][_pointer].sub(_amount); 451 | 452 | updatePool(_pid); 453 | 454 | if (user.amount > 0) { 455 | uint256 pending = 456 | user.amount.mul(pool.accRewardPerShare).div(PER_SHARE_SIZE).sub( 457 | user.rewardDebt 458 | ); 459 | user.rewardAmount = user.rewardAmount.add(pending); 460 | } 461 | 462 | user.amount = user.amount.sub(_amount); 463 | user.rewardDebt = user.amount.mul(pool.accRewardPerShare).div(PER_SHARE_SIZE); 464 | 465 | uint256 realAmount = _amount.mul(currentPointer.sub(_pointer)).div(pool.lockForDays); 466 | 467 | // Transfer. 468 | pool.token.safeTransfer(address(msg.sender), realAmount); 469 | 470 | // Burn. 471 | blesToken.burn(_amount.sub(realAmount)); 472 | 473 | pool.totalBalance = pool.totalBalance.sub(_amount); 474 | 475 | emit Withdraw(msg.sender, _pid, _amount); 476 | 477 | // isBles 478 | blesPrincipal = blesPrincipal.sub(_amount); 479 | } 480 | 481 | // claim reward immediately 482 | function claimNow(uint256 _pid, uint256 _amount) external { 483 | PoolInfo storage pool = poolInfo[_pid]; 484 | UserInfo storage user = userInfo[_pid][msg.sender]; 485 | 486 | // Make sure we don't claim user's principal. 487 | uint256 balance = blesToken.balanceOf(address(this)); 488 | require(balance.sub(_amount) >= blesPrincipal, "Only claim rewards"); 489 | 490 | uint256 extra = 0; 491 | if (isBles(pool.token) && hasVoteStaking()) { 492 | extra = voteStaking.claim(_pid, msg.sender); 493 | } 494 | 495 | updatePool(_pid); 496 | 497 | uint256 pending = user.amount.mul(pool.accRewardPerShare).div( 498 | PER_SHARE_SIZE).sub(user.rewardDebt); 499 | uint256 rewardTotal = user.rewardAmount.add(pending).add(extra); 500 | require(rewardTotal >= _amount, "Not enough reward"); 501 | 502 | // Burn 50%. 503 | uint256 rewardBurn = _amount.div(2); 504 | blesToken.burn(rewardBurn); 505 | blesToken.transfer(address(msg.sender), _amount.sub(rewardBurn)); 506 | 507 | user.rewardAmount = rewardTotal.sub(_amount); 508 | user.rewardDebt = user.amount.mul(pool.accRewardPerShare).div(PER_SHARE_SIZE); 509 | 510 | emit ClaimNow(msg.sender, _pid, _amount); 511 | } 512 | 513 | // Request to claim reward later. 514 | function claimLater(uint256 _pid, uint256 _amount) external { 515 | PoolInfo storage pool = poolInfo[_pid]; 516 | UserInfo storage user = userInfo[_pid][msg.sender]; 517 | 518 | // Make sure we don't claim user's principal. 519 | uint256 balance = blesToken.balanceOf(address(this)); 520 | require(balance.sub(_amount) >= blesPrincipal, "Only claim rewards"); 521 | 522 | uint256 extra = 0; 523 | if (isBles(pool.token) && hasVoteStaking()) { 524 | extra = voteStaking.claim(_pid, msg.sender); 525 | } 526 | 527 | updatePool(_pid); 528 | 529 | uint256 pending = user.amount.mul(pool.accRewardPerShare).div( 530 | PER_SHARE_SIZE).sub(user.rewardDebt); 531 | uint256 rewardTotal = user.rewardAmount.add(pending).add(extra); 532 | require(rewardTotal >= _amount, "Not enough reward"); 533 | 534 | claimRequestMap[msg.sender].push(ClaimRequest({ 535 | time: now, 536 | amount: _amount, 537 | executed: false 538 | })); 539 | 540 | user.rewardAmount = rewardTotal.sub(_amount); 541 | user.rewardDebt = user.amount.mul(pool.accRewardPerShare).div(PER_SHARE_SIZE); 542 | 543 | emit ClaimLater(msg.sender, _pid, _amount, claimRequestMap[msg.sender].length.sub(1)); 544 | } 545 | 546 | function claimLaterReady(uint256 _index) external { 547 | ClaimRequest storage request = claimRequestMap[msg.sender][_index]; 548 | 549 | require(request.amount > 0, "Not request found"); 550 | require(now >= request.time.add(claimWaitTime), "Not ready yet"); 551 | require(!request.executed, "Already executed"); 552 | 553 | blesToken.transfer(address(msg.sender), request.amount); 554 | request.executed = true; 555 | 556 | emit ClaimLaterExecution(msg.sender, request.amount, _index); 557 | } 558 | 559 | // Voting should have no overlaps. 560 | function addProposal( 561 | string memory _description, 562 | uint256 _startBlock, 563 | uint256 _endBlock, 564 | uint256 _optionCount, 565 | uint256[] memory _pidArray, 566 | uint256[] memory _rewardPerBlockArray 567 | ) external onlyOwner { 568 | require(hasVoteStaking(), "voteStaking not null"); 569 | require(_pidArray.length == _rewardPerBlockArray.length, "length"); 570 | 571 | if (proposals.length > 0) { 572 | require(block.number >= proposals[proposals.length - 1].endBlock, 573 | "Last vote unfinished"); 574 | } 575 | 576 | require(_startBlock > block.number, "requires valid start block"); 577 | require(_endBlock > _startBlock && 578 | _endBlock <= _startBlock + maximumVotingBlocks, "requires valid end block"); 579 | 580 | Proposal memory proposal; 581 | proposal.description = _description; 582 | proposal.startBlock = _startBlock; 583 | proposal.endBlock = _endBlock; 584 | proposal.optionCount = _optionCount; 585 | 586 | proposals.push(proposal); 587 | 588 | // set pool in VoteStaking 589 | for (uint256 i = 0; i < _pidArray.length; ++i) { 590 | voteStaking.set(_pidArray[i], _rewardPerBlockArray[i], _startBlock, _endBlock, true); 591 | } 592 | } 593 | 594 | function voteProposal(uint256 _pid, uint256 _index, uint256 _optionIndex, uint256 _votes) external { 595 | require(hasVoteStaking(), "voteStaking not null"); 596 | require(_votes > 0, "No votes"); 597 | 598 | Proposal storage proposal = proposals[_index]; 599 | require(block.number >= proposal.startBlock, "Not started"); 600 | require(block.number < proposal.endBlock, "Already ended"); 601 | require(_optionIndex < proposal.optionCount, "Invalid option index"); 602 | 603 | // If user didn't withdraw from an earlier vote yet, do it now so that he don't have any locked in voteStaking. 604 | if (userVoteEndBlock[msg.sender] < proposal.startBlock && voteStaking.getUserStakedAmount(_pid, msg.sender) > 0) { 605 | voteStaking.withdraw(_pid, msg.sender); 606 | } 607 | 608 | // NOTE: We allow user to vote for more than one options, and vote for multiple times. 609 | 610 | proposal.optionVotes[_optionIndex] = proposal.optionVotes[_optionIndex].add(_votes); 611 | proposal.userOptionVotes[msg.sender][_optionIndex] = proposal.userOptionVotes[msg.sender][_optionIndex].add(_votes); 612 | 613 | // User will get extra rewards before end block, however won't be able to withdraw 614 | if (proposal.endBlock > userVoteEndBlock[msg.sender]) { 615 | userVoteEndBlock[msg.sender] = proposal.endBlock; 616 | } 617 | 618 | // Stake to voteStake for extra reward. 619 | voteStaking.deposit(_pid, msg.sender, _votes); 620 | 621 | emit Vote(msg.sender, _pid, _index, _optionIndex, _votes); 622 | } 623 | 624 | function getProposalOptionVotes(uint256 _proposalIndex, uint256 _optionIndex) external view returns(uint256) { 625 | return proposals[_proposalIndex].optionVotes[_optionIndex]; 626 | } 627 | 628 | function getProposalUserOptionVotes(uint256 _proposalIndex, address _who, uint256 _optionIndex) external view returns(uint256) { 629 | return proposals[_proposalIndex].userOptionVotes[_who][_optionIndex]; 630 | } 631 | } 632 | -------------------------------------------------------------------------------- /contracts/Timelock.sol: -------------------------------------------------------------------------------- 1 | //SPDX-License-Identifier: UNLICENSED 2 | pragma solidity 0.6.12; 3 | 4 | import "@openzeppelin/contracts/math/SafeMath.sol"; 5 | 6 | contract Timelock { 7 | using SafeMath for uint; 8 | 9 | event NewAdmin(address indexed newAdmin); 10 | event NewPendingAdmin(address indexed newPendingAdmin); 11 | event NewDelay(uint indexed newDelay); 12 | event CancelTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); 13 | event ExecuteTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); 14 | event QueueTransaction(bytes32 indexed txHash, address indexed target, uint value, string signature, bytes data, uint eta); 15 | 16 | uint public constant GRACE_PERIOD = 14 days; 17 | uint public constant MINIMUM_DELAY = 1 days; 18 | uint public constant MAXIMUM_DELAY = 30 days; 19 | 20 | address public admin; 21 | address public pendingAdmin; 22 | uint public delay; 23 | bool public admin_initialized; 24 | 25 | mapping (bytes32 => bool) public queuedTransactions; 26 | 27 | constructor(address admin_, uint delay_) public { 28 | require(delay_ >= MINIMUM_DELAY, "Timelock::constructor: Delay must exceed minimum delay."); 29 | require(delay_ <= MAXIMUM_DELAY, "Timelock::constructor: Delay must not exceed maximum delay."); 30 | 31 | admin = admin_; 32 | delay = delay_; 33 | admin_initialized = false; 34 | } 35 | 36 | receive() external payable { } 37 | 38 | function setDelay(uint delay_) public { 39 | require(msg.sender == address(this), "Timelock::setDelay: Call must come from Timelock."); 40 | require(delay_ >= MINIMUM_DELAY, "Timelock::setDelay: Delay must exceed minimum delay."); 41 | require(delay_ <= MAXIMUM_DELAY, "Timelock::setDelay: Delay must not exceed maximum delay."); 42 | delay = delay_; 43 | 44 | emit NewDelay(delay); 45 | } 46 | 47 | function acceptAdmin() public { 48 | require(msg.sender == pendingAdmin, "Timelock::acceptAdmin: Call must come from pendingAdmin."); 49 | admin = msg.sender; 50 | pendingAdmin = address(0); 51 | 52 | emit NewAdmin(admin); 53 | } 54 | 55 | function setPendingAdmin(address pendingAdmin_) public { 56 | // allows one time setting of admin for deployment purposes 57 | if (admin_initialized) { 58 | require(msg.sender == address(this), "Timelock::setPendingAdmin: Call must come from Timelock."); 59 | } else { 60 | require(msg.sender == admin, "Timelock::setPendingAdmin: First call must come from admin."); 61 | admin_initialized = true; 62 | } 63 | pendingAdmin = pendingAdmin_; 64 | 65 | emit NewPendingAdmin(pendingAdmin); 66 | } 67 | 68 | function queueTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public returns (bytes32) { 69 | require(msg.sender == admin, "Timelock::queueTransaction: Call must come from admin."); 70 | require(eta >= getBlockTimestamp().add(delay), "Timelock::queueTransaction: Estimated execution block must satisfy delay."); 71 | 72 | bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); 73 | queuedTransactions[txHash] = true; 74 | 75 | emit QueueTransaction(txHash, target, value, signature, data, eta); 76 | return txHash; 77 | } 78 | 79 | function cancelTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public { 80 | require(msg.sender == admin, "Timelock::cancelTransaction: Call must come from admin."); 81 | 82 | bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); 83 | queuedTransactions[txHash] = false; 84 | 85 | emit CancelTransaction(txHash, target, value, signature, data, eta); 86 | } 87 | 88 | function executeTransaction(address target, uint value, string memory signature, bytes memory data, uint eta) public payable returns (bytes memory) { 89 | require(msg.sender == admin, "Timelock::executeTransaction: Call must come from admin."); 90 | 91 | bytes32 txHash = keccak256(abi.encode(target, value, signature, data, eta)); 92 | require(queuedTransactions[txHash], "Timelock::executeTransaction: Transaction hasn't been queued."); 93 | require(getBlockTimestamp() >= eta, "Timelock::executeTransaction: Transaction hasn't surpassed time lock."); 94 | require(getBlockTimestamp() <= eta.add(GRACE_PERIOD), "Timelock::executeTransaction: Transaction is stale."); 95 | 96 | queuedTransactions[txHash] = false; 97 | 98 | bytes memory callData; 99 | 100 | if (bytes(signature).length == 0) { 101 | callData = data; 102 | } else { 103 | callData = abi.encodePacked(bytes4(keccak256(bytes(signature))), data); 104 | } 105 | 106 | // solium-disable-next-line security/no-call-value 107 | (bool success, bytes memory returnData) = (target.call{value:value})(callData); 108 | require(success, "Timelock::executeTransaction: Transaction execution reverted."); 109 | 110 | emit ExecuteTransaction(txHash, target, value, signature, data, eta); 111 | 112 | return returnData; 113 | } 114 | 115 | function getBlockTimestamp() internal view returns (uint) { 116 | // solium-disable-next-line security/no-block-members 117 | return block.timestamp; 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /contracts/VoteStaking.sol: -------------------------------------------------------------------------------- 1 | 2 | // SPDX-License-Identifier: MIT 3 | 4 | pragma solidity 0.6.12; 5 | 6 | import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; 7 | import "@openzeppelin/contracts/math/SafeMath.sol"; 8 | import "@openzeppelin/contracts/access/Ownable.sol"; 9 | 10 | import "./interfaces/IVoteStaking.sol"; 11 | 12 | interface IStaking { 13 | function userInfo(uint256 pid, address who) external view returns(uint256, uint256, uint256); 14 | } 15 | 16 | // VoteStaking is a small pool that provides extra staking reward, and should be called by Staking. 17 | contract VoteStaking is Ownable, IVoteStaking { 18 | 19 | using SafeMath for uint256; 20 | 21 | uint256 constant PER_SHARE_SIZE = 1e12; 22 | 23 | // Info of each user. 24 | struct UserInfo { 25 | uint256 amount; // How many tokens the user has provided. 26 | uint256 rewardAmount; 27 | uint256 rewardDebt; // Reward debt. 28 | } 29 | 30 | // Info of each user that stakes tokens. 31 | mapping(uint256 => mapping(address => UserInfo)) public userInfo; 32 | 33 | // Info of the pool. 34 | struct PoolInfo { 35 | uint256 totalBalance; 36 | uint256 rewardPerBlock; 37 | uint256 startBlock; 38 | uint256 endBlock; 39 | uint256 lastRewardBlock; 40 | uint256 accRewardPerShare; // Accumulated BLES per share, times PER_SHARE_SIZE. 41 | } 42 | 43 | // Info of the pool. 44 | mapping(uint256 => PoolInfo) public poolInfo; 45 | 46 | address public stakingAddress; 47 | 48 | event Deposit(uint256 indexed pid, address indexed user, uint256 amount); 49 | event Withdraw(uint256 indexed pid, address indexed user, uint256 amount); 50 | event Claim(uint256 indexed pid, address indexed user, uint256 amount); 51 | 52 | constructor( 53 | address _stakingAddress 54 | ) public { 55 | stakingAddress = _stakingAddress; 56 | } 57 | 58 | function changeStakingAddress(address _stakingAddress) external onlyOwner { 59 | stakingAddress = _stakingAddress; 60 | } 61 | 62 | // Update the given pool's SUSHI allocation point. Can only be called by the owner. 63 | function set( 64 | uint256 _pid, 65 | uint256 _rewardPerBlock, 66 | uint256 _startBlock, 67 | uint256 _endBlock, 68 | bool _withUpdate 69 | ) external override { 70 | require(msg.sender == stakingAddress, "Only staking address can call"); 71 | 72 | if (_withUpdate) { 73 | updatePool(_pid); 74 | } 75 | 76 | poolInfo[_pid].rewardPerBlock = _rewardPerBlock; 77 | poolInfo[_pid].startBlock = _startBlock; 78 | poolInfo[_pid].endBlock = _endBlock; 79 | } 80 | 81 | // Return reward multiplier over the given _from to _to block. 82 | function getReward(uint256 _pid, uint256 _from, uint256 _to) 83 | public 84 | view 85 | returns (uint256) 86 | { 87 | if (_to <= _from || _from > poolInfo[_pid].endBlock || _to < poolInfo[_pid].startBlock) { 88 | return 0; 89 | } 90 | 91 | uint256 startBlock = _from < poolInfo[_pid].startBlock ? poolInfo[_pid].startBlock : _from; 92 | uint256 endBlock = _to < poolInfo[_pid].endBlock ? _to : poolInfo[_pid].endBlock; 93 | return endBlock.sub(startBlock).mul(poolInfo[_pid].rewardPerBlock); 94 | } 95 | 96 | // View function to see pending BLES on frontend. 97 | function pendingReward(uint256 _pid, address _user) 98 | external 99 | view 100 | override 101 | returns (uint256) 102 | { 103 | UserInfo storage user = userInfo[_pid][_user]; 104 | 105 | uint256 accRewardPerShare = poolInfo[_pid].accRewardPerShare; 106 | 107 | if (block.number > poolInfo[_pid].lastRewardBlock && poolInfo[_pid].totalBalance > 0) { 108 | uint256 reward = getReward(_pid, poolInfo[_pid].lastRewardBlock, block.number); 109 | accRewardPerShare = accRewardPerShare.add( 110 | reward.mul(PER_SHARE_SIZE).div(poolInfo[_pid].totalBalance) 111 | ); 112 | } 113 | 114 | return user.amount.mul(accRewardPerShare).div( 115 | PER_SHARE_SIZE).sub(user.rewardDebt).add(user.rewardAmount); 116 | } 117 | 118 | // Update reward variables of the given pool to be up-to-date. 119 | function updatePool(uint256 _pid) public override { 120 | if (block.number <= poolInfo[_pid].lastRewardBlock) { 121 | return; 122 | } 123 | 124 | if (poolInfo[_pid].totalBalance == 0) { 125 | poolInfo[_pid].lastRewardBlock = block.number; 126 | return; 127 | } 128 | 129 | uint256 reward = getReward(_pid, poolInfo[_pid].lastRewardBlock, block.number); 130 | 131 | poolInfo[_pid].accRewardPerShare = poolInfo[_pid].accRewardPerShare.add( 132 | reward.mul(PER_SHARE_SIZE).div(poolInfo[_pid].totalBalance) 133 | ); 134 | 135 | poolInfo[_pid].lastRewardBlock = block.number; 136 | } 137 | 138 | // Deposit tokens for BLES allocation. 139 | function deposit(uint256 _pid, address _who, uint256 _amount) external override { 140 | require(msg.sender == stakingAddress, "Only staking address can call"); 141 | 142 | PoolInfo storage pool = poolInfo[_pid]; 143 | UserInfo storage user = userInfo[_pid][_who]; 144 | 145 | (uint256 stakingAmount,,) = IStaking(stakingAddress).userInfo(_pid, _who); 146 | require(stakingAmount >= user.amount.add(_amount), "Not enough staking amount"); 147 | 148 | updatePool(_pid); 149 | 150 | if (user.amount > 0) { 151 | uint256 pending = 152 | user.amount.mul(pool.accRewardPerShare).div(PER_SHARE_SIZE).sub( 153 | user.rewardDebt 154 | ); 155 | 156 | user.rewardAmount = user.rewardAmount.add(pending); 157 | } 158 | 159 | pool.totalBalance = pool.totalBalance.add(_amount); 160 | 161 | user.amount = user.amount.add(_amount); 162 | user.rewardDebt = user.amount.mul(pool.accRewardPerShare).div(PER_SHARE_SIZE); 163 | emit Deposit(_pid, _who, _amount); 164 | } 165 | 166 | // Withdraw all tokens. 167 | function withdraw(uint256 _pid, address _who) external override returns(uint256) { 168 | require(msg.sender == stakingAddress, "Only staking address can call"); 169 | 170 | PoolInfo storage pool = poolInfo[_pid]; 171 | UserInfo storage user = userInfo[_pid][_who]; 172 | 173 | uint256 userAmount = user.amount; 174 | 175 | if (userAmount == 0) { 176 | return 0; 177 | } 178 | 179 | updatePool(_pid); 180 | 181 | uint256 pending = userAmount.mul(pool.accRewardPerShare).div( 182 | PER_SHARE_SIZE).sub(user.rewardDebt); 183 | user.rewardAmount = user.rewardAmount.add(pending); 184 | 185 | user.amount = 0; 186 | user.rewardDebt = 0; 187 | 188 | pool.totalBalance = pool.totalBalance.sub(userAmount); 189 | 190 | emit Withdraw(_pid, _who, userAmount); 191 | 192 | return userAmount; 193 | } 194 | 195 | // claim all reward. 196 | function claim(uint256 _pid, address _who) external override returns(uint256) { 197 | require(msg.sender == stakingAddress, "Only staking address can call"); 198 | 199 | PoolInfo storage pool = poolInfo[_pid]; 200 | UserInfo storage user = userInfo[_pid][_who]; 201 | 202 | updatePool(_pid); 203 | 204 | uint256 pending = user.amount.mul(pool.accRewardPerShare).div( 205 | PER_SHARE_SIZE).sub(user.rewardDebt); 206 | uint256 rewardTotal = user.rewardAmount.add(pending); 207 | 208 | user.rewardAmount = 0; 209 | user.rewardDebt = user.amount.mul(pool.accRewardPerShare).div(PER_SHARE_SIZE); 210 | 211 | emit Claim(_pid, _who, rewardTotal); 212 | 213 | return rewardTotal; 214 | } 215 | 216 | function getUserStakedAmount(uint256 _pid, address _who) external override view returns(uint256) { 217 | UserInfo storage user = userInfo[_pid][_who]; 218 | return user.amount; 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /contracts/interfaces/ILinkAccessor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.12; 3 | 4 | interface ILinkAccessor { 5 | function requestRandomness(uint256 userProvidedSeed_) external returns(bytes32); 6 | } 7 | -------------------------------------------------------------------------------- /contracts/interfaces/INFTMaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.12; 3 | 4 | interface INFTMaster { 5 | function fulfillRandomness(bytes32 requestId, uint256 randomness) external; 6 | } 7 | -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV2Router01.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.6.2; 2 | 3 | interface IUniswapV2Router01 { 4 | function factory() external pure returns (address); 5 | function WETH() external pure returns (address); 6 | 7 | function addLiquidity( 8 | address tokenA, 9 | address tokenB, 10 | uint amountADesired, 11 | uint amountBDesired, 12 | uint amountAMin, 13 | uint amountBMin, 14 | address to, 15 | uint deadline 16 | ) external returns (uint amountA, uint amountB, uint liquidity); 17 | function addLiquidityETH( 18 | address token, 19 | uint amountTokenDesired, 20 | uint amountTokenMin, 21 | uint amountETHMin, 22 | address to, 23 | uint deadline 24 | ) external payable returns (uint amountToken, uint amountETH, uint liquidity); 25 | function removeLiquidity( 26 | address tokenA, 27 | address tokenB, 28 | uint liquidity, 29 | uint amountAMin, 30 | uint amountBMin, 31 | address to, 32 | uint deadline 33 | ) external returns (uint amountA, uint amountB); 34 | function removeLiquidityETH( 35 | address token, 36 | uint liquidity, 37 | uint amountTokenMin, 38 | uint amountETHMin, 39 | address to, 40 | uint deadline 41 | ) external returns (uint amountToken, uint amountETH); 42 | function removeLiquidityWithPermit( 43 | address tokenA, 44 | address tokenB, 45 | uint liquidity, 46 | uint amountAMin, 47 | uint amountBMin, 48 | address to, 49 | uint deadline, 50 | bool approveMax, uint8 v, bytes32 r, bytes32 s 51 | ) external returns (uint amountA, uint amountB); 52 | function removeLiquidityETHWithPermit( 53 | address token, 54 | uint liquidity, 55 | uint amountTokenMin, 56 | uint amountETHMin, 57 | address to, 58 | uint deadline, 59 | bool approveMax, uint8 v, bytes32 r, bytes32 s 60 | ) external returns (uint amountToken, uint amountETH); 61 | function swapExactTokensForTokens( 62 | uint amountIn, 63 | uint amountOutMin, 64 | address[] calldata path, 65 | address to, 66 | uint deadline 67 | ) external returns (uint[] memory amounts); 68 | function swapTokensForExactTokens( 69 | uint amountOut, 70 | uint amountInMax, 71 | address[] calldata path, 72 | address to, 73 | uint deadline 74 | ) external returns (uint[] memory amounts); 75 | function swapExactETHForTokens(uint amountOutMin, address[] calldata path, address to, uint deadline) 76 | external 77 | payable 78 | returns (uint[] memory amounts); 79 | function swapTokensForExactETH(uint amountOut, uint amountInMax, address[] calldata path, address to, uint deadline) 80 | external 81 | returns (uint[] memory amounts); 82 | function swapExactTokensForETH(uint amountIn, uint amountOutMin, address[] calldata path, address to, uint deadline) 83 | external 84 | returns (uint[] memory amounts); 85 | function swapETHForExactTokens(uint amountOut, address[] calldata path, address to, uint deadline) 86 | external 87 | payable 88 | returns (uint[] memory amounts); 89 | 90 | function quote(uint amountA, uint reserveA, uint reserveB) external pure returns (uint amountB); 91 | function getAmountOut(uint amountIn, uint reserveIn, uint reserveOut) external pure returns (uint amountOut); 92 | function getAmountIn(uint amountOut, uint reserveIn, uint reserveOut) external pure returns (uint amountIn); 93 | function getAmountsOut(uint amountIn, address[] calldata path) external view returns (uint[] memory amounts); 94 | function getAmountsIn(uint amountOut, address[] calldata path) external view returns (uint[] memory amounts); 95 | } -------------------------------------------------------------------------------- /contracts/interfaces/IUniswapV2Router02.sol: -------------------------------------------------------------------------------- 1 | pragma solidity >=0.6.2; 2 | 3 | import './IUniswapV2Router01.sol'; 4 | 5 | interface IUniswapV2Router02 is IUniswapV2Router01 { 6 | function removeLiquidityETHSupportingFeeOnTransferTokens( 7 | address token, 8 | uint liquidity, 9 | uint amountTokenMin, 10 | uint amountETHMin, 11 | address to, 12 | uint deadline 13 | ) external returns (uint amountETH); 14 | function removeLiquidityETHWithPermitSupportingFeeOnTransferTokens( 15 | address token, 16 | uint liquidity, 17 | uint amountTokenMin, 18 | uint amountETHMin, 19 | address to, 20 | uint deadline, 21 | bool approveMax, uint8 v, bytes32 r, bytes32 s 22 | ) external returns (uint amountETH); 23 | 24 | function swapExactTokensForTokensSupportingFeeOnTransferTokens( 25 | uint amountIn, 26 | uint amountOutMin, 27 | address[] calldata path, 28 | address to, 29 | uint deadline 30 | ) external; 31 | function swapExactETHForTokensSupportingFeeOnTransferTokens( 32 | uint amountOutMin, 33 | address[] calldata path, 34 | address to, 35 | uint deadline 36 | ) external payable; 37 | function swapExactTokensForETHSupportingFeeOnTransferTokens( 38 | uint amountIn, 39 | uint amountOutMin, 40 | address[] calldata path, 41 | address to, 42 | uint deadline 43 | ) external; 44 | } -------------------------------------------------------------------------------- /contracts/interfaces/IVoteStaking.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.12; 3 | 4 | interface IVoteStaking { 5 | function set( 6 | uint256 _pid, 7 | uint256 _rewardPerBlock, 8 | uint256 _startBlock, 9 | uint256 _endBlock, 10 | bool _withUpdate 11 | ) external; 12 | 13 | function pendingReward(uint256 _pid, address _user) external view returns (uint256); 14 | 15 | function updatePool(uint256 _pid) external; 16 | 17 | function deposit(uint256 _pid, address _who, uint256 _amount) external; 18 | 19 | function withdraw(uint256 _pid, address _who) external returns(uint256); 20 | 21 | function claim(uint256 _pid, address _who) external returns(uint256); 22 | 23 | function getUserStakedAmount(uint256 _pid, address _who) external view returns(uint256); 24 | } 25 | -------------------------------------------------------------------------------- /contracts/mock/MockERC20.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.12; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | import "@openzeppelin/contracts/access/Ownable.sol"; 6 | 7 | contract MockERC20 is ERC20, Ownable { 8 | constructor( 9 | string memory name, 10 | string memory symbol, 11 | uint256 supply 12 | ) public ERC20(name, symbol) { 13 | _mint(msg.sender, supply); 14 | } 15 | 16 | function mint(uint256 amount) external onlyOwner { 17 | _mint(msg.sender, amount); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /contracts/mock/MockLinkAccessor.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.12; 3 | 4 | import "../interfaces/INFTMaster.sol"; 5 | import "../interfaces/ILinkAccessor.sol"; 6 | 7 | contract MockLinkAccessor is ILinkAccessor { 8 | 9 | INFTMaster public nftMaster; 10 | uint256 public randomness; 11 | bytes32 public requestId; 12 | 13 | constructor( 14 | INFTMaster nftMaster_ 15 | ) public { 16 | nftMaster = nftMaster_; 17 | } 18 | 19 | function setRandomness(uint256 randomness_) external { 20 | randomness = randomness_; 21 | } 22 | 23 | function triggerRandomness() external { 24 | nftMaster.fulfillRandomness(requestId, randomness); 25 | } 26 | 27 | function requestRandomness(uint256 userProvidedSeed_) public override returns(bytes32) { 28 | requestId = blockhash(block.number); 29 | return requestId; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /contracts/mock/MockNFT.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.12; 3 | 4 | import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; 5 | 6 | contract MockNFT is ERC721 { 7 | 8 | constructor (string memory name_, string memory symbol_) ERC721(name_, symbol_) public {} 9 | 10 | function mint(address to, uint256 tokenId) public { 11 | _mint(to, tokenId); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /contracts/mock/MockNFTMaster.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.12; 3 | 4 | import "../NFTMaster.sol"; 5 | 6 | contract MockNFTMaster is NFTMaster { 7 | 8 | constructor() public { 9 | } 10 | 11 | function buyLink(uint256 times_, address[] calldata path, uint256 amountInMax_, uint256 deadline_) internal override { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /contracts/tokens/BLES.sol: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: MIT 2 | pragma solidity 0.6.12; 3 | 4 | import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; 5 | 6 | // This token is owned by Timelock. 7 | contract BLES is ERC20("Blind Boxes Token", "BLES") { 8 | 9 | constructor() public { 10 | _mint(_msgSender(), 1e26); // 100 million, 18 decimals 11 | } 12 | 13 | function burn(uint256 _amount) external { 14 | _burn(_msgSender(), _amount); 15 | } 16 | 17 | function burnFrom(address account, uint256 amount) external { 18 | uint256 currentAllowance = allowance(account, _msgSender()); 19 | require(currentAllowance >= amount, "ERC20: burn amount exceeds allowance"); 20 | _approve(account, _msgSender(), currentAllowance - amount); 21 | _burn(account, amount); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /migrations/1_initial_migration.js: -------------------------------------------------------------------------------- 1 | const Migrations = artifacts.require("Migrations"); 2 | 3 | module.exports = function(deployer) { 4 | deployer.deploy(Migrations); 5 | }; -------------------------------------------------------------------------------- /migrations/2_deploy_contracts.js: -------------------------------------------------------------------------------- 1 | module.exports = function(deployer) {}; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "blindboxes-contracts", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@chainlink/contracts": "^0.1.6", 8 | "@openzeppelin/contracts": "^3.1.0", 9 | "@openzeppelin/test-helpers": "^0.5.6", 10 | "truffle": "^5.1.41", 11 | "truffle-flattener": "^1.4.4" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /test/NFTMaster.test.js: -------------------------------------------------------------------------------- 1 | const { expectRevert, time } = require('@openzeppelin/test-helpers'); 2 | const ethers = require('ethers'); 3 | const MockERC20 = artifacts.require('MockERC20'); 4 | const MockLinkAccessor = artifacts.require('MockLinkAccessor'); 5 | const MockNFT = artifacts.require('MockNFT'); 6 | const MockNFTMaster = artifacts.require('MockNFTMaster'); 7 | 8 | function encodeParameters(types, values) { 9 | const abi = new ethers.utils.AbiCoder(); 10 | return abi.encode(types, values); 11 | } 12 | 13 | function appendZeroes(value, count) { 14 | var result = value.toString(); 15 | for (var i = 0; i < count; ++i) { 16 | result += '0'; 17 | } 18 | 19 | return result; 20 | } 21 | 22 | contract('NFTMaster', ([dev, curator, artist, buyer0, buyer1, feeTo, randomGuy, linkToken]) => { 23 | beforeEach(async () => { 24 | // Mock USDC, 100 million, then transfer to buyer0, and buyer1 each 10 million 25 | this.baseToken = await MockERC20.new("Mock USDC", "USDC", appendZeroes(1, 26), { from: dev }); 26 | await this.baseToken.transfer(buyer0, appendZeroes(1, 25), { from: dev }); 27 | await this.baseToken.transfer(buyer1, appendZeroes(1, 25), { from: dev }); 28 | 29 | // Mock BLES, 100 million 30 | this.blesToken = await MockERC20.new("Mock BLES", "BLES", appendZeroes(1, 26), { from: dev }); 31 | 32 | // Mock NFTMaster 33 | this.nftMaster = await MockNFTMaster.new({ from: dev }); 34 | 35 | // Mock NFT 36 | this.mockCat = await MockNFT.new("Mock Cat", "CAT", { from: dev }); 37 | this.mockDog = await MockNFT.new("Mock Dog", "DOG", { from: dev }); 38 | 39 | await this.mockCat.mint(curator, 0, { from: dev }); 40 | await this.mockCat.mint(artist, 1, { from: dev }); 41 | await this.mockDog.mint(curator, 0, { from: dev }); 42 | await this.mockDog.mint(artist, 1, { from: dev }); 43 | 44 | await this.nftMaster.setBaseToken(this.baseToken.address, { from: dev }); 45 | await this.nftMaster.setBlesToken(this.blesToken.address, { from: dev }); 46 | await this.nftMaster.setLinkToken(linkToken, { from: dev }); 47 | await this.nftMaster.setFeeTo(feeTo, { from: dev }); 48 | }); 49 | 50 | it('create, add, and buy with USDC', async () => { 51 | this.linkAccessor = await MockLinkAccessor.new(this.nftMaster.address, { from: dev }); 52 | await this.nftMaster.setLinkAccessor(this.linkAccessor.address, { from: dev }); 53 | 54 | // Curator create an empty collection, charges 10% commission. 55 | await this.nftMaster.createCollection("Art gallery", 4, 1000, false, [artist], { from: curator }); 56 | const collectionId = await this.nftMaster.nextCollectionId(); 57 | assert.equal(collectionId.valueOf(), 1); 58 | 59 | // Add NFTs to collection. 60 | 61 | // 100 USDC 62 | await this.mockCat.approve(this.nftMaster.address, 0, {from: curator}); 63 | await this.nftMaster.addNFTToCollection(this.mockCat.address, 0, collectionId, appendZeroes(1, 20), {from: curator}); 64 | 65 | // 200 USDC 66 | await this.mockCat.approve(this.nftMaster.address, 1, {from: artist}); 67 | await this.nftMaster.addNFTToCollection(this.mockCat.address, 1, collectionId, appendZeroes(2, 20), {from: artist}); 68 | 69 | // 300 USDC 70 | await this.mockDog.approve(this.nftMaster.address, 0, {from: curator}); 71 | await this.nftMaster.addNFTToCollection(this.mockDog.address, 0, collectionId, appendZeroes(3, 20), {from: curator}); 72 | 73 | // 400 USDC 74 | await this.mockDog.approve(this.nftMaster.address, 1, {from: artist}); 75 | await this.nftMaster.addNFTToCollection(this.mockDog.address, 1, collectionId, appendZeroes(3, 20), {from: artist}); 76 | 77 | // Remove dog #1. 78 | const dog1NFTId = await this.nftMaster.nftIdMap(this.mockDog.address, 1, {from: artist}); 79 | await this.nftMaster.removeNFTFromCollection(dog1NFTId.valueOf(), collectionId, {from: artist}); 80 | assert.equal(await this.mockDog.ownerOf(1), artist); 81 | 82 | // Publish 83 | await this.nftMaster.publishCollection(1, [linkToken], 0, 0, {from: curator}); 84 | 85 | // View the published collection. 86 | const collection = await this.nftMaster.allCollections(collectionId, {from: buyer0}); 87 | assert.equal(collection[0], curator); // owner 88 | assert.equal(collection[1], "Art gallery"); // name 89 | assert.equal(collection[2].valueOf(), 3); // size 90 | assert.equal(collection[3].valueOf(), 1000); // commissionRate 91 | assert.equal(collection[4].valueOf(), 0); // willAcceptBLES 92 | assert.equal(collection[5].valueOf(), 6e20); // totalPrice 93 | assert.equal(collection[6].valueOf(), 2e20); // averagePrice 94 | assert.equal(collection[7].valueOf(), 3e19); // fee 95 | assert.equal(collection[8].valueOf(), 6e19); // commission 96 | 97 | assert.notEqual(collection[9].valueOf(), 0); // isPublished 98 | 99 | assert.equal(await this.nftMaster.collaborators(collectionId, 0), artist); 100 | 101 | // Buy and withdraw 102 | await this.linkAccessor.setRandomness(11, {from: dev}); 103 | 104 | // buyer0 buys 1. 105 | await this.baseToken.approve(this.nftMaster.address, appendZeroes(2, 20), {from: buyer0}); 106 | await this.nftMaster.drawBoxes(collectionId, 1, {from: buyer0}); 107 | // buyer1 buys 2. 108 | await this.baseToken.approve(this.nftMaster.address, appendZeroes(4, 20), {from: buyer1}); 109 | await this.nftMaster.drawBoxes(collectionId, 2, {from: buyer1}); 110 | 111 | // Trigger randomness (mock) 112 | await this.linkAccessor.triggerRandomness({from: dev}); 113 | 114 | const rrr = await this.nftMaster.nftMapping(collectionId, 0, {from: randomGuy}); 115 | 116 | // Check for result. 117 | const winner0 = await this.nftMaster.getWinner(collectionId, 0, {from: randomGuy}); 118 | const winner1 = await this.nftMaster.getWinner(collectionId, 1, {from: randomGuy}); 119 | const winner2 = await this.nftMaster.getWinner(collectionId, 2, {from: randomGuy}); 120 | 121 | const nftId0 = await this.nftMaster.nftIdMap(this.mockCat.address, 0, {from: artist}); 122 | const nftId1 = await this.nftMaster.nftIdMap(this.mockCat.address, 1, {from: artist}); 123 | const nftId2 = await this.nftMaster.nftIdMap(this.mockDog.address, 0, {from: artist}); 124 | const winnerData = [[winner0, nftId0], [winner1, nftId1], [winner2, nftId2]]; 125 | 126 | const buyer0NFTIdArray = winnerData.filter(entry => entry[0] == buyer0).map(entry => entry[1]); 127 | const buyer1NFTIdArray = winnerData.filter(entry => entry[0] == buyer1).map(entry => entry[1]); 128 | 129 | assert.equal(buyer0NFTIdArray.length, 1); 130 | assert.equal(buyer1NFTIdArray.length, 2); 131 | 132 | // Withdraw. 133 | // Here nft index happen to be nftId - 1. 134 | await this.nftMaster.claimNFT(collectionId, buyer0NFTIdArray[0] - 1, {from: buyer0}); 135 | await this.nftMaster.claimNFT(collectionId, buyer1NFTIdArray[0] - 1, {from: buyer1}); 136 | await this.nftMaster.claimNFT(collectionId, buyer1NFTIdArray[1] - 1, {from: buyer1}); 137 | 138 | // Curator and artist all get money, after 5% fee and 10% commission deducted. 139 | const curatorBalance = await this.baseToken.balanceOf(curator, {from: curator}); 140 | assert.equal(curatorBalance.valueOf(), 34e19); 141 | const artistBalance = await this.baseToken.balanceOf(artist, {from: artist}); 142 | assert.equal(artistBalance.valueOf(), 17e19); 143 | 144 | // feeTo got fee. 145 | await this.nftMaster.claimFee(collectionId, {from: randomGuy}); 146 | const feeToBalance = await this.baseToken.balanceOf(feeTo, {from: feeTo}); 147 | assert.equal(feeToBalance.valueOf(), 3e19); 148 | 149 | // curator got commission. 150 | await this.nftMaster.claimCommission(collectionId, {from: curator}); 151 | const curatorNewBalance = await this.baseToken.balanceOf(curator, {from: curator}); 152 | assert.equal(curatorNewBalance.valueOf(), 40e19); // 34 + 6 = 40 153 | }); 154 | 155 | it('create, add, published and unpublished', async () => { 156 | // Curator create an empty collection, charges 10% commission. 157 | await this.nftMaster.createCollection("Art gallery", 4, 1000, false, [artist], { from: curator }); 158 | const collectionId = await this.nftMaster.nextCollectionId(); 159 | assert.equal(collectionId.valueOf(), 1); 160 | 161 | // Add NFTs to collection. 162 | 163 | // 100 USDC 164 | await this.mockCat.approve(this.nftMaster.address, 0, {from: curator}); 165 | await this.nftMaster.addNFTToCollection(this.mockCat.address, 0, collectionId, appendZeroes(1, 20), {from: curator}); 166 | 167 | // 200 USDC 168 | await this.mockCat.approve(this.nftMaster.address, 1, {from: artist}); 169 | await this.nftMaster.addNFTToCollection(this.mockCat.address, 1, collectionId, appendZeroes(2, 20), {from: artist}); 170 | 171 | // 300 USDC 172 | await this.mockDog.approve(this.nftMaster.address, 0, {from: curator}); 173 | await this.nftMaster.addNFTToCollection(this.mockDog.address, 0, collectionId, appendZeroes(3, 20), {from: curator}); 174 | 175 | // Publish 176 | await this.nftMaster.publishCollection(1, [linkToken], 0, 0, {from: curator}); 177 | 178 | // View the published collection. 179 | const collection = await this.nftMaster.allCollections(collectionId, {from: buyer0}); 180 | assert.notEqual(collection[9].valueOf(), 0); // isPublished 181 | 182 | assert.equal(await this.nftMaster.collaborators(collectionId, 0), artist); 183 | 184 | // buyer0 buys 1. 185 | await this.baseToken.approve(this.nftMaster.address, appendZeroes(2e20), {from: buyer0}); 186 | await this.nftMaster.drawBoxes(collectionId, 1, {from: buyer0}); 187 | 188 | await expectRevert( 189 | this.nftMaster.unpublishCollection(collectionId, {from: curator}), 190 | 'Not expired yet', 191 | ); 192 | 193 | // buyer1 buys 2. 194 | await this.baseToken.approve(this.nftMaster.address, appendZeroes(4e20), {from: buyer1}); 195 | await this.nftMaster.drawBoxes(collectionId, 1, {from: buyer1}); 196 | 197 | await time.increase(time.duration.days(15)); 198 | await this.nftMaster.unpublishCollection(collectionId, {from: curator}); 199 | const collectionAfterUnpublished = await this.nftMaster.allCollections(collectionId, {from: buyer0}); 200 | assert.equal(collectionAfterUnpublished[9].valueOf(), 0); //isPublished 201 | assert.equal(collectionAfterUnpublished[11].valueOf(), 0); //soldCount 202 | 203 | const buy0Balance = await this.baseToken.balanceOf(buyer0, {from: buyer0}); 204 | assert.equal(buy0Balance.valueOf(), appendZeroes(1, 25)); 205 | const buy1Balance = await this.baseToken.balanceOf(buyer1, {from: buyer1}); 206 | assert.equal(buy1Balance.valueOf(), appendZeroes(1, 25)); 207 | 208 | // Publish 209 | await this.nftMaster.publishCollection(1, [linkToken], 0, 0, {from: curator}); 210 | 211 | const publishCollectionSecond = await this.nftMaster.allCollections(collectionId, {from: buyer0}); 212 | assert.notEqual(publishCollectionSecond[9].valueOf(), 0); // isPublished 213 | 214 | // buyer0 buys 1. 215 | await this.baseToken.approve(this.nftMaster.address, appendZeroes(2e20), {from: buyer0}); 216 | await this.nftMaster.drawBoxes(collectionId, 1, {from: buyer0}); 217 | // buyer1 buys 2. 218 | await this.baseToken.approve(this.nftMaster.address, appendZeroes(4e20), {from: buyer1}); 219 | await this.nftMaster.drawBoxes(collectionId, 2, {from: buyer1}); 220 | 221 | await time.increase(time.duration.days(15)); 222 | // sold out 223 | await expectRevert( 224 | this.nftMaster.unpublishCollection(collectionId, {from: curator}), 225 | 'Sold out', 226 | ); 227 | }); 228 | 229 | it('test 100 NFT.', async () => { 230 | // Curator create an empty collection, charges 10% commission. 231 | await this.nftMaster.createCollection("Pig gallery", 100, 1000, false, [artist], { from: curator }); 232 | const collectionId = await this.nftMaster.nextCollectionId(); 233 | assert.equal(collectionId.valueOf(), 1); 234 | 235 | const mockPig = await MockNFT.new("Mock Pig", "PIG", { from: dev }); 236 | 237 | // Add NFTs to collection. 238 | 239 | for (let i = 0; i < 100; ++i) { 240 | await mockPig.mint(curator, i, { from: dev }); 241 | await mockPig.approve(this.nftMaster.address, i, {from: curator}); 242 | // (i + 1) * 100 USDC 243 | await this.nftMaster.addNFTToCollection(mockPig.address, i, collectionId, appendZeroes((i + 1), 20), {from: curator}); 244 | } 245 | 246 | // Publish 247 | await this.nftMaster.publishCollection(1, [linkToken], 0, 0, {from: curator}); 248 | 249 | // View the published collection. 250 | const collection = await this.nftMaster.allCollections(collectionId, {from: buyer0}); 251 | 252 | assert.equal(collection[0], curator); // owner 253 | assert.equal(collection[1], "Pig gallery"); // name 254 | assert.equal(collection[2].valueOf(), 100); // size 255 | assert.equal(collection[3].valueOf(), 1000); // commissionRate 256 | assert.equal(collection[4].valueOf(), 0); // willAcceptBLES 257 | assert.equal(collection[5].valueOf(), 505e21); // totalPrice 258 | assert.equal(collection[6].valueOf(), 505e19); // averagePrice 259 | assert.equal(collection[7].valueOf(), 2525e19); // fee 260 | assert.equal(collection[8].valueOf(), 505e20); // commission 261 | 262 | assert.notEqual(collection[9].valueOf(), 0); // isPublished 263 | assert.equal(await this.nftMaster.collaborators(collectionId, 0), artist); 264 | 265 | // Buy and withdraw 266 | 267 | // buyer0 buys 40. 268 | for (let i = 0; i < 40; ++i) { 269 | await this.baseToken.approve(this.nftMaster.address, appendZeroes(505, 19), {from: buyer0}); 270 | await this.nftMaster.drawBoxes(collectionId, 1, {from: buyer0}); 271 | } 272 | // buyer1 buys 60. 273 | for (let i = 0; i < 60; ++i) { 274 | await this.baseToken.approve(this.nftMaster.address, appendZeroes(505, 19), {from: buyer1}); 275 | await this.nftMaster.drawBoxes(collectionId, 1, {from: buyer1}); 276 | } 277 | 278 | // Check for result. 279 | let buyer0Count = 0; 280 | let buyer1Count = 0; 281 | for (let i = 0; i < 100; ++i) { 282 | const winner = await this.nftMaster.getWinner(collectionId, i, {from: randomGuy}); 283 | if (winner == buyer0) { 284 | ++buyer0Count; 285 | } else if (winner == buyer1) { 286 | ++buyer1Count; 287 | } 288 | } 289 | 290 | assert.equal(buyer0Count, 40); 291 | assert.equal(buyer1Count, 60); 292 | }); 293 | }); 294 | -------------------------------------------------------------------------------- /test/Staking.test.js: -------------------------------------------------------------------------------- 1 | const { expectRevert, time } = require('@openzeppelin/test-helpers'); 2 | const ethers = require('ethers'); 3 | const BLES = artifacts.require('BLES'); 4 | const Staking = artifacts.require('Staking'); 5 | 6 | function encodeParameters(types, values) { 7 | const abi = new ethers.utils.AbiCoder(); 8 | return abi.encode(types, values); 9 | } 10 | 11 | function appendZeroes(value, count) { 12 | var result = value.toString(); 13 | for (var i = 0; i < count; ++i) { 14 | result += '0'; 15 | } 16 | 17 | return result; 18 | } 19 | 20 | contract('Staking', ([dev, user0, user1]) => { 21 | beforeEach(async () => { 22 | // Mock BLES, 100 million, then transfer to user0, and user1 each 1000 23 | this.blesToken = await BLES.new({ from: dev }); 24 | await this.blesToken.transfer(user0, appendZeroes(1, 21), { from: dev }); 25 | await this.blesToken.transfer(user1, appendZeroes(1, 21), { from: dev }); 26 | 27 | // Staking 28 | this.staking = await Staking.new(this.blesToken.address, { from: dev }); 29 | 30 | this.firstBlock = await time.latestBlock(); 31 | 32 | // Adds 2 pools to staking, from block 10 to block 1000010, 1 BLES per block. 33 | // Lock for 0 days. 34 | await this.staking.add(this.blesToken.address, appendZeroes(1, 18), this.firstBlock + 10, this.firstBlock + 1000010, 0, { from: dev }) 35 | // Lock for 40 days. 36 | await this.staking.add(this.blesToken.address, appendZeroes(1, 18), this.firstBlock + 10, this.firstBlock + 1000010, 40, { from: dev }) 37 | }); 38 | 39 | it('deposit, withdraw and claim', async () => { 40 | await this.blesToken.approve(this.staking.address, appendZeroes(1, 21), { from: user0 }); 41 | await this.blesToken.approve(this.staking.address, appendZeroes(1, 21), { from: user1 }); 42 | 43 | // Each of user0 and user1, deposit 100 BLES to each pool. 44 | await this.staking.deposit(0, appendZeroes(1, 20), { from: user0 }); 45 | await this.staking.deposit(1, appendZeroes(1, 20), { from: user0 }); 46 | await this.staking.deposit(0, appendZeroes(1, 20), { from: user1 }); 47 | await this.staking.deposit(1, appendZeroes(1, 20), { from: user1 }); 48 | 49 | await time.advanceBlockTo(this.firstBlock + 20); 50 | 51 | const pending0_0 = await this.staking.pendingReward(0, user0); 52 | const pending0_1 = await this.staking.pendingReward(1, user0); 53 | const pending1_0 = await this.staking.pendingReward(0, user1); 54 | const pending1_1 = await this.staking.pendingReward(1, user1); 55 | assert.equal(pending0_0.valueOf(), 5e18); 56 | assert.equal(pending0_1.valueOf(), 5e18); 57 | assert.equal(pending1_0.valueOf(), 5e18); 58 | assert.equal(pending1_1.valueOf(), 5e18); 59 | 60 | // Currently in pool 1, nothing is unlocked. 61 | const info0 = await this.staking.userLockInfo(user0, 1); 62 | assert.equal(info0.amount.valueOf(), 0); 63 | 64 | // Withrawing even 1 token will fail. 65 | await expectRevert( 66 | this.staking.withdraw(1, 1, { from: user0 }), 67 | 'Please wait for unlock', 68 | ); 69 | 70 | // Advance 1 day, and each of them deposit another 100 BLES to pool #1. 71 | await time.increase(time.duration.days(1)); 72 | await this.staking.deposit(1, appendZeroes(1, 20), { from: user0 }); 73 | await this.staking.deposit(1, appendZeroes(1, 20), { from: user1 }); 74 | 75 | // Advance another 1 day, and each of them deposit another 100 BLES to pool #1. 76 | await time.increase(time.duration.days(1)); 77 | await this.staking.deposit(1, appendZeroes(1, 20), { from: user0 }); 78 | await this.staking.deposit(1, appendZeroes(1, 20), { from: user1 }); 79 | 80 | // Advance 20 days, user1 withdraws early half of tokens on each of the 3 points. 81 | await time.increase(time.duration.days(20)); 82 | // List unlock info array. 83 | const unlockInfoArray = await this.staking.getUnlockArray(user1, 1, 0, 100); 84 | assert.equal(unlockInfoArray.length, 3); 85 | assert.equal(unlockInfoArray[0].amount.valueOf(), 1e20); 86 | assert.equal(unlockInfoArray[1].amount.valueOf(), 1e20); 87 | assert.equal(unlockInfoArray[2].amount.valueOf(), 1e20); 88 | await this.staking.withdrawEarly(1, unlockInfoArray[0].pointer, appendZeroes(5, 19), { from: user1 }); // 22 / 40 89 | await this.staking.withdrawEarly(1, unlockInfoArray[1].pointer, appendZeroes(5, 19), { from: user1 }); // 21 / 40 90 | await this.staking.withdrawEarly(1, unlockInfoArray[2].pointer, appendZeroes(5, 19), { from: user1 }); // 20 / 40 91 | 92 | await expectRevert( 93 | this.staking.withdrawEarly(1, unlockInfoArray[0].pointer, appendZeroes(6, 19), { from: user1 }), 94 | '_amount too large', 95 | ); 96 | 97 | const balance1_0 = await this.blesToken.balanceOf(user1); 98 | assert.equal(balance1_0.valueOf(), 6e20 + 275e17 + 2625e16 + 25e18); 99 | 100 | // Advance another 19 days, 2/3 of principal should be unlocked. 101 | await time.increase(time.duration.days(19)); 102 | const unlockAmount0 = await this.staking.getUnlockAmount(user0, 1); 103 | assert.equal(unlockAmount0.valueOf(), 2e20); 104 | 105 | // User0, just withdraws 2/3. Now he has 800, 106 | // because 1000 - 100 * 4 + 100 * 2 = 800 107 | await this.staking.withdraw(1, appendZeroes(2, 20), { from: user0 }); 108 | const balance0_0 = await this.blesToken.balanceOf(user0); 109 | assert.equal(balance0_0.valueOf(), 8e20); 110 | 111 | // User0 withdrawing more will fail. 112 | await expectRevert( 113 | this.staking.withdraw(1, 1, { from: user0 }), 114 | 'Please wait for unlock', 115 | ); 116 | 117 | // Advance 1 more day, now user0 can withdraw all. 118 | await time.increase(time.duration.days(1)); 119 | await this.staking.withdraw(1, appendZeroes(1, 20), { from: user0 }); 120 | const balance0_1 = await this.blesToken.balanceOf(user0); 121 | assert.equal(balance0_1.valueOf(), 9e20); 122 | }); 123 | }); 124 | -------------------------------------------------------------------------------- /truffle-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | // Uncommenting the defaults below 3 | // provides for an easier quick-start with Ganache. 4 | // You can also follow this format for other networks; 5 | // see 6 | // for more details on how to specify configuration options! 7 | // 8 | //networks: { 9 | // development: { 10 | // host: "127.0.0.1", 11 | // port: 7545, 12 | // network_id: "*" 13 | // }, 14 | // test: { 15 | // host: "127.0.0.1", 16 | // port: 7545, 17 | // network_id: "*" 18 | // } 19 | //} 20 | // 21 | compilers: { 22 | solc: { 23 | version: "0.6.12", 24 | settings: { 25 | "optimizer": { 26 | "enabled": true, 27 | "runs": 200 28 | } 29 | } 30 | } 31 | }, 32 | 33 | mocha: { 34 | timeout: 12000000 35 | } 36 | }; 37 | --------------------------------------------------------------------------------