Created
April 25, 2023 07:53
-
-
Save tokdaniel/e49920c1239facc86725cb5b873bbc51 to your computer and use it in GitHub Desktop.
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 "@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 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 { | |
PrizePool memory pool = createPrizePool( | |
uint16(s_pools.length), | |
opensAt, | |
prizes, | |
name | |
); | |
s_pools.push(pool); | |
emit PrizePools_PoolCreated(pool.name, pool.id, pool.opensAt); | |
} | |
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 | |
); | |
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 | |
]; | |
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 (TokenValidity[] memory) { | |
TokenValidity[] memory tokenValidity = new TokenValidity[]( | |
tokenIds.length | |
); | |
unchecked { | |
for (uint256 i = 0; i < tokenIds.length; i++) { | |
IMetaData.TokenMetaData memory metadata = i_metaData | |
.getTokenMetaData(tokenIds[i]); | |
tokenValidity[i] = TokenValidity( | |
tokenIds[i], | |
metadata.status == IMetaData.NFTStatus.PRIZE_REVEALED && | |
metadata.winner == false | |
); | |
} | |
} | |
return tokenValidity; | |
} | |
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 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