Created
March 15, 2023 08:08
-
-
Save tokdaniel/37adafa6a2782faf280d80c8e9286e3f 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/math/Math.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/IRaffle.sol"; | |
import "./utils/RandomUtils.sol"; | |
import "./utils/ArrayUtils.sol"; | |
import "hardhat/console.sol"; | |
contract Raffle is IRaffle, VRFConsumerBaseV2, Ownable { | |
using AddressArrayUtils for address[]; | |
using UintArrayUtils for uint256[]; | |
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 => bytes32) private s_poolOpenings; | |
mapping(bytes32 => RafflePrizePool) private s_pools; | |
bytes32[] s_poolIds; | |
mapping(uint256 => bytes32) private s_requestIdToPoolId; | |
mapping(bytes32 => Winner[]) s_poolWinners; | |
constructor( | |
// VRF Requirements | |
uint64 subscriptionId, | |
bytes32 keyHash, | |
uint32 callbackGasLimit, | |
address vrfCoordinator, | |
// VRF Requirements | |
IMetaData metaData, | |
bytes[4][] memory prizePoolSettings | |
) Ownable() VRFConsumerBaseV2(vrfCoordinator) { | |
i_subscriptionId = subscriptionId; | |
i_keyHash = keyHash; | |
i_callbackGasLimit = callbackGasLimit; | |
i_vrfCoordinator = VRFCoordinatorV2Interface(vrfCoordinator); | |
i_metaData = metaData; | |
unchecked { | |
for (uint256 index = 0; index < prizePoolSettings.length; index++) { | |
initializePool(prizePoolSettings[index], index); | |
} | |
} | |
} | |
function initializePool( | |
bytes[4] memory prizePoolSetting, | |
uint index | |
) private { | |
uint256 opensAt = abi.decode( | |
prizePoolSetting[uint256(PrizePoolSettings.OPENS_AT)], | |
(uint256) | |
); | |
string[] memory prizeNames = abi.decode( | |
prizePoolSetting[uint256(PrizePoolSettings.PRIZE_NAMES)], | |
(string[]) | |
); | |
uint256[] memory prizeQuantities = abi.decode( | |
prizePoolSetting[uint256(PrizePoolSettings.PRIZE_QUANTITIES)], | |
(uint256[]) | |
); | |
uint256[] memory prizeAmounts = abi.decode( | |
prizePoolSetting[uint256(PrizePoolSettings.PRIZE_AMOUNTS)], | |
(uint256[]) | |
); | |
string memory name = string.concat( | |
"Second Chance #", | |
Strings.toString(index + 1) | |
); | |
bytes32 poolId = keccak256(abi.encodePacked(name)); | |
address[] memory users; | |
uint256[][] memory tokens; | |
RafflePrizePool memory prizePool = RafflePrizePool( | |
poolId, | |
opensAt, | |
0, | |
name, | |
prizeNames, | |
prizeQuantities, | |
prizeAmounts, | |
users, | |
tokens, | |
false, | |
false, | |
true | |
); | |
s_poolOpenings[opensAt] = poolId; | |
s_pools[poolId] = prizePool; | |
s_poolIds.push(poolId); | |
} | |
function openPrizePool( | |
bytes32 poolId, | |
uint256 openForSeconds | |
) external override { | |
if (msg.sender != address(i_metaData) && msg.sender != owner()) { | |
revert Raffle_Unauthorized(); | |
} | |
RafflePrizePool memory pool = s_pools[poolId]; | |
if (!pool.exists) { | |
revert Raffle_PrizePoolNotFound(poolId); | |
} | |
if (pool.isOpen) { | |
revert Raffle_PoolAlreadyOpen(poolId); | |
} | |
pool.isOpen = true; | |
pool.openUntil = block.timestamp + openForSeconds; | |
s_pools[poolId] = pool; | |
emit Raffle_PoolOpened(poolId, pool.openUntil); | |
} | |
function addPrizePool( | |
string calldata name, | |
uint256 openForSeconds, | |
string[] calldata prizeNames, | |
uint256[] calldata prizeQuantities, | |
uint256[] calldata prizeAmounts | |
) external override onlyOwner { | |
bytes32 poolId = keccak256(abi.encodePacked(name)); | |
if (s_pools[poolId].exists) { | |
revert Raffle_PrizePoolAlreadyExists(poolId); | |
} | |
uint256 openUntil = block.timestamp + openForSeconds; | |
address[] memory users; | |
uint256[][] memory tokens; | |
RafflePrizePool memory prizePool = RafflePrizePool( | |
poolId, | |
0, | |
openUntil, | |
name, | |
prizeNames, | |
prizeQuantities, | |
prizeAmounts, | |
users, | |
tokens, | |
false, | |
false, | |
true | |
); | |
s_pools[poolId] = prizePool; | |
} | |
function registerUser( | |
bytes32 poolId, | |
address user | |
) external override onlyOwner { | |
RafflePrizePool memory pool = s_pools[poolId]; | |
if (!pool.exists) { | |
revert Raffle_PrizePoolNotFound(poolId); | |
} | |
if (!pool.isOpen) { | |
revert Raffle_PoolNotOpen(poolId); | |
} | |
if (pool.openUntil < block.timestamp) { | |
revert Raffle_RegistrationClosedByTime(poolId); | |
} | |
address[] memory registeredUsers = new address[]( | |
pool.registeredUsers.length + 1 | |
); | |
bool isDuplicate = false; | |
unchecked { | |
for (uint256 i = 0; i < pool.registeredUsers.length; i++) { | |
registeredUsers[i] = pool.registeredUsers[i]; | |
if (pool.registeredUsers[i] == user) { | |
isDuplicate = true; | |
} | |
} | |
} | |
if (isDuplicate) { | |
revert Raffle_UserAlreadyRegistered(poolId, user); | |
} | |
registeredUsers[registeredUsers.length - 1] = user; | |
pool.registeredUsers = registeredUsers; | |
s_pools[poolId] = pool; | |
emit Raffle_UserRegisteredIntoPool(user, poolId); | |
} | |
function initiateRaffle( | |
bytes32 poolId, | |
uint256[][] memory tokenIds | |
) external override onlyOwner { | |
RafflePrizePool memory pool = s_pools[poolId]; | |
if (!pool.exists) { | |
revert Raffle_PrizePoolNotFound(poolId); | |
} | |
if (pool.registeredUsers.length != tokenIds.length) { | |
revert Raffle_RaffleTokenCountMismatch(poolId); | |
} | |
// Maybe this is an unnecessary check | |
if (!pool.isOpen) { | |
revert Raffle_PoolNotOpen(poolId); | |
} | |
if (pool.openUntil > block.timestamp) { | |
revert Raffle_PoolNotReadyForRaffle( | |
pool.openUntil - block.timestamp | |
); | |
} | |
console.log( | |
"Closing pool: ", | |
pool.name, | |
"Users: ", | |
pool.registeredUsers.length | |
); | |
console.log("Tokens: ", tokenIds.length); | |
pool.isOpen = false; | |
pool.registeredTokens = tokenIds; | |
s_pools[poolId] = pool; | |
uint256 requestId = i_vrfCoordinator.requestRandomWords( | |
i_keyHash, | |
i_subscriptionId, | |
REQUEST_CONFIRMATIONS, | |
i_callbackGasLimit, | |
100 | |
); | |
s_requestIdToPoolId[requestId] = poolId; | |
emit Raffle_Initiated(poolId, requestId); | |
} | |
string[] private s_logs; | |
function getLogs() public view returns (string[] memory logs) { | |
return s_logs; | |
} | |
event Raffle_ResultsWinners( | |
bytes32 indexed poolId, | |
Winner[] indexed winners | |
); | |
function fulfillRandomWords( | |
uint256 requestId, | |
uint256[] memory randomWords | |
) internal override { | |
console.log("RequestId: ", requestId); | |
RafflePrizePool memory pool = s_pools[s_requestIdToPoolId[requestId]]; | |
console.log("FulfillRandomWords: ", pool.name); | |
uint drawablePrizes = Math.min( | |
pool.registeredUsers.length, | |
pool.prizeQuantities.sum() | |
); | |
console.log("drawablePrizes: ", drawablePrizes); | |
uint[] memory prizes = getPrizes(pool); | |
uint256[] memory entries = new uint256[](pool.registeredUsers.length); | |
unchecked { | |
for (uint256 i = 0; i < pool.registeredUsers.length; i++) { | |
entries[i] = calculateEntries(pool.registeredTokens[i].length); | |
} | |
} | |
uint256 totalEntries = entries.sum(); | |
console.log("Registered users: ", pool.registeredUsers.length); | |
console.log("Total entries", totalEntries); | |
address[] memory winners = new address[](drawablePrizes); | |
uint256 randomOffset = 0; | |
uint256 winnerIterator = 0; | |
while (winnerIterator < drawablePrizes) { | |
console.log( | |
"\nNew round: ", | |
randomOffset, | |
randomWords[randomOffset] | |
); | |
uint256 winnerIndex; | |
unchecked { | |
winnerIndex = RandomUtils.weightDistributedRandom( | |
randomWords[randomOffset++], | |
entries, | |
totalEntries, | |
true | |
); | |
} | |
address winner = pool.registeredUsers[winnerIndex]; | |
console.log("user", winner); | |
if (winners.contains(winner)) { | |
console.log("User already won", winnerIndex); | |
// The same user can't win multiple prizes | |
continue; | |
} | |
console.log( | |
"Winner iterator/index: ", | |
winnerIterator, | |
" / ", | |
winnerIndex | |
); | |
winners[winnerIterator] = winner; | |
uint prize = prizes[winnerIterator]; | |
console.log("prize", prize); | |
registerWinner(pool.id, winner, prize); | |
winnerIterator++; | |
} | |
console.log("Raffle ended"); | |
emit Raffle_Results(pool.id, winners); | |
} | |
function registerWinner( | |
bytes32 poolId, | |
address winner, | |
uint256 prize | |
) internal { | |
console.log("registerWinner: ", winner, prize); | |
console.log("winners count before: ", s_poolWinners[poolId].length); | |
s_poolWinners[poolId].push(Winner(winner, prize)); | |
logWinners(poolId); | |
console.log("winners count after: ", s_poolWinners[poolId].length); | |
} | |
function logWinners(bytes32 poolId) public view { | |
RafflePrizePool memory pool = s_pools[poolId]; | |
for (uint256 i = 0; i < s_poolWinners[pool.id].length; i++) { | |
console.log( | |
"poolwinners: ", | |
s_poolWinners[pool.id][i].user, | |
s_poolWinners[pool.id][i].prize | |
); | |
} | |
} | |
function getPrizes( | |
RafflePrizePool memory pool | |
) internal view returns (uint256[] memory) { | |
uint numOfPrizes = pool.prizeQuantities.sum(); | |
uint256[] memory prizes = new uint256[](numOfPrizes); | |
uint k = 0; | |
for (uint256 i = 0; i < pool.prizeQuantities.length; i++) { | |
for (uint256 j = 0; j < pool.prizeQuantities[i]; j++) { | |
prizes[k++] = pool.prizeAmounts[i]; | |
console.log("K: ", prizes[k - 1]); | |
} | |
} | |
console.log("Prizes: ", prizes.length); | |
return prizes; | |
} | |
function setPaid(bytes32 poolId) external onlyOwner { | |
s_pools[poolId].isPaid = true; | |
} | |
function calculateEntries( | |
uint256 numOfScratchedTickets | |
) public pure override returns (uint256) { | |
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, bytes32) { | |
if (s_poolOpenings[scratchedOffCount] == bytes32(0)) { | |
return (false, 0); | |
} | |
return (true, s_poolOpenings[scratchedOffCount]); | |
} | |
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 (RafflePrizePool[] memory) { | |
RafflePrizePool[] memory pools = new RafflePrizePool[]( | |
s_poolIds.length | |
); | |
for (uint i = 0; i < s_poolIds.length; i++) { | |
pools[i] = s_pools[s_poolIds[i]]; | |
} | |
return pools; | |
} | |
function getPrizePool( | |
bytes32 poolId | |
) external view returns (RafflePrizePool memory) { | |
return s_pools[poolId]; | |
} | |
function getWinners( | |
bytes32 poolId | |
) external view returns (Winner[] memory) { | |
return s_poolWinners[poolId]; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment