Created
March 20, 2023 08:24
-
-
Save tokdaniel/c5fb43de193e0d62c019cbeb86502c2a 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 => uint256) private s_requestIdToPoolId; | |
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( | |
uint256 poolId, | |
uint16[][] calldata tokenIds | |
) public { | |
if (msg.sender != owner() && msg.sender != address(i_vrfCoordinator)) { | |
revert PrizePools_Unauthorized(); | |
} | |
// PrizePool memory pool = s_pools[poolId]; | |
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 != tokenIds.length) { | |
revert PrizePools_RaffleTokenCountMismatch(poolId); | |
} | |
s_pools[poolId].userEntries = new uint16[]( | |
s_pools[poolId].registeredUsers.length | |
); | |
for (uint256 i = 0; i < s_pools[poolId].registeredUsers.length; i++) { | |
s_pools[poolId].userEntries[i] = calculateEntries( | |
uint16(tokenIds[i].length) | |
); | |
} | |
uint32 totalEntries = uint32(s_pools[poolId].userEntries.sum()); | |
s_pools[poolId].isOpen = false; | |
s_pools[poolId].totalEntries = totalEntries; | |
initiateVRF(poolId); | |
} | |
function initiateVRF(uint poolId) internal { | |
uint256 requestId = i_vrfCoordinator.requestRandomWords( | |
i_keyHash, | |
i_subscriptionId, | |
REQUEST_CONFIRMATIONS, | |
i_callbackGasLimit, | |
10 | |
); | |
s_requestIdToPoolId[requestId] = poolId; | |
emit PrizePools_RaffleInitiated(poolId, requestId); | |
} | |
function fulfillRandomWords( | |
uint256 requestId, | |
uint256[] memory randomWords | |
) internal override { | |
uint256 offset = 0; | |
uint256 poolId = s_requestIdToPoolId[requestId]; | |
uint256 winnerIndex = RandomUtils.weightDistributedRandom( | |
randomWords[offset], | |
s_pools[poolId].userEntries, | |
s_pools[poolId].totalEntries, | |
true | |
); | |
address winner = s_pools[poolId].registeredUsers[winnerIndex]; | |
while (s_winnerCursor[winner][poolId]) { | |
offset++; | |
winnerIndex = RandomUtils.weightDistributedRandom( | |
randomWords[offset], | |
s_pools[poolId].userEntries, | |
s_pools[poolId].totalEntries, | |
true | |
); | |
winner = s_pools[poolId].registeredUsers[winnerIndex]; | |
} | |
s_pools[poolId].winners.push(winner); | |
s_winnerCursor[winner][poolId] = true; | |
if (s_pools[poolId].winners.length < s_pools[poolId].prizes.length) { | |
initiateVRF(poolId); | |
} else { | |
emit PrizePools_RaffleResults(poolId, s_pools[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 (address[] memory) { | |
return s_pools[poolId].winners; | |
} | |
function createPrizePool( | |
uint8 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, | |
totalEntries: 0, | |
isOpen: false, | |
isPaid: false, | |
name: name, | |
registeredUsers: users, | |
winners: winners, | |
prizes: prizes, | |
userEntries: entries | |
}); | |
return pool; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment