Skip to content

Instantly share code, notes, and snippets.

@tokdaniel
Created April 25, 2023 07:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tokdaniel/e49920c1239facc86725cb5b873bbc51 to your computer and use it in GitHub Desktop.
Save tokdaniel/e49920c1239facc86725cb5b873bbc51 to your computer and use it in GitHub Desktop.
// 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