Giveaway.sol
The Giveaway.sol contract represents a single giveaway on Xenona. Each giveaway runs independently, handling participant entries, enforcing rules, managing prizes, and using Chainlink VRF for verifiable random winner selection.
Overview
Giveaway.sol supports:
- Native or ERC-20 prizes
- Configurable participant limits and duration
- Signature-based entry verification
- Automatic winner selection via Chainlink VRF
- 1% platform fee for Xenona
- Emergency cancellation and refunds
This ensures trustless, transparent, and secure execution throughout the giveaway lifecycle.
Participation & Completion
Participants enter via factory-authorized signatures. The contract handles:
- Cancellation: if minimum participants are not met
- Single participant: completes immediately
- Multiple participants: requests randomness for winner selection
Upon completion:
- 1% fee goes to the Factory, 99% to the winner
- Remaining LINK is refunded to the Factory
- Status updates and events are emitted
Security & Fairness
- Signature-verified entries
- Fully on-chain state management
- Provably fair winner selection
- Automatic prize and fee distribution
Contract Source Code
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
import {ERC20} from "@openzeppelin/contracts/token/ERC20/ERC20.sol";
import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol";
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
import {VRFV2PlusWrapperConsumerBase} from "@chainlink/contracts/src/v0.8/vrf/dev/VRFV2PlusWrapperConsumerBase.sol";
/**
* @notice Individual giveaway contract with verifiably random winner selection
* @dev Integrates with Chainlink VRF v2.5 for provably fair winner selection. Each giveaway
* is deployed as a separate contract instance by the Factory. Supports both native
* and ERC20 token prizes with configurable participation limits and duration.
*
* Key Features:
* - Signature-based participant authorization to prevent spam
* - Chainlink VRF integration for verifiably random winner selection
* - Automatic refunds if minimum participant threshold not met
* - 1% platform fee on completed giveaways
* - Cancellation mechanism for problematic giveaways
*/
contract Giveaway is VRFV2PlusWrapperConsumerBase {
using SafeERC20 for ERC20;
/// Status of the giveaway lifecycle
enum Status {
OPEN, // Accepting participants
PENDING, // Awaiting VRF callback for winner selection
COMPLETED, // Winner selected and prize distributed
CANCELED // Giveaway canceled and prize refunded
}
/*//////////////////////////////////////////////////////////////
STORAGE
//////////////////////////////////////////////////////////////*/
/// Unique identifier for this giveaway
uint64 private _giveawayId;
/// Address of the giveaway creator who provided the prize
address payable private _creator;
/// Address of the randomly selected winner
address payable private _winner;
/// Array of all participant addresses
address payable[] private _participants;
/// Mapping for O(1) participant existence checks
mapping(address => bool) private _participantsMap;
/// Address of the prize token (address(0) for native token)
address private _prizeToken;
/// Amount of prize tokens to award
uint256 private _prizeAmount;
/// Minimum number of participants required for giveaway to end
uint32 private _minParticipants;
/// Maximum number of participants allowed (0 = unlimited)
uint32 private _maxParticipants;
/// Duration in seconds for participation period
uint64 private _duration;
/// Timestamp when the giveaway was created
uint256 private _startTime;
/// Current status of the giveaway
Status private _status;
/// Factory owner address for signature verification
address private _owner;
/// Factory contract address that deployed this giveaway
address payable private _factory;
/// Address of the LINK token contract used for VRF payments
address private _linkToken;
/*//////////////////////////////////////////////////////////////
EVENTS
//////////////////////////////////////////////////////////////*/
/// Emitted when a new participant successfully enters the giveaway
event NewParticipant(address participant);
/// Emitted when VRF request is made and giveaway enters pending state
event Pending();
/// Emitted when winner is selected and prize is distributed
event Completed(address winner);
/// Emitted when giveaway is canceled and prize is refunded
event Canceled();
/*//////////////////////////////////////////////////////////////
MODIFIERS
//////////////////////////////////////////////////////////////*/
/// Restricts function access to the factory contract only
modifier onlyFactory() {
_onlyFactory();
_;
}
/// Validates that the caller is the factory contract
function _onlyFactory() internal view {
require(msg.sender == _factory, "Only factory can call");
}
/*//////////////////////////////////////////////////////////////
CONSTRUCTOR
//////////////////////////////////////////////////////////////*/
/**
* @notice Initializes a new giveaway with specified parameters
* @dev Called by Factory contract during deployment
*/
constructor(
uint64 giveawayId,
address creator,
address prizeTokenType,
uint256 prizeAmount,
uint64 duration,
uint32 minParticipants,
uint32 maxParticipants,
address owner,
address factory,
address linkToken,
address vrfWrapper
) payable VRFV2PlusWrapperConsumerBase(vrfWrapper) {
_giveawayId = giveawayId;
_creator = payable(creator);
_prizeToken = prizeTokenType;
_prizeAmount = prizeAmount;
_duration = duration;
_status = Status.OPEN;
_minParticipants = minParticipants;
_maxParticipants = maxParticipants;
_startTime = block.timestamp;
_owner = owner;
_factory = payable(factory);
_linkToken = linkToken;
}
/*//////////////////////////////////////////////////////////////
PARTICIPATION LOGIC
//////////////////////////////////////////////////////////////*/
/**
* @notice Checks if an address is eligible to enter the giveaway
* @dev Validates giveaway status, applicant eligibility, and participant limits
*
* @param applicant Address to check for eligibility
*
* @return canEnter Whether the applicant can enter
* @return state Description of the result or reason for rejection
*
* Requirements:
* - Giveaway must be in OPEN status
* - Applicant must not be the creator
* - Applicant must not have already entered
* - Participant limit must not be exceeded (if set)
*/
function checkCanEnter(address applicant) public view returns (bool canEnter, string memory state) {
if (_status != Status.OPEN) {
return (false, "The giveaway does not accept new participants");
}
if (applicant == _creator) {
return (false, "You can not enter your own giveaway");
}
if (_participantsMap[applicant]) {
return (false, "You have already entered this giveaway");
}
if (_maxParticipants != 0 && _participants.length >= _maxParticipants) {
return (false, "The max participants limit has been exceeded");
}
return (true, "");
}
/**
* @notice Allows a user to enter the giveaway with owner authorization
* @dev Verifies owner signature and adds participant to the giveaway
*
* @param signature Owner's authorization signature for this participant
*
* Requirements:
* - Giveaway must be in OPEN status
* - Caller must not be the creator
* - Caller must not have already entered
* - Participant limit must not be exceeded
* - Signature must be from factory owner
*/
function enter(bytes calldata signature) external {
// Validate eligibility
(bool canEnter, string memory state) = checkCanEnter(msg.sender);
require(canEnter, state);
// Verify owner signature
bytes32 messageHash = keccak256(abi.encodePacked(address(this), msg.sender));
address signer = ECDSA.recover(messageHash, signature);
require(signer == _owner, "Invalid owner signature");
// Add participant
_participants.push(payable(msg.sender));
_participantsMap[msg.sender] = true;
emit NewParticipant(msg.sender);
}
/*//////////////////////////////////////////////////////////////
COMPLETION LOGIC
//////////////////////////////////////////////////////////////*/
/**
* @notice Checks if the giveaway is ready to be ended
* @dev Validates that duration expired or max participants reached
* @return readyToEnd Whether the giveaway can be ended
* @return state Description of the result
*
* Requirements:
* - Giveaway must not already be COMPLETED or CANCELED
* - Either max participants reached or duration expired
*/
function checkReadyToEnd() public view returns (bool readyToEnd, string memory state) {
if (_status == Status.COMPLETED || _status == Status.CANCELED) {
return (false, "Already ended");
}
if (_maxParticipants != 0 && _participants.length == _maxParticipants) {
return (true, "Ready to end");
}
if ((block.timestamp - _startTime) <= _duration) {
return (false, "Duration has not expired");
}
return (true, "Ready to end");
}
/**
* @notice Ends the giveaway and initiates winner selection
* @dev Handles three scenarios:
* - Cancels if minimum participants not met
* - Completes immediately if only one participant
* - Requests VRF for random selection with multiple participants
*
* @param vrfCallbackGasLimit Gas limit for VRF callback execution
* @param maxRequestPrice Maximum acceptable price for VRF request
*
* Requirements:
* - Giveaway must be ready to end (duration expired or max participants reached)
* - Must have sufficient LINK balance for VRF request
* - VRF request price must not exceed maxRequestPrice
*/
function end(uint32 vrfCallbackGasLimit, uint256 maxRequestPrice) public {
(bool readyToEnd, string memory state) = checkReadyToEnd();
require(readyToEnd, state);
// Cancel if minimum participants not met
if ((_minParticipants != 0 && _participants.length < _minParticipants) || _participants.length == 0) {
_cancel();
return;
}
// Complete immediately if only one participant
if (_participants.length == 1) {
complete(_participants[0]);
return;
}
// Ensure LINK balance is sufficient for VRF
if (_prizeToken == _linkToken) {
require(getLinkBalance() - _prizeAmount >= maxRequestPrice, "LINK balance is too low");
}
// Request random number from VRF
(, uint256 requestPrice) = requestRandomness(vrfCallbackGasLimit, 3, 1, "");
require(requestPrice <= maxRequestPrice, "Request price is too high");
_status = Status.PENDING;
emit Pending();
}
/**
* @notice Cancels the giveaway and refunds the prize
* @dev Can only be called by factory. Refunds prize to creator and LINK to factory
*
* Requirements:
* - Caller must be factory
* - Giveaway must be in OPEN or PENDING status
*/
function cancel() external onlyFactory {
require(_status == Status.OPEN || _status == Status.PENDING, "Giveaway ended");
_cancel();
}
/// Internal function that executes cancellation logic
function _cancel() internal {
// Refund prize to creator
transfer(_prizeToken, _creator, _prizeAmount);
// Refund LINK to factory
transfer(_linkToken, _factory, getLinkBalance());
_status = Status.CANCELED;
emit Canceled();
}
/**
* @notice Callback function called by Chainlink VRF with random number
* @dev Selects winner using modulo of random word and completes giveaway
*
* Requirements:
* - Giveaway must be in PENDING status
* - Can only be called by VRF Wrapper contract
*/
function fulfillRandomWords(uint256, uint256[] memory words) internal override {
require(_status == Status.PENDING, "Giveaway is not pending");
// Select random participant using modulo
uint256 index = words[0] % _participants.length;
complete(_participants[index]);
}
/**
* @notice Completes the giveaway and distributes prize and platform fee
* @dev Transfers 99% of prize to winner and 1% fee to factory
*/
function complete(address winner) internal {
_winner = payable(winner);
// Calculate 1% platform fee
uint256 fee = _prizeAmount / 100;
uint256 prize = _prizeAmount - fee;
// Distribute prize and fee
transfer(_prizeToken, _winner, prize);
transfer(_prizeToken, _factory, fee);
// Refund remaining LINK to factory
transfer(_linkToken, _factory, getLinkBalance());
_status = Status.COMPLETED;
emit Completed(_winner);
}
/*//////////////////////////////////////////////////////////////
UTILITY FUNCTIONS
//////////////////////////////////////////////////////////////*/
/// Internal function for transferring tokens
function transfer(address token, address receiver, uint256 amount) internal {
if (amount == 0) return;
if (token == address(0)) {
(bool success,) = receiver.call{value: amount}("");
require(success, "Transfer failed");
} else {
ERC20(token).safeTransfer(receiver, amount);
}
}
/**
* @return linkBalance Contract's LINK token balance
*/
function getLinkBalance() internal view returns (uint256 linkBalance) {
return ERC20(_linkToken).balanceOf(address(this));
}
/*//////////////////////////////////////////////////////////////
VIEW FUNCTIONS
//////////////////////////////////////////////////////////////*/
/**
* @return giveawayId Unique ID assigned to this giveaway
*/
function getId() external view returns (uint64 giveawayId) {
return _giveawayId;
}
/**
* @return creator Address that created and funded this giveaway
*/
function getCreator() external view returns (address creator) {
return _creator;
}
/**
* @return winner Winner address
*/
function getWinner() external view returns (address winner) {
return _winner;
}
/**
* @return participants Total count of participants who have entered
*/
function getParticipants() external view returns (uint256 participants) {
return _participants.length;
}
/**
* @return status Current giveaway status
*/
function getStatus() external view returns (Status status) {
return _status;
}
/**
* @return time Time left in seconds until participation period expires
*/
function getTimeLeft() external view returns (uint256 time) {
return _duration - (block.timestamp - _startTime);
}
/**
* @dev For native token prizes returns address(0) and contract balance
* @return token Address of prize token
* @return balance Current prize balance held by contract
*/
function getPrize() external view returns (address token, uint256 balance) {
if (_prizeToken == address(0)) {
balance = address(this).balance;
} else {
balance = ERC20(_prizeToken).balanceOf(address(this));
}
return (_prizeToken, balance);
}
}
Giveaway.sol guarantees each giveaway runs autonomously, fairly, and securely, providing verifiable outcomes and trustless participation for all users.