Skip to content

Instantly share code, notes, and snippets.

@mlegls
Created September 16, 2021 19:06
Show Gist options
  • Save mlegls/a2725aa4d1edbb124875e030818fa75d to your computer and use it in GitHub Desktop.
Save mlegls/a2725aa4d1edbb124875e030818fa75d 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=
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 "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/utils/ERC721Holder.sol";
// for debugging
import "@openzeppelin/contracts/token/ERC721/presets/ERC721PresetMinterPauserAutoId.sol";
import "@openzeppelin/contracts/token/ERC1155/presets/ERC1155PresetMinterPauser.sol";
contract Lootbox is ERC1155Holder, ERC721Holder {
// NULL is for depleted items
enum TokenType{ NULL, NONFUNGIBLE, SEMIFUNGIBLE }
// pity indicates whether an item is in the pity pool
struct LootboxItem {
TokenType tokenType;
TokenData token;
string name;
uint32 unitWeight;
bool pity;
}
struct TokenData {
address contractAddress;
uint id;
}
LootboxItem[] public pool; // list of LootboxItems is the Lootbox pool
uint public pityNumber = 8; // guaranteed pity roll every this often + 2
// 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]) public normalRates;
mapping(uint => uint32[2]) public 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() public 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() public 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 {
ERC721 tokenContract = ERC721(_tokenContract);
require(tokenContract.ownerOf(_tokenId)==msg.sender);
tokenContract.safeTransferFrom(msg.sender, address(this), _tokenId);
uint32 rangeEnd = uint32(_rangeEnd()+1);
uint32 pityRangeEnd = uint32(_pityRangeEnd()+1);
TokenData memory tokenData = TokenData({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);
TokenData memory tokenData = TokenData({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];
if (item_.tokenType == TokenType.NONFUNGIBLE) {
ERC721 tokenContract = ERC721(item_.token.contractAddress);
tokenContract.safeTransferFrom(address(this), msg.sender, item_.token.id);
} else if (item_.tokenType == TokenType.SEMIFUNGIBLE) {
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