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.