How it Works

User joins giveaway

smart contract locks prize

Random winner is selected on-chain

Prize is sent automatically

Smart Contracts Code

Factory.sol

//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 {Giveaway} from "src/Giveaway.sol";

contract Factory {
    using SafeERC20 for ERC20;

    // Variables

    address private _owner;
    address private _linkToken;
    address private _vrfWrapper;

    // Events

    event NewGiveaway(uint256 indexed giveawayId, address indexed giveawayAddress, uint256 indexed endTime);

    // Modifiers

    modifier onlyOwner() {
        _onlyOwner();
        _;
    }

    function _onlyOwner() internal view {
        require(msg.sender == _owner, "Only factory owner can call");
    }

    // Constructor

    constructor(address linkToken, address vrfWrapper) {
        _owner = msg.sender;
        _linkToken = linkToken;
        _vrfWrapper = vrfWrapper;
    }

    receive() external payable {}

    // Methods

    function createGiveaway(
        uint64 giveawayId,
        address prizeToken,
        uint256 prizeAmount,
        uint64 duration,
        uint32 minParticipants,
        uint32 maxParticipants,
        bytes calldata signature
    ) external payable {
        // Validate owner signature

        bytes32 messageHash =
            keccak256(abi.encodePacked(giveawayId, prizeToken, prizeAmount, duration, minParticipants, maxParticipants));
        address signer = ECDSA.recover(messageHash, signature);
        require(signer == _owner, "Invalid owner signature");

        // Create giveaway

        Giveaway giveaway;

        if (prizeToken == address(0)) {
            // Prize is in ETH

            require(msg.value >= prizeAmount, "Not enough ETH sent");

            giveaway = new Giveaway{value: msg.value}(
                giveawayId,
                msg.sender,
                prizeToken,
                prizeAmount,
                duration,
                minParticipants,
                maxParticipants,
                _owner,
                address(this),
                _linkToken,
                _vrfWrapper
            );
        } else {
            // Prize is an ERC20 token

            giveaway = new Giveaway(
                giveawayId,
                msg.sender,
                prizeToken,
                prizeAmount,
                duration,
                minParticipants,
                maxParticipants,
                _owner,
                address(this),
                _linkToken,
                _vrfWrapper
            );

            ERC20(prizeToken).safeTransferFrom(msg.sender, address(giveaway), prizeAmount);
        }

        // Emit event

        uint256 endTime = block.timestamp + duration;
        emit NewGiveaway(giveawayId, address(giveaway), endTime);
    }

    function endGiveaway(address giveawayAddress, uint32 vrfCallbackGasLimit) external onlyOwner {
        Giveaway giveaway = Giveaway(giveawayAddress);

        // Check if giveaway is ready to be ended

        (bool isReady, string memory reason) = giveaway.isGiveawayReadyToEnd();
        require(isReady, reason);

        // Estimate VRF request price

        uint256 requestPrice = giveaway.i_vrfV2PlusWrapper().calculateRequestPrice(vrfCallbackGasLimit, 1);

        // Check LINK balance

        uint256 linkBalance = ERC20(_linkToken).balanceOf(address(this));
        require(linkBalance >= requestPrice, "LINK balance is too low");

        // Transfer LINK to giveaway contract

        ERC20(_linkToken).safeTransfer(giveawayAddress, requestPrice);

        // End giveaway

        giveaway.endGiveaway(vrfCallbackGasLimit, requestPrice);
    }

    function transfer(address token, address receiver, uint256 amount) external onlyOwner {
        if (amount == 0) return;

        if (token == address(0)) {
            (bool success,) = receiver.call{value: amount}("");
            require(success, "Transfer failed");
        } else {
            ERC20(token).safeTransfer(receiver, amount);
        }
    }
}
Loading...

Giveaway.sol

// 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";

contract Giveaway is VRFV2PlusWrapperConsumerBase {
    using SafeERC20 for ERC20;

    // Types

    enum GiveawayStatus {
        OPEN,
        PENDING,
        COMPLETED,
        CANCELED
    }

    // Variables

    uint64 private _giveawayId;
    address payable private _creator;
    address payable private _winner;
    address payable[] private _participants;
    mapping(address => bool) private _participantsMap;

    address private _prizeToken;
    uint256 private _prizeAmount;
    uint64 private _duration;
    uint256 private _startTime;
    GiveawayStatus private _status;
    uint32 _minParticipants;
    uint32 _maxParticipants;

    address private _owner;
    address payable private _factory;
    address private _linkToken;

    // Events

    event NewParticipant(address indexed participant);
    event Pending();
    event Completed(address indexed winner);
    event Canceled();

    // Constructor

    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 = GiveawayStatus.OPEN;
        _minParticipants = minParticipants;
        _maxParticipants = maxParticipants;

        _startTime = block.timestamp;
        _owner = owner;
        _factory = payable(factory);
        _linkToken = linkToken;
    }

    function checkCanEnterGiveaway(address applicant) public view returns (bool, string memory state) {
        if (_status != GiveawayStatus.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, "");
    }

    // Methods

    function enterGiveaway(bytes calldata signature) external {
        // Check if the address can participate

        (bool canEnter, string memory state) = checkCanEnterGiveaway(msg.sender);
        require(canEnter, state);

        // Validate owner signature

        bytes32 messageHash = keccak256(abi.encodePacked(address(this), msg.sender));
        address signer = ECDSA.recover(messageHash, signature);
        require(signer == _owner, "Invalid owner signature");

        // Add new participant

        _participants.push(payable(msg.sender));
        _participantsMap[msg.sender] = true;

        // Emit event

        emit NewParticipant(msg.sender);
    }

    // End methods

    function isGiveawayReadyToEnd() public view returns (bool isReady, string memory state) {
        if (_status == GiveawayStatus.COMPLETED || _status == GiveawayStatus.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");
    }

    function endGiveaway(uint32 vrfCallbackGasLimit, uint256 maxRequestPrice) public {
        (bool isReady, string memory state) = isGiveawayReadyToEnd();
        require(isReady, state);

        // Check the minimum number of participants

        if ((_minParticipants != 0 && _participants.length < _minParticipants) || _participants.length == 0) {
            cancelGiveaway();
            return;
        }

        // Prevent paying for VRF request with prize money

        if (_prizeToken == _linkToken) {
            require(getLinkBalance() - _prizeAmount >= maxRequestPrice, "LINK balance is too low");
        }

        // Request VRF

        (, uint256 requestPrice) = requestRandomness(vrfCallbackGasLimit, 3, 1, "");
        require(requestPrice <= maxRequestPrice, "Request price is too high");

        // Update status

        _status = GiveawayStatus.PENDING;

        // Emit event

        emit Pending();
    }

    function cancelGiveaway() internal {
        // Refund

        transfer(_prizeToken, _creator, _prizeAmount);
        transfer(_linkToken, _factory, getLinkBalance());

        // Update status

        _status = GiveawayStatus.CANCELED;

        // Emit event

        emit Canceled();
    }

    function fulfillRandomWords(uint256, uint256[] memory words) internal override {
        require(_status == GiveawayStatus.PENDING, "Giveaway is not pending");

        // Select winner

        uint256 index = words[0] % _participants.length;
        _winner = _participants[index];

        // Transfer money

        uint256 fee = _prizeAmount / 100;
        uint256 prize = _prizeAmount - fee;

        transfer(_prizeToken, _winner, prize);
        transfer(_prizeToken, _factory, fee);
        transfer(_linkToken, _factory, getLinkBalance());

        // Update status

        _status = GiveawayStatus.COMPLETED;

        // Emit event

        emit Completed(_winner);
    }

    // Util methods

    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);
        }
    }

    function getLinkBalance() internal view returns (uint256) {
        return ERC20(_linkToken).balanceOf(address(this));
    }

    // Info methods

    function getCreator() external view returns (address) {
        return _creator;
    }

    function getWinner() external view returns (address) {
        return _winner;
    }

    function getParticipants() external view returns (uint256) {
        return _participants.length;
    }

    function getStatus() external view returns (GiveawayStatus) {
        return _status;
    }

    function getTimeLeft() external view returns (uint256) {
        return _duration - (block.timestamp - _startTime);
    }

    function getPrize() external view returns (address, uint256) {
        uint256 balance;

        if (_prizeToken == address(0)) {
            balance = address(this).balance;
        } else {
            balance = ERC20(_prizeToken).balanceOf(address(this));
        }

        return (_prizeToken, balance);
    }
}
Loading...

Deployed Factories

Chain: Polygon Amoy

Address: 0x0C...c603

Total giveaways: 123

Chain: Sepolia

Address: 0x95...284b

Total giveaways: 123