Created
March 20, 2023 15:21
-
-
Save tokdaniel/7f3afeef972bc4d9b66d7d9931eb26ea 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.18+commit.87f61d96.js&optimize=false&runs=200&gist=
This file contains 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
// SPDX-License-Identifier: MIT | |
pragma solidity ^0.8.17; | |
import "@openzeppelin/contracts/access/Ownable.sol"; | |
import "@openzeppelin/contracts/utils/Strings.sol"; | |
import "@openzeppelin/contracts/utils/math/Math.sol"; | |
import "@chainlink/contracts/src/v0.8/VRFConsumerBaseV2.sol"; | |
import "@chainlink/contracts/src/v0.8/interfaces/VRFCoordinatorV2Interface.sol"; | |
import "./interfaces/IMetaData.sol"; | |
import "./interfaces/IPrizePools.sol"; | |
import "./utils/RandomUtils.sol"; | |
import "./utils/ArrayUtils.sol"; | |
contract PrizePools is IPrizePools, VRFConsumerBaseV2, Ownable { | |
using AddressArrayUtils for address[]; | |
using UintArrayUtils for uint256[]; | |
using UintArrayUtils for uint16[]; | |
uint16 private constant REQUEST_CONFIRMATIONS = 3; | |
bytes32 private immutable i_keyHash; | |
uint32 private immutable i_callbackGasLimit; | |
uint64 private immutable i_subscriptionId; | |
VRFCoordinatorV2Interface private immutable i_vrfCoordinator; | |
IMetaData private immutable i_metaData; | |
mapping(uint256 => bytes) private s_poolOpenings; | |
mapping(uint256 => PoolChunk) private s_requestIdToPoolChunk; | |
mapping(address => mapping(uint256 => bool)) private s_userCursor; | |
mapping(address => mapping(uint256 => bool)) private s_winnerCursor; | |
PrizePool[] private s_pools; | |
constructor( | |
// VRF Requirements | |
uint64 subscriptionId, | |
bytes32 keyHash, | |
uint32 callbackGasLimit, | |
address vrfCoordinator, | |
// VRF Requirements | |
IMetaData metaData, | |
uint16[] memory prizePoolsPoolOpenings, | |
uint256[][] memory prizePoolsPrizes | |
) Ownable() VRFConsumerBaseV2(vrfCoordinator) { | |
i_subscriptionId = subscriptionId; | |
i_keyHash = keyHash; | |
i_callbackGasLimit = callbackGasLimit; | |
i_vrfCoordinator = VRFCoordinatorV2Interface(vrfCoordinator); | |
i_metaData = metaData; | |
uint256 salt = 1337; | |
unchecked { | |
for (uint256 id = 0; id < prizePoolsPoolOpenings.length; id++) { | |
addPrizePool( | |
string.concat("SecondChance #", Strings.toString(id)), | |
prizePoolsPoolOpenings[id], | |
prizePoolsPrizes[id] | |
); | |
s_poolOpenings[prizePoolsPoolOpenings[id]] = abi.encodePacked( | |
id, | |
salt | |
); | |
} | |
} | |
} | |
function addPrizePool( | |
string memory name, | |
uint32 opensAt, | |
uint256[] memory prizes | |
) public onlyOwner { | |
s_pools.push( | |
createPrizePool(uint8(s_pools.length), opensAt, prizes, name) | |
); | |
} | |
function openPrizePool( | |
uint256 poolId, | |
uint32 openForSeconds | |
) external override { | |
if (poolId > s_pools.length - 1) { | |
revert PrizePools_PrizePoolNotFound(poolId); | |
} | |
if (msg.sender != address(i_metaData) && msg.sender != owner()) { | |
revert PrizePools_Unauthorized(); | |
} | |
PrizePool memory pool = s_pools[poolId]; | |
pool.isOpen = true; | |
pool.openUntil = uint32(block.timestamp) + openForSeconds; | |
s_pools[poolId] = pool; | |
emit PrizePools_PoolOpened(poolId, pool.openUntil); | |
} | |
function registerUser( | |
uint256 poolId, | |
address user | |
) external override onlyOwner { | |
if (poolId > s_pools.length - 1) { | |
revert PrizePools_PrizePoolNotFound(poolId); | |
} | |
if (!s_pools[poolId].isOpen) { | |
revert PrizePools_PoolNotOpen(poolId); | |
} | |
if (s_userCursor[user][poolId] == true) { | |
revert PrizePools_UserAlreadyRegisteredIn(poolId); | |
} | |
if (s_pools[poolId].openUntil < block.timestamp) { | |
revert PrizePools_PoolClosedForRegistration(poolId); | |
} | |
s_pools[poolId].registeredUsers.push(user); | |
s_userCursor[user][poolId] = true; | |
emit PrizePools_UserRegisteredIntoPool(user, poolId); | |
} | |
function initiateRaffle(uint16 poolId, uint8[] memory numOfTokens) public { | |
if (msg.sender != owner() && msg.sender != address(i_vrfCoordinator)) { | |
revert PrizePools_Unauthorized(); | |
} | |
if (!s_pools[poolId].isOpen) { | |
revert PrizePools_PoolNotOpen(poolId); | |
} | |
if (s_pools[poolId].openUntil > block.timestamp) { | |
revert PrizePools_PoolNotReadyForRaffle( | |
s_pools[poolId].openUntil - block.timestamp | |
); | |
} | |
if (s_pools[poolId].registeredUsers.length != numOfTokens.length) { | |
revert PrizePools_RaffleTokenCountMismatch(poolId); | |
} | |
s_pools[poolId].isOpen = false; | |
uint16 chunkSize = uint16( | |
numOfTokens.length / s_pools[poolId].prizes.length | |
); | |
console.log(chunkSize); | |
uint16[] memory userEntries = new uint16[]( | |
s_pools[poolId].registeredUsers.length | |
); | |
unchecked { | |
for (uint256 i = 0; i < numOfTokens.length; i++) { | |
userEntries[i] = calculateEntries(uint16(numOfTokens[i])); | |
} | |
} | |
unchecked { | |
for (uint256 i = 0; i < s_pools[poolId].prizes.length; i++) { | |
uint256 chunkStart = chunkSize * i; | |
uint256 chunkEnd = chunkStart + chunkSize; | |
if ( | |
i == s_pools[poolId].prizes.length - 1 && | |
chunkEnd < userEntries.length | |
) { | |
chunkEnd = userEntries.length; | |
} | |
s_pools[poolId].userEntryChunks.push( | |
userEntries.slice(chunkStart, chunkEnd) | |
); | |
} | |
} | |
initiateVRF(poolId, 0, chunkSize); | |
} | |
function initiateVRF( | |
uint16 poolId, | |
uint16 iteration, | |
uint16 chunkSize | |
) internal { | |
uint256 requestId = i_vrfCoordinator.requestRandomWords( | |
i_keyHash, | |
i_subscriptionId, | |
REQUEST_CONFIRMATIONS, | |
i_callbackGasLimit, | |
10 | |
); | |
s_requestIdToPoolChunk[requestId] = PoolChunk( | |
poolId, | |
iteration, | |
chunkSize | |
); | |
emit PrizePools_RaffleInitiated(poolId, requestId); | |
} | |
function fulfillRandomWords( | |
uint256 requestId, | |
uint256[] memory randomWords | |
) internal override { | |
uint256 offset = 0; | |
PoolChunk memory chunk = s_requestIdToPoolChunk[requestId]; | |
uint32 totalEntries = uint32( | |
s_pools[chunk.poolId].userEntryChunks[chunk.iteration].sum() | |
); | |
uint256 winnerIndex = RandomUtils.weightDistributedRandom( | |
randomWords[offset], | |
s_pools[chunk.poolId].userEntryChunks[chunk.iteration], | |
totalEntries, | |
true | |
); | |
address winner = s_pools[chunk.poolId].registeredUsers[ | |
chunk.iteration * chunk.size + winnerIndex | |
]; | |
console.log(chunk.iteration * chunk.size + winnerIndex); | |
while (s_winnerCursor[winner][chunk.poolId]) { | |
offset++; | |
winnerIndex = RandomUtils.weightDistributedRandom( | |
randomWords[offset], | |
s_pools[chunk.poolId].userEntryChunks[chunk.iteration], | |
totalEntries, | |
true | |
); | |
winner = s_pools[chunk.poolId].registeredUsers[ | |
chunk.iteration * chunk.size + winnerIndex | |
]; | |
} | |
s_pools[chunk.poolId].winners.push(winner); | |
s_winnerCursor[winner][chunk.poolId] = true; | |
if ( | |
s_pools[chunk.poolId].winners.length < | |
s_pools[chunk.poolId].prizes.length | |
) { | |
initiateVRF(chunk.poolId, ++chunk.iteration, chunk.size); | |
} else { | |
s_pools[chunk.poolId].winners = RandomUtils.shuffle( | |
s_pools[chunk.poolId].winners, | |
randomWords[0] | |
); | |
emit PrizePools_RaffleResults( | |
chunk.poolId, | |
s_pools[chunk.poolId].winners | |
); | |
} | |
} | |
function setPaid(uint256 poolId) external onlyOwner { | |
s_pools[poolId].isPaid = true; | |
} | |
function calculateEntries( | |
uint16 numOfScratchedTickets | |
) public pure override returns (uint16) { | |
if (numOfScratchedTickets >= 1 && numOfScratchedTickets < 5) { | |
return numOfScratchedTickets * 10; | |
} | |
if (numOfScratchedTickets >= 5 && numOfScratchedTickets < 10) { | |
return numOfScratchedTickets * 15; | |
} | |
if (numOfScratchedTickets >= 10 && numOfScratchedTickets < 15) { | |
return numOfScratchedTickets * 20; | |
} | |
if (numOfScratchedTickets >= 15 && numOfScratchedTickets < 20) { | |
return numOfScratchedTickets * 25; | |
} | |
if (numOfScratchedTickets >= 20 && numOfScratchedTickets < 25) { | |
return numOfScratchedTickets * 30; | |
} | |
if (numOfScratchedTickets >= 25 && numOfScratchedTickets < 30) { | |
return numOfScratchedTickets * 35; | |
} | |
if (numOfScratchedTickets >= 30) { | |
return 1200; | |
} | |
return 0; | |
} | |
function needsToOpenPool( | |
uint256 scratchedOffCount | |
) external view override returns (bool, uint256) { | |
bytes memory empty = new bytes(0); | |
if (keccak256(s_poolOpenings[scratchedOffCount]) == keccak256(empty)) { | |
return (false, 0); | |
} | |
(uint256 id, ) = abi.decode( | |
s_poolOpenings[scratchedOffCount], | |
(uint256, uint256) | |
); | |
return (true, id); | |
} | |
function validateEligibleTokenIds( | |
uint256[] calldata tokenIds | |
) public view override returns (uint256[] memory) { | |
uint256[] memory eligibleTokenIds; | |
uint256 counter = 0; | |
unchecked { | |
for (uint256 i = 0; i < tokenIds.length; i++) { | |
IMetaData.TokenMetaData memory metadata = i_metaData | |
.getTokenMetaData(tokenIds[i]); | |
if ( | |
metadata.status == IMetaData.NFTStatus.PRIZE_REVEALED && | |
metadata.winner == false | |
) { | |
eligibleTokenIds[counter] = (tokenIds[i]); | |
counter++; | |
} | |
} | |
} | |
return eligibleTokenIds; | |
} | |
function getPrizePools() external view returns (PrizePool[] memory) { | |
return s_pools; | |
} | |
function getWinners( | |
uint256 poolId | |
) external view returns (Winner[] memory) { | |
address[] memory winnerAddresses = s_pools[poolId].winners; | |
uint256[] memory prizes = s_pools[poolId].prizes; | |
Winner[] memory winners = new Winner[](winnerAddresses.length); | |
unchecked { | |
for (uint256 i = 0; i < winners.length; i++) { | |
winners[i] = Winner(winnerAddresses[i], prizes[i]); | |
} | |
} | |
return winners; | |
} | |
function logArray(uint16[] memory arr) internal view { | |
unchecked { | |
for (uint256 i = 0; i < arr.length; i++) { | |
console.log(arr[i]); | |
} | |
} | |
} | |
function createPrizePool( | |
uint16 id, | |
uint32 opensAt, | |
uint256[] memory prizes, | |
string memory name | |
) internal pure returns (PrizePool memory) { | |
address[] memory users; | |
uint16[][] memory entries; | |
address[] memory winners; | |
PrizePool memory pool = PrizePool({ | |
id: id, | |
opensAt: opensAt, | |
openUntil: 0, | |
isOpen: false, | |
isPaid: false, | |
name: name, | |
registeredUsers: users, | |
winners: winners, | |
prizes: prizes, | |
userEntryChunks: entries | |
}); | |
return pool; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment