Created
September 16, 2021 05:17
-
-
Save mlegls/ed296142f636814dacbca6550a254a7b to your computer and use it in GitHub Desktop.
Created using remix-ide: Realtime Ethereum Contract Compiler and Runtime. Load this file by pasting this gists URL or ID at https://remix.ethereum.org/#version=soljson-v0.8.7+commit.e28d00a7.js&optimize=false&runs=200&gist=
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
pragma solidity >=0.8.0 <0.9.0; | |
/** | |
* @title Lootbox | |
* @dev Matrix lootbox contract | |
*/ | |
import "@openzeppelin/contracts/token/ERC1155/ERC1155.sol"; | |
import "@openzeppelin/contracts/token/ERC1155/utils/ERC1155Holder.sol"; | |
// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155/ERC1155.sol"; | |
// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/v3.4.0/contracts/token/ERC1155/ERC1155Holder.sol"; | |
// import "https://github.com/OpenZeppelin/openzeppelin-contracts/blob/master/contracts/token/ERC1155/presets/ERC1155PresetMinterPauser.sol"; | |
contract Lootbox is ERC1155Holder { | |
// NULL is for depleted items; fungible tokens not yet implemented | |
enum TokenType{ NULL, NONFUNGIBLE, SEMIFUNGIBLE, FUNGIBLE } | |
// pity indicates whether an item is in the pity pool | |
struct LootboxItem { | |
TokenType tokenType; | |
ERC1155Token token; | |
string name; | |
uint32 unitWeight; | |
bool pity; | |
} | |
struct ERC1155Token { | |
address contractAddress; | |
uint id; | |
} | |
LootboxItem[] public pool; // list of LootboxItems is the Lootbox pool | |
uint public pityNumber = 9; // guaranteed pity roll every this often + 1 | |
// maps id to rate ranges | |
// width of range, inclusive. is the total weight (i.e. unit weight for NFTs, unit weight * qty for SFTs) | |
// open function generates a random int and checks which range it falls in | |
mapping(uint => uint32[2]) private normalRates; | |
mapping(uint => uint32[2]) private pityRates; | |
mapping(address => uint) public pityCounter; // counts rolls to pity for each lootbox opening address | |
// random integer between 1 and rangeEnd, inclusive | |
function _fakeRandint(uint rangeEnd) private view returns (uint randint_) { | |
randint_ = (uint(keccak256(abi.encodePacked(block.timestamp, block.difficulty))) % rangeEnd) + 1; | |
} | |
// end of normalRates ranges | |
function _rangeEnd() private view returns (uint rangeEnd_) { | |
if (pool.length == 0) return 0; | |
for (uint i=pool.length-1; i>=0; i--) { | |
if (i==0 && pool[0].tokenType == TokenType.NULL) return 0; | |
if (pool[i].tokenType == TokenType.NULL) continue; | |
rangeEnd_ = normalRates[i][1]; | |
break; | |
} | |
} | |
// end of pityRates ranges | |
function _pityRangeEnd() private view returns (uint rangeEnd_) { | |
if (pool.length == 0) return 0; | |
for (uint i=pool.length-1; i>=0; i--) { | |
if (i==0 && pool[0].tokenType == TokenType.NULL) return 0; | |
if (i==0 && !pool[0].pity) return 0; | |
if (pool[i].tokenType == TokenType.NULL) continue; | |
if (!pool[i].pity) continue; | |
rangeEnd_ = pityRates[i][1]; | |
break; | |
} | |
} | |
function addNft(string memory _name, uint32 _weight, bool _pity, address _tokenContract, uint _tokenId) public { | |
ERC1155 tokenContract = ERC1155(_tokenContract); | |
require(tokenContract.balanceOf(msg.sender, _tokenId)>0); | |
tokenContract.safeTransferFrom(msg.sender, address(this), _tokenId, 1, ""); | |
uint32 rangeEnd = uint32(_rangeEnd()+1); | |
uint32 pityRangeEnd = uint32(_pityRangeEnd()+1); | |
ERC1155Token memory tokenData = ERC1155Token({contractAddress: _tokenContract, id: _tokenId}); | |
LootboxItem memory item = LootboxItem({tokenType: TokenType.NONFUNGIBLE, token: tokenData, name: _name, unitWeight: _weight, pity: _pity}); | |
pool.push(item); | |
normalRates[pool.length-1] = [rangeEnd, rangeEnd+_weight-1]; | |
if (_pity) { | |
pityRates[pool.length-1] = [pityRangeEnd, pityRangeEnd+_weight-1]; | |
} | |
} | |
// added item not in pity pool by default | |
function addNft(string memory _name, uint32 _weight, address _tokenContract, uint _tokenId) public { | |
addNft(_name, _weight, false, _tokenContract, _tokenId); | |
} | |
function addSft(string memory _name, uint32 _weight, uint32 _qty, bool _pity, address _tokenContract, uint _tokenId) public { | |
ERC1155 tokenContract = ERC1155(_tokenContract); | |
require(tokenContract.balanceOf(msg.sender, _tokenId)>0); | |
tokenContract.safeTransferFrom(msg.sender, address(this), _tokenId, _qty, ""); | |
uint32 rangeEnd = uint32(_rangeEnd()+1); | |
uint32 pityRangeEnd = uint32(_pityRangeEnd()+1); | |
ERC1155Token memory tokenData = ERC1155Token({contractAddress: _tokenContract, id: _tokenId}); | |
LootboxItem memory item = LootboxItem({tokenType: TokenType.SEMIFUNGIBLE, token: tokenData, name: _name, unitWeight: _weight, pity: _pity}); | |
pool.push(item); | |
normalRates[pool.length-1] = [rangeEnd, rangeEnd+(_weight*_qty)-1]; | |
if (_pity) { | |
pityRates[pool.length-1] = [pityRangeEnd, pityRangeEnd+(_weight*_qty)-1]; | |
} | |
} | |
// added item not in pity pool by default | |
function addSft(string memory _name, uint32 _weight, uint32 _qty, address _tokenContract, uint _tokenId) public { | |
addSft(_name, _weight, _qty, false, _tokenContract, _tokenId); | |
} | |
function _removeItem(uint _index) private { | |
LootboxItem storage toRemove = pool[_index]; | |
// change ranges of all other items first because unitWeight may be deleted | |
if (pool.length>0 && _index<pool.length-1) { // if removed item is not the last item | |
for (uint i=_index+1; i<pool.length; i++) { | |
if (pool[i].tokenType == TokenType.NULL) continue; | |
normalRates[i][0] -= toRemove.unitWeight; | |
normalRates[i][1] -= toRemove.unitWeight; | |
if (pool[i].pity) { | |
pityRates[i][0] -= toRemove.unitWeight; | |
pityRates[i][1] -= toRemove.unitWeight; | |
} | |
} | |
} | |
// remove relevant item | |
if (toRemove.tokenType == TokenType.SEMIFUNGIBLE) { // remove 1 unit if more is left | |
if (normalRates[_index][1] >= normalRates[_index][0] + toRemove.unitWeight) { | |
normalRates[_index][1] -= toRemove.unitWeight; | |
if (toRemove.pity) pityRates[_index][1] -= toRemove.unitWeight; | |
} else { // when item is completely depleted | |
pool[_index].tokenType = TokenType.NULL; | |
} | |
} else if (toRemove.tokenType == TokenType.NONFUNGIBLE) { | |
pool[_index].tokenType = TokenType.NULL; | |
} else revert("Trying to remove null item"); | |
} | |
function _open() private returns (LootboxItem memory item_) { | |
uint rangeEnd = _rangeEnd(); | |
if (rangeEnd == 0) revert("Box empty"); | |
uint32 randIndex = uint32(_fakeRandint(rangeEnd)); | |
for (uint i=0; i<pool.length; i++) { | |
if (pool[i].tokenType == TokenType.NULL) continue; | |
if (randIndex <= normalRates[i][1]) { | |
item_ = pool[i]; | |
ERC1155 tokenContract = ERC1155(item_.token.contractAddress); | |
tokenContract.safeTransferFrom(address(this), msg.sender, item_.token.id, 1, ""); | |
_removeItem(i); | |
break; | |
} | |
} | |
} | |
function _pityOpen() private returns (LootboxItem memory item_) { | |
uint rangeEnd = _pityRangeEnd(); | |
if (rangeEnd == 0) revert("Pity pool empty"); | |
uint32 randIndex = uint32(_fakeRandint(rangeEnd)); | |
for (uint i=0; i<pool.length; i++) { | |
if (pool[i].tokenType == TokenType.NULL) continue; | |
if (!pool[i].pity) continue; | |
if (randIndex <= pityRates[i][1]) { | |
item_ = pool[i]; | |
ERC1155 tokenContract = ERC1155(item_.token.contractAddress); | |
tokenContract.safeTransferFrom(address(this), msg.sender, item_.token.id, 1, ""); | |
_removeItem(i); | |
break; | |
} | |
} | |
} | |
function open() public returns (LootboxItem memory item_) { | |
if (pityCounter[msg.sender] >= pityNumber) { // pity roll | |
pityCounter[msg.sender] = 0; | |
// if pity pool empty, draws from normal pool | |
if (_pityRangeEnd() != 0) { | |
item_ = _pityOpen(); | |
} else { | |
item_ = _open(); | |
} | |
} else { // normal roll | |
pityCounter[msg.sender] += 1; | |
item_ = _open(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment