// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/IERC20.sol)
pragma solidity ^0.8.20;
/**
* @dev Interface of the ERC20 standard as defined in the EIP.
*/
interface IERC20 {
/**
* @dev Emitted when `value` tokens are moved from one account (`from`) to
* another (`to`).
*
* Note that `value` may be zero.
*/
event Transfer(address indexed from, address indexed to, uint256 value);
/**
* @dev Emitted when the allowance of a `spender` for an `owner` is set by
* a call to {approve}. `value` is the new allowance.
*/
event Approval(address indexed owner, address indexed spender, uint256 value);
/**
* @dev Returns the value of tokens in existence.
*/
function totalSupply() external view returns (uint256);
/**
* @dev Returns the value of tokens owned by `account`.
*/
function balanceOf(address account) external view returns (uint256);
/**
* @dev Moves a `value` amount of tokens from the caller's account to `to`.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transfer(address to, uint256 value) external returns (bool);
/**
* @dev Returns the remaining number of tokens that `spender` will be
* allowed to spend on behalf of `owner` through {transferFrom}. This is
* zero by default.
*
* This value changes when {approve} or {transferFrom} are called.
*/
function allowance(address owner, address spender) external view returns (uint256);
/**
* @dev Sets a `value` amount of tokens as the allowance of `spender` over the
* caller's tokens.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* IMPORTANT: Beware that changing an allowance with this method brings the risk
* that someone may use both the old and the new allowance by unfortunate
* transaction ordering. One possible solution to mitigate this race
* condition is to first reduce the spender's allowance to 0 and set the
* desired value afterwards:
* https://github.com/ethereum/EIPs/issues/20#issuecomment-263524729
*
* Emits an {Approval} event.
*/
function approve(address spender, uint256 value) external returns (bool);
/**
* @dev Moves a `value` amount of tokens from `from` to `to` using the
* allowance mechanism. `value` is then deducted from the caller's
* allowance.
*
* Returns a boolean value indicating whether the operation succeeded.
*
* Emits a {Transfer} event.
*/
function transferFrom(address from, address to, uint256 value) external returns (bool);
}
// SPDX-License-Identifier: MIT
// OpenZeppelin Contracts (last updated v5.0.0) (token/ERC20/extensions/IERC20Metadata.sol)
pragma solidity ^0.8.20;
import {IERC20} from "../IERC20.sol";
/**
* @dev Interface for the optional metadata functions from the ERC20 standard.
*/
interface IERC20Metadata is IERC20 {
/**
* @dev Returns the name of the token.
*/
function name() external view returns (string memory);
/**
* @dev Returns the symbol of the token.
*/
function symbol() external view returns (string memory);
/**
* @dev Returns the decimals places of the token.
*/
function decimals() external view returns (uint8);
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;
import {IERC20Metadata} from "@openzeppelin/contracts/token/ERC20/extensions/IERC20Metadata.sol";
/// @title Raffle contract to start a raffle with as many users as possible
/// @author @Bullrich
/// @notice Only the deployer of the contract can finish the raffle
/// @custom:security-contact info+security@rafflchain.com
contract Raffle {
/// Array with all the players participating. Each element represents a ticket
address[] public players;
/// Address of the deployer of the contract.
/// @notice This is the user that can finalize the raffle and receives the commision
address private immutable owner;
/// Timestamp of when the raffle ends
uint public immutable raffleEndDate;
/// Total amount of the tokens in the contract
uint public pot;
/// Token used in the contract as the currency
IERC20Metadata public immutable token;
/// Address of the winner
/// @dev this value is set up only after the raffle end
address public winner;
/// Container of ticket information.
struct Bundle {
uint amount;
uint price;
}
/// Price and amount of the small bundle
Bundle public smallBundle;
/// Price and amount of the medium bundle
/// @notice the final price should be discounted than buying the same amount of small bundles
Bundle public mediumBundle;
/// Price and amount of the big bundle
/// @notice the final price should be discounted than buying the same amount of small bundles
Bundle public largeBundle;
/// @param _ticketPrice Price of each ticket (without the decimals)
/// @param daysToEndDate Duration of the Raffle (in days)
/// @param _token Address of the ERC20 token that will be used in the Raffle
constructor(uint _ticketPrice, uint8 daysToEndDate, IERC20Metadata _token) {
raffleEndDate = getFutureTimestamp(daysToEndDate);
require(block.timestamp < raffleEndDate, "Unlock time should be in the future");
owner = msg.sender;
token = _token;
uint ticketPrice = _ticketPrice * (10 ** token.decimals());
smallBundle = Bundle(1, ticketPrice);
mediumBundle = Bundle(10, ticketPrice * 8);
largeBundle = Bundle(100, ticketPrice * 60);
}
/// Utility method used to buy any given amount of tickets
/// @param bundle the bundle that will be purchased
function buyCollectionOfTickets(Bundle memory bundle) private returns (uint) {
require(block.timestamp < raffleEndDate, "Raffle is over");
require(bundle.amount > 0, "Can not buy 0 tickets");
require(msg.sender != owner, "Owner cannot participate in the Raffle");
require(token.balanceOf(msg.sender) >= bundle.price, "Insufficient funds");
require(token.allowance(msg.sender, address(this)) >= bundle.price, "Insufficient Allowance");
token.transferFrom(msg.sender, address(this), bundle.price);
pot += bundle.price;
for (uint256 i = 0; i < bundle.amount; i++) {
players.push(msg.sender);
}
return bundle.amount;
}
/// Buy an individual ticket
function buySmallTicketBundle() public returns (uint) {
return buyCollectionOfTickets(smallBundle);
}
/// Buy a collection of 10 tickets
function buyMediumTicketBundle() public returns (uint) {
return buyCollectionOfTickets(mediumBundle);
}
/// Buy a collection of 100 tickets
function buyLargeTicketBundle() public returns (uint) {
return buyCollectionOfTickets(largeBundle);
}
/// Returns all the available bundles sorted from smaller to bigger
function getBundles() public view returns (Bundle[] memory) {
Bundle[] memory bundles = new Bundle[](3);
bundles[0] = smallBundle;
bundles[1] = mediumBundle;
bundles[2] = largeBundle;
return bundles;
}
/// User obtains a free ticket
/// @notice only the fist ticket is free
function getFreeTicket() public returns (uint) {
require(countUserTickets() == 0, "User already owns tickets");
require(msg.sender != owner, "Owner can not participate in the Raffle");
players.push(msg.sender);
return 1;
}
/// Check how many tickets the current user has
/// @return amount of tickets the user owns
function countUserTickets() public view returns (uint) {
uint tickets = 0;
for (uint256 i = 0; i < players.length; i++) {
if (players[i] == msg.sender) {
tickets++;
}
}
return tickets;
}
/// Function to calculate the timestamp X days from now
function getFutureTimestamp(uint8 daysFromNow) private view returns (uint256) {
require(daysFromNow > 0, "Future timestamp must be at least 1 day");
// Convert days to seconds
uint256 futureTimestamp = block.timestamp + (daysFromNow * 1 days);
return futureTimestamp;
}
/// List all the tickets in the system
/// @notice Can only be invoked by the contract owner
function listSoldTickets() public view returns (uint256) {
require(msg.sender == owner, "Invoker must be the owner");
return players.length;
}
/// Value used to generate randomness
uint private counter = 1;
function random() private returns (uint) {
counter++;
return uint(keccak256(abi.encodePacked(block.prevrandao, block.timestamp, players, counter)));
}
function pickRandomWinner() private returns (address) {
uint index = random() % players.length;
return players[index];
}
/// See what would be the prize pool with the current treasury
function prizePool() public view returns (uint) {
return pot / 2;
}
/// See what amount would be donated to the charity with the current treasury
function donationAmount() public view returns (uint) {
uint halfOfPot = prizePool();
uint commision = (halfOfPot / 100) * 5;
return halfOfPot - commision;
}
/// Method used to finish a raffle
/// @param donationAddress Address of the charity that will receive the tokens
/// @notice Can only be called by the owner after the timestamp of the raffle has been reached
function finishRaffle(address donationAddress) public returns (address) {
require(msg.sender == owner, "Invoker must be the owner");
require(block.timestamp > raffleEndDate, "End date has not being reached yet");
require(pot > 0, "The pot is empty. Raffle is invalid");
require(winner == address(0), "A winner has already been selected");
winner = pickRandomWinner();
// Divide into parts
uint halfOfPot = prizePool();
uint donation = donationAmount();
uint commision = (pot - halfOfPot) - donation;
// Send to the winner
token.transfer(winner, halfOfPot);
// Send to the charity address
token.transfer(donationAddress, donation);
// Get the commision
token.transfer(owner, commision);
return winner;
}
}
{
"compilationTarget": {
"contracts/Raffle.sol": "Raffle"
},
"evmVersion": "paris",
"libraries": {},
"metadata": {
"bytecodeHash": "ipfs"
},
"optimizer": {
"enabled": true,
"runs": 1000
},
"remappings": []
}
[{"inputs":[{"internalType":"uint256","name":"_ticketPrice","type":"uint256"},{"internalType":"uint8","name":"daysToEndDate","type":"uint8"},{"internalType":"contract IERC20Metadata","name":"_token","type":"address"}],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[],"name":"buyLargeTicketBundle","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"buyMediumTicketBundle","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"buySmallTicketBundle","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"countUserTickets","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"donationAmount","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"address","name":"donationAddress","type":"address"}],"name":"finishRaffle","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"getBundles","outputs":[{"components":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"price","type":"uint256"}],"internalType":"struct Raffle.Bundle[]","name":"","type":"tuple[]"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"getFreeTicket","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"name":"largeBundle","outputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"price","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"listSoldTickets","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"mediumBundle","outputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"price","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"players","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"pot","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"prizePool","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"raffleEndDate","outputs":[{"internalType":"uint256","name":"","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"smallBundle","outputs":[{"internalType":"uint256","name":"amount","type":"uint256"},{"internalType":"uint256","name":"price","type":"uint256"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"token","outputs":[{"internalType":"contract IERC20Metadata","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"winner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"}]