Skip to content

Instantly share code, notes, and snippets.

@tokdaniel
Created March 15, 2023 08:08
Show Gist options
  • Save tokdaniel/37adafa6a2782faf280d80c8e9286e3f to your computer and use it in GitHub Desktop.
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=
// 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