Skip to main content

Donation

Donate

Overview

The Donation contract is a smart contract that allows users to contribute ERC20 tokens to participate in a donation-based lottery. A portion of the contributions is allocated to a prize pool, a developer fee, and a charitable donation. Winners are selected based on a randomized draw using the Pyth Network's entropy service to ensure fairness.

Features

  • Token Contribution: Participants send ERC20 tokens to the contract. The contributions are split between the prize pool, developer fee, and a charitable donation.
  • Random Winner Selection: Winners are chosen using a random number provided by the Pyth Network's entropy service.
  • Transparency: Detailed records of participants, contributions, and winners are maintained.

Key Components

State Variables

  • entropy: Instance of the Pyth Network's entropy service.
  • token: ERC20 token used for contributions.
  • participants: Array of participant addresses who have contributed tokens.
  • contributions: Mapping of participant addresses to their contribution amounts.
  • hasParticipated: Mapping to track whether an address has participated.
  • winners: Array of selected winner addresses.
  • winningNumbers: Array of winning numbers corresponding to each winner.
  • participantRanges: Mapping of participant addresses to their contribution range in the weighted selection.
  • totalPool: Total amount in the prize pool.
  • minTokenAmount: Minimum token amount required for participation.
  • startTime: Time when the contract starts accepting contributions.
  • totalTokensSent: Total tokens sent by participants.
  • tokensDonated: Total tokens donated.
  • developerAddress: Address receiving the developer fee.
  • donationAddress: Address receiving the donation portion.
  • drawState: Current state of the draw (OPEN, CLOSED, IN_PROGRESS).
  • currentWinnerIndex: Index of the current winner being selected.
  • winnerSet: Mapping to ensure winners are unique.

Constants

  • WINNERS_COUNT: Number of winners to be selected (10).
  • MAX_PARTICIPANTS: Maximum number of participants allowed (10000).
  • BPS_FACTOR: Basis points factor for percentage calculations (10000).
  • POOL_BPS: Basis points allocated to the prize pool (5000).
  • DEVELOPER_BPS: Basis points allocated to the developer fee (1000).
  • DONATION_BPS: Basis points allocated to the donation (4000).

Structs

  • ParticipantRange: Represents the start and end of a participant's contribution range in the weighted selection.

Functions

constructor

Initializes the contract with:

  • _entropyAddress: Address of the Pyth Network's entropy service.
  • _tokenAddress: Address of the ERC20 token.
  • _minTokenAmount: Minimum token amount for participation.
  • _startTime: Start time for accepting contributions.
  • _developerAddress: Address for developer fees.
  • _donationAddress: Address for donations.

getEntropy

Overrides the getEntropy function to return the address of the Entropy service.

sendTokens

Allows users to send tokens to participate in the draw:

  • Parameters: _amount (Amount of tokens to send).
  • Actions:
    • Transfers tokens to the contract.
    • Allocates portions to the prize pool, developer, and donation.
    • Adds participant if not already present.
    • Updates participant contributions and emits a TokensSent event.

findParticipantIndex

Finds the participant corresponding to a weighted random number:

  • Parameters: weightedRandom (Weighted random number).
  • Returns: Address of the participant.

entropyCallback

Callback function from Entropy SDK to process the random number:

  • Parameters: randomNumber (Random number generated by Entropy).
  • Actions:
    • Validates the draw state and selects a winner.
    • Updates winner list and transfers tokens to the winner.
    • Emits a RandomNumberRecorded and DrawExecuted event.

drawWinners

Starts the draw process:

  • Parameters: clientRandomNumber (Random number generated by the client).
  • Actions:
    • Requests a random number from the Entropy service.
    • Updates the draw state to IN_PROGRESS.
    • Emits a DrawStarted event.

View Functions

  • getParticipants: Returns the list of participant addresses.
  • getParticipantContribution: Returns the contribution amount for a specific participant.
  • getTotalPool: Returns the total amount in the prize pool.
  • getTotalTokensSent: Returns the total amount of tokens sent by participants.
  • getTokensDonated: Returns the total amount of tokens donated.
  • getDonationAddress: Returns the donation address.
  • getWinners: Returns the list of winners.
  • getParticipantsCount: Returns the number of participants.
  • getParticipantsAndContributions: Returns the list of participants and their contributions.
  • getParticipantChanceAndRange: Returns a participant's chance of winning and their contribution range in the accumulated weight.

Use Cases

  1. Lottery Draws: Run token-based lottery draws with transparent winner selection.
  2. Charity Fundraising: Allocate a portion of contributions to charitable causes.
  3. Transparent and Fair Draws: Ensure fair and verifiable random number generation and winner selection.

Security Considerations

  • Ensure correct configuration of contract and Entropy service addresses.
  • Handle fees and token transfers securely to prevent unauthorized access.

Additional Information

For more details on integrating with the Pyth Network's entropy service or customizing the contract, refer to the Pyth Network Documentation and OpenZeppelin Documentation.

Contract Source

// SPDX-License-Identifier: BUSL-1.1
pragma solidity ^0.8.10;

import '@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol';
import '@pythnetwork/entropy-sdk-solidity/IEntropy.sol';
import '@openzeppelin/contracts/token/ERC20/IERC20.sol';
import '@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol';
import '@openzeppelin/contracts/security/ReentrancyGuard.sol';

contract Donation is IEntropyConsumer, ReentrancyGuard {
using SafeERC20 for IERC20;

IEntropy public immutable entropy;
IERC20 public immutable token;

address[] public participants; // List of participants who contributed tokens
mapping(address => uint256) public contributions; // Mapping of contributions by participant address
mapping(address => bool) public hasParticipated; // Mapping to check if an address has participated

address[] public winners; // List of winners
uint256[] public winningNumbers; // List of winning numbers corresponding to winners
mapping(address => ParticipantRange) public participantRanges; // Mapping of participant ranges for weighted selection
uint256 public totalPool; // Total amount in the prize pool
uint256 public immutable minTokenAmount; // Minimum token amount required to participate
uint256 public immutable startTime; // Start time of the draw
uint256 public totalTokensSent; // Total tokens sent by participants
uint256 public tokensDonated; // Total tokens donated
uint256 public constant WINNERS_COUNT = 10; // Number of winners to be selected
uint256 public constant MAX_PARTICIPANTS = 100000; // Maximum number of participants allowed
address public immutable developerAddress; // Address for developer fee
address public immutable donationAddress; // Address for donations

struct ParticipantRange {
uint256 startRange; // Start of the participant's range in accumulated weight
uint256 endRange; // End of the participant's range in accumulated weight
}

uint256 public constant BPS_FACTOR = 10000; // Basis points factor for percentage calculations
uint256 public constant POOL_BPS = 5000; // Basis points for pool allocation
uint256 public constant DEVELOPER_BPS = 1000; // Basis points for developer fee
uint256 public constant DONATION_BPS = 4000; // Basis points for donation allocation

enum DrawState {
OPEN, // Draw is open for participation
CLOSED, // Draw is closed
IN_PROGRESS // Draw is currently in progress
}
DrawState public drawState;

uint256 public currentWinnerIndex; // Index of the current winner
mapping(address => bool) public winnerSet; // Mapping to ensure winners are unique

event DrawStarted(); // Event emitted when the draw starts
event DrawExecuted(address winner, uint256 winnerShare); // Event emitted when a winner is selected and rewarded
event TokensSent(address sender, uint256 amount); // Event emitted when tokens are sent
event RandomNumberRecorded(bytes32 randomNumber); // Event emitted when a random number is recorded

constructor(
address _entropyAddress,
address _tokenAddress,
uint256 _minTokenAmount,
uint256 _startTime,
address _developerAddress,
address _donationAddress
) {
entropy = IEntropy(_entropyAddress); // Initialize Entropy SDK
token = IERC20(_tokenAddress); // Initialize ERC20 token
minTokenAmount = _minTokenAmount; // Set minimum token amount for participation
startTime = _startTime; // Set start time of the draw
developerAddress = _developerAddress; // Set developer address
donationAddress = _donationAddress; // Set donation address
drawState = DrawState.OPEN; // Set initial draw state to OPEN
}

function getEntropy() internal view override returns (address) {
return address(entropy); // Return the address of the Entropy SDK
}

/**
* @notice Allows users to send tokens to participate in the draw.
* @param _amount The amount of tokens to send.
*/
function sendTokens(uint256 _amount) external nonReentrant {
require(drawState == DrawState.OPEN, 'Deposits are closed after the draw'); // Check if draw is open for deposits
require(participants.length < MAX_PARTICIPANTS, 'Participant limit reached'); // Check participant limit
require(_amount >= minTokenAmount, 'Minimum token amount not met'); // Check if the token amount meets the minimum requirement

uint256 amountForPool = (_amount * POOL_BPS) / BPS_FACTOR; // Calculate amount for the pool
uint256 amountForDeveloper = (_amount * DEVELOPER_BPS) / BPS_FACTOR; // Calculate amount for the developer
uint256 amountToDonation = (_amount * DONATION_BPS) / BPS_FACTOR; // Calculate amount for donation

token.safeTransferFrom(msg.sender, address(this), _amount); // Transfer tokens from sender to the contract
token.safeTransfer(developerAddress, amountForDeveloper); // Transfer developer fee
token.safeTransfer(donationAddress, amountToDonation); // Transfer donation amount
totalPool += amountForPool; // Update total pool amount

if (!hasParticipated[msg.sender]) {
participants.push(msg.sender); // Add participant if not already added
hasParticipated[msg.sender] = true; // Mark as participated
}

contributions[msg.sender] += _amount; // Update participant's contribution
totalTokensSent += _amount; // Update total tokens sent
tokensDonated += amountToDonation; // Update total tokens donated
emit TokensSent(msg.sender, _amount); // Emit event for token transfer

mergeSort(0, participants.length - 1); // Sort participants based on contribution
}

/**
* @notice MergeSort algorithm to sort participants based on their contribution ranges.
* @param low The starting index of the range to sort.
* @param high The ending index of the range to sort.
*/
function mergeSort(uint256 low, uint256 high) internal {
if (low < high) {
uint256 mid = (low + high) / 2;
mergeSort(low, mid);
mergeSort(mid + 1, high);
merge(low, mid, high);
}
}

/**
* @notice Merge function for MergeSort.
* @param low The starting index of the range to merge.
* @param mid The middle index of the range to merge.
* @param high The ending index of the range to merge.
*/
function merge(uint256 low, uint256 mid, uint256 high) internal {
uint256 n1 = mid - low + 1;
uint256 n2 = high - mid;

address[] memory left = new address[](n1);
address[] memory right = new address[](n2);

for (uint256 x = 0; x < n1; x++) {
left[x] = participants[low + x];
}
for (uint256 y = 0; y < n2; y++) {
right[y] = participants[mid + 1 + y];
}

uint256 i = 0;
uint256 j = 0;
uint256 k = low;

while (i < n1 && j < n2) {
if (participantRanges[left[i]].startRange <= participantRanges[right[j]].startRange) {
participants[k] = left[i];
i++;
} else {
participants[k] = right[j];
j++;
}
k++;
}

while (i < n1) {
participants[k] = left[i];
i++;
k++;
}

while (j < n2) {
participants[k] = right[j];
j++;
k++;
}
}

/**
* @notice Finds the participant corresponding to a weighted random number.
* @param weightedRandom The weighted random number.
* @return The address of the participant.
*/
function findParticipantIndex(uint256 weightedRandom) internal view returns (address) {
uint256 low = 0;
uint256 high = participants.length - 1;

while (low <= high) {
uint256 mid = (low + high) / 2;
ParticipantRange memory range = participantRanges[participants[mid]];

if (weightedRandom >= range.startRange && weightedRandom <= range.endRange) {
return participants[mid]; // Return participant if within range
} else if (weightedRandom < range.startRange) {
high = mid - 1;
} else {
low = mid + 1;
}
}

revert('No participant found'); // Revert if no participant found
}

/**
* @notice Callback function from Entropy SDK to process the random number.
* @param randomNumber The random number generated by Entropy.
*/
function entropyCallback(uint64 /*sequenceNumber*/, address /*provider*/, bytes32 randomNumber) internal override {
require(msg.sender == address(entropy), 'Caller is not authorized');
require(drawState == DrawState.IN_PROGRESS, 'Draw is not in progress');
require(currentWinnerIndex < WINNERS_COUNT, 'All winners have been selected');

emit RandomNumberRecorded(randomNumber);

uint256 accumulatedWeight = 0;

// Calculate accumulated weight and participant ranges
for (uint256 i = 0; i < participants.length; i++) {
address participant = participants[i];
uint256 contribution = contributions[participant];
participantRanges[participant] = ParticipantRange(accumulatedWeight, accumulatedWeight + contribution - 1);
accumulatedWeight += contribution;
}

uint256 weightedRandom = uint256(randomNumber) % accumulatedWeight; // Generate weighted random number
address winner = findParticipantIndex(weightedRandom); // Find the winner based on random number

require(!winnerSet[winner], 'Winner already selected');

winners.push(winner);
winnerSet[winner] = true;

uint256 winnerShare = totalPool / WINNERS_COUNT;
token.safeTransfer(winner, winnerShare);

winningNumbers.push(weightedRandom); // Update winningNumbers
emit DrawExecuted(winner, winnerShare);

currentWinnerIndex++;

if (currentWinnerIndex == WINNERS_COUNT) {
drawState = DrawState.CLOSED;
}
}

/**
* @notice Starts the draw process.
* @param clientRandomNumber A random number generated by the client.
*/
function drawWinners(bytes32 clientRandomNumber) external payable nonReentrant {
require(block.timestamp >= startTime + 3 hours, 'Draw can only be called 8 hours after start time');
require(participants.length > 0, 'No participants to draw winners');
require(currentWinnerIndex < WINNERS_COUNT, 'All winners have been selected');

address provider = entropy.getDefaultProvider();
uint128 fee = entropy.getFee(provider);

if (msg.value < fee) revert('not enough fees');

entropy.requestWithCallback{value: fee}(provider, clientRandomNumber);
drawState = DrawState.IN_PROGRESS;

emit DrawStarted();
}

// View functions to retrieve various contract states and participant details
function getParticipants() external view returns (address[] memory) {
return participants;
}

function getParticipantContribution(address _participant) external view returns (uint256) {
return contributions[_participant];
}

function getTotalPool() external view returns (uint256) {
return totalPool;
}

function getTotalTokensSent() external view returns (uint256) {
return totalTokensSent;
}

function getTokensDonated() external view returns (uint256) {
return tokensDonated;
}

function getDonationAddress() external view returns (address) {
return donationAddress;
}

function getWinners() external view returns (address[] memory, uint256[] memory, ParticipantRange[] memory) {
uint256 length = winners.length;
uint256[] memory winningNumbersArray = new uint256[](length);
ParticipantRange[] memory participantRangesArray = new ParticipantRange[](length);

for (uint256 i = 0; i < length; i++) {
winningNumbersArray[i] = winningNumbers[i];
participantRangesArray[i] = participantRanges[winners[i]];
}

return (winners, winningNumbersArray, participantRangesArray);
}

function getParticipantsCount() external view returns (uint256) {
return participants.length;
}

function getParticipantsAndContributions() external view returns (address[] memory, uint256[] memory) {
uint256[] memory contributionsList = new uint256[](participants.length);
for (uint256 i = 0; i < participants.length; i++) {
contributionsList[i] = contributions[participants[i]];
}
return (participants, contributionsList);
}

function getParticipantChanceAndRange(address participant) external view returns (uint256, uint256, uint256) {
ParticipantRange memory range = participantRanges[participant];
uint256 totalWeight = totalPool;
uint256 participantWeight = contributions[participant];
return ((participantWeight * BPS_FACTOR) / totalWeight, range.startRange, range.endRange);
}
}

Prepare deploy

const hre = require('hardhat');
const { ethers } = hre;

async function main() {
const network = hre.network.name;
console.log('Network', network);

const [signer] = await ethers.getSigners();
console.log('Signer', await signer.getAddress());

// CONTRACT DEPLOYMENT //
const Donation = await ethers.getContractFactory('Donation');
const entropyAddress = '0x6E7D74FA7d5c90FEF9F0512987605a6d546181Bb'; // Replace with actual entropy contract address
const tokenAddress = '0x0000000000000000000000000000000000000000'; // Replace with actual token contract address
const minTokenAmount = 1; // Replace with actual minimum token amount wei
const startTime = 1724759018; // Replace with actual unix start time
const developerAddress = '0x0000000000000000000000000000000000000000';
const donationAddress = '0x0000000000000000000000000000000000000000';

const donation = await Donation.deploy(entropyAddress, tokenAddress, minTokenAmount, startTime, developerAddress, donationAddress);
await donation.deployed();
console.log(`Donation deployed to ${donation.address}.`);
}

main().catch((error) => {
console.error(error);
process.exitCode = 1;
});

Example: Winner Selection Process

1. Setup

Let's assume the following initial conditions:

  • Total Pool: 10,000 tokens
  • Number of Winners: 10
  • Participants and Contributions:
    • Alice: 1,000 tokens
    • Bob: 2,000 tokens
    • Carol: 3,000 tokens
    • Dave: 4,000 tokens

2. Contribution Weights

The total weight of contributions is 10,000 tokens (sum of all individual contributions). The contribution ranges are calculated as follows:

  • Alice:
    • Contribution: 1,000 tokens
    • Start Range: 0
    • End Range: 999
  • Bob:
    • Contribution: 2,000 tokens
    • Start Range: 1,000
    • End Range: 2,999
  • Carol:
    • Contribution: 3,000 tokens
    • Start Range: 3,000
    • End Range: 5,999
  • Dave:
    • Contribution: 4,000 tokens
    • Start Range: 6,000
    • End Range: 9,999

3. Random Number Generation

The Entropy service generates a random number. For this example, let’s say the random number returned is 0x4b2a1f5e.

Convert the random number to a 256-bit unsigned integer:

uint256 randomNumber = uint256(0x4b2a1f5e);

4. Weighted Random Number

Calculate the weighted random number:

uint256 weightedRandom = randomNumber % 10,000; // 10,000 is the total accumulated weight

Suppose the result is 3,200.

5. Find Winner

Use the findParticipantIndex function to determine the winner based on the weighted random number:

address winner = findParticipantIndex(weightedRandom); // 3,200 falls into Carol's range

  • Weighted Random Number: 3,200
  • Participant Range Check:
    • Alice: 0 - 999
    • Bob: 1,000 - 2,999
    • Carol: 3,000 - 5,999
    • Dave: 6,000 - 9,999

Since 3,200 falls within Carol's range, Carol is selected as the winner.

6. Transfer Prize

Calculate the prize amount for each winner:

uint256 winnerShare = totalPool / WINNERS_COUNT; // 10,000 tokens / 10 winners = 1,000 tokens per winner

Transfer the prize to Carol:

token.safeTransfer(Carol, winnerShare); // Carol receives 1,000 tokens

7. Update Contract State

  • Add Carol to the list of winners:

    winners.push(Carol); winnerSet[Carol] = true;

  • Update the index for the next winner:

    currentWinnerIndex++;

If currentWinnerIndex reaches WINNERS_COUNT, close the draw:

if (currentWinnerIndex == WINNERS_COUNT) { drawState = DrawState.CLOSED; }

Summary

In this example:

  • The contract selects a winner based on a random number provided by the Entropy service.
  • The contribution ranges determine which participant's contribution weight corresponds to the generated random number.
  • The prize is distributed equally among the winners, and the contract state is updated accordingly.

This example demonstrates the essential steps involved in selecting winners and distributing prizes in the Donation contract.

Listing

Only contracts that can transparently justify the donation address and have passed community review are included in the interface listing. The developer address for collecting fees is the DePerp management address—please request this before deployment. The token being collected has been listed on DePerp.