diff --git a/entropy/raffle-for-good/README.md b/entropy/raffle-for-good/README.md new file mode 100644 index 00000000..1b235a6f --- /dev/null +++ b/entropy/raffle-for-good/README.md @@ -0,0 +1,263 @@ +# RaffleForGood - Decentralized Raffle System with Pyth Entropy + +> A transparent crowdfunding platform that gamifies donations through blockchain-based raffles using Pyth Entropy for verifiable randomness. + +## 🎯 Overview + +RaffleForGood transforms traditional crowdfunding by creating raffles where participants buy tickets to support projects while having a chance to win prizes. The system uses **Pyth Entropy** to ensure fair and verifiable random winner selection. + +## ✨ Key Features + +- **Pyth Entropy Integration**: Verifiable random number generation for fair winner selection +- **Factory Pattern**: Easy raffle creation without code +- **Gas Optimized**: Binary search O(log n) for winner selection +- **Secure**: OpenZeppelin's PullPayment pattern for fund distribution +- **Transparent**: All operations on-chain and auditable +- **Low Fees**: Only 0.05% platform fee + +## 🏗️ Architecture + +``` +User → RaffleFactory.createRaffle() + ↓ +ProjectRaffle Contract + ↓ +buyTickets() → Pyth Entropy Request → entropyCallback() + ↓ +Winner Selection (Binary Search) → Fund Distribution +``` + +## 📋 Contracts + +### `ProjectRaffle.sol` +Main raffle contract that: +- Manages ticket purchases (1 wei = 1 ticket) +- Requests randomness from Pyth Entropy +- Selects winner using binary search +- Distributes funds via pull payment pattern + +### `RaffleFactory.sol` +Factory contract for creating multiple raffles with custom parameters. + +### Pyth Entropy Integration + +```solidity +// Request entropy +function requestEntropy(bytes32 userRandomNumber) external payable { + uint256 fee = entropy.getFee(entropyProvider); + entropySequenceNumber = entropy.request{value: fee}( + entropyProvider, + userRandomNumber, + true // use blockhash + ); +} + +// Receive callback +function entropyCallback( + uint64 sequenceNumber, + address provider, + bytes32 randomNumber +) external override { + require(msg.sender == address(entropy), "Only entropy contract"); + winner = _selectWinner(randomNumber); + state = RaffleState.DrawExecuted; +} +``` + +## 🚀 Deployment + +### Prerequisites + +```bash +npm install +``` + +### Environment Variables + +Create `.env`: +```env +PRIVATE_KEY=your_private_key +BASE_SEPOLIA_RPC_URL=your_rpc_url +ENTROPY_ADDRESS=0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c +``` + +### Deploy Factory + +```bash +npx hardhat ignition deploy ignition/modules/RaffleFactoryModule.ts --network baseSepolia +``` + +### Create a Raffle + +```bash +npx tsx scripts/createNewRaffle.ts +``` + +## 📖 Usage Example + +### 1. Create Raffle +```typescript +const tx = await factory.createRaffle( + "Save the Ocean", // name + "Help clean our oceans", // description + 3000, // 30% for project + projectAddress, // beneficiary + 604800 // 7 days duration +); +``` + +### 2. Buy Tickets +```typescript +await raffle.buyTickets({ value: parseEther("0.01") }); +``` + +### 3. Close Raffle & Select Winner +```typescript +// Request entropy (owner/admin only) +await raffle.requestEntropy(randomBytes32, { value: entropyFee }); + +// Pyth calls entropyCallback() automatically +// Winner is selected using binary search +``` + +### 4. Distribute Funds +```typescript +await raffle.distributeFunds(); +// Beneficiaries withdraw with withdrawPayments() +``` + +## 🧪 Testing + +```bash +npm test +``` + +Tests include: +- Ticket purchasing and range calculation +- Entropy request and callback simulation +- Winner selection accuracy +- Fund distribution correctness +- Edge cases and security scenarios + +## 🎲 Pyth Entropy Benefits + +1. **Verifiable Randomness**: Cryptographically secure and verifiable on-chain +2. **Tamper-Proof**: No party can manipulate the outcome +3. **Cost-Effective**: Low fee (~0.0001 ETH per request) +4. **Fast**: Callback within 2-5 minutes +5. **Reliable**: Backed by Pyth's oracle network + +## 📊 Live Deployment + +- **Network**: Base Sepolia +- **Factory**: `0x104032d5377be9b78441551e169f3C8a3d520672` +- **Entropy**: `0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c` +- **Active Raffles**: 10+ + +## 🔐 Security Features + +- ReentrancyGuard on all state-changing functions +- PullPayment pattern prevents reentrancy in withdrawals +- Only authorized addresses can request entropy +- Time-locked raffle closure (optional) +- Comprehensive input validation + +## 💡 Use Cases + +- **Open Source Projects**: Fund development while rewarding community +- **Social Causes**: Transparent fundraising for NGOs +- **Content Creators**: Monetize audience without ads +- **Web3 Startups**: Community-driven fundraising with incentives + +## 🛠️ Technical Highlights + +### Binary Search Winner Selection + +```solidity +function _selectWinner(bytes32 entropySeed) internal view returns (address) { + uint256 randomTicket = uint256(entropySeed) % totalTickets; + + uint256 left = 0; + uint256 right = participants.length - 1; + + while (left < right) { + uint256 mid = (left + right) / 2; + if (participants[mid].upperBound > randomTicket) { + right = mid; + } else { + left = mid + 1; + } + } + + return participants[left].owner; +} +``` + +This O(log n) algorithm efficiently finds winners even with millions of participants. + +## 📈 Project Structure + +``` +contract/ +├── contracts/ +│ ├── ProjectRaffle.sol # Main raffle logic +│ ├── RaffleFactory.sol # Factory for creating raffles +│ └── interfaces/ +│ ├── IEntropyConsumer.sol # Pyth callback interface +│ └── IEntropyV2.sol # Pyth entropy interface +├── scripts/ +│ ├── createNewRaffle.ts # Deploy new raffle +│ ├── buyTickets.ts # Purchase tickets +│ ├── closeRaffle.ts # Request entropy & close +│ ├── distributeFunds.ts # Distribute winnings +│ └── listRaffles.ts # List all raffles +├── hardhat.config.ts +├── package.json +└── tsconfig.json +``` + +## 🔄 Raffle Lifecycle + +1. **Creation**: Owner creates raffle with project details and percentages +2. **Active**: Users buy tickets (1 wei = 1 ticket) +3. **Close**: Owner requests Pyth Entropy for randomness +4. **Callback**: Pyth responds with random number, winner is selected +5. **Distribution**: Funds are split between project, winner, and platform +6. **Withdrawal**: Each beneficiary withdraws their share + +## 💰 Fee Structure + +- **Platform Fee**: 0.05% of total pool +- **Project**: Configurable percentage (e.g., 30%) +- **Winner**: Remaining pool after fees + +Example with 1 ETH pool and 30% project: +- Platform: 0.0005 ETH (0.05%) +- Project: 0.2998 ETH (30% of 0.9995 ETH) +- Winner: 0.6997 ETH (remaining) + +## 🚦 Getting Started + +1. Clone the repository +2. Install dependencies: `npm install` +3. Set up `.env` with your keys +4. Deploy factory: `npx hardhat ignition deploy...` +5. Create your first raffle: `npx tsx scripts/createNewRaffle.ts` +6. Share raffle address with participants +7. Close raffle after duration expires +8. Distribute funds and celebrate! 🎉 + +## 📝 License + +MIT + +## 🙋 Support & Contact + +- GitHub: [eth-global-hackathon](https://github.com/NicoCaz/eth-global-hackathon) +- Built for ETH Global Hackathon +- Powered by Pyth Network + +--- + +**Built with ❤️ using Pyth Entropy for fair and transparent crowdfunding** + diff --git a/entropy/raffle-for-good/contract/contracts/IEntropyConsumer.sol b/entropy/raffle-for-good/contract/contracts/IEntropyConsumer.sol new file mode 100644 index 00000000..1e3229bf --- /dev/null +++ b/entropy/raffle-for-good/contract/contracts/IEntropyConsumer.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @title IEntropyConsumer + * @notice Minimal interface for contracts that consume Pyth Entropy randomness. + * @dev Only the functions required by the project have been declared. + */ +interface IEntropyConsumer { + /** + * @notice Callback invoked by the Entropy contract when randomness is ready. + */ + function entropyCallback( + uint64 sequenceNumber, + address provider, + bytes32 randomNumber + ) external; + + /** + * @notice Returns the address of the Entropy contract being used. + */ + function getEntropy() external view returns (address); +} + diff --git a/entropy/raffle-for-good/contract/contracts/IEntropyV2.sol b/entropy/raffle-for-good/contract/contracts/IEntropyV2.sol new file mode 100644 index 00000000..f75db350 --- /dev/null +++ b/entropy/raffle-for-good/contract/contracts/IEntropyV2.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @title IEntropyV2 + * @notice Minimal subset of the Pyth Entropy V2 interface used by ProjectRaffle. + */ +interface IEntropyV2 { + /** + * @notice Returns the default randomness provider configured in the Entropy contract. + */ + function getDefaultProvider() external view returns (address); + + /** + * @notice Returns the fee required to request randomness from a provider. + */ + function getFee(address provider) external view returns (uint256); + + /** + * @notice Requests randomness from the Entropy contract. + * @param provider Randomness provider address. + * @param userRandomNumber User provided commitment (bytes32). + * @param useBlockhash Flag to mix in the blockhash. + * @return sequenceNumber Unique sequence identifier for the request. + */ + function request( + address provider, + bytes32 userRandomNumber, + bool useBlockhash + ) external payable returns (uint64 sequenceNumber); +} + diff --git a/entropy/raffle-for-good/contract/contracts/ProjectRaffle.sol b/entropy/raffle-for-good/contract/contracts/ProjectRaffle.sol new file mode 100644 index 00000000..2b34c816 --- /dev/null +++ b/entropy/raffle-for-good/contract/contracts/ProjectRaffle.sol @@ -0,0 +1,345 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import { PullPayment } from "@openzeppelin/contracts/security/PullPayment.sol"; +import { ReentrancyGuard } from "@openzeppelin/contracts/security/ReentrancyGuard.sol"; +import { IEntropyConsumer } from "./interfaces/IEntropyConsumer.sol"; +import { IEntropyV2 } from "./interfaces/IEntropyV2.sol"; + +/** + * @title ProjectRaffle + * @notice Contrato de rifa para proyectos con integración de Pyth Entropy + * @dev Los fondos se distribuyen entre: proyecto, owner (mantenimiento), y ganador + * @dev Usa PullPayment para seguridad y Binary Search para eficiencia + */ +contract ProjectRaffle is Ownable, ReentrancyGuard, PullPayment, IEntropyConsumer { + // Información del proyecto + string public projectName; + string public projectDescription; + uint256 public projectPercentage; // Porcentaje para el proyecto (Basis Points: 100 = 1%) + uint256 public constant BASIS_POINTS = 10000; + uint256 public constant PLATFORM_FEE = 5; // 0.05% (5/10000) + uint256 public constant MIN_TICKET_PRICE = 0.0001 ether; + + // Estado de la rifa + enum RaffleState { Active, EntropyRequested, DrawExecuted } + RaffleState public state; + + // Participantes y tickets + struct TicketRange { + address owner; + uint256 upperBound; + } + TicketRange[] public participants; + uint256 public totalTickets; + + + // Ganador y distribución + address public winner; + bool public fundsDistributed; + + // Control de tiempo + uint256 public immutable raffleDuration; + uint256 public immutable raffleStartTime; + address public projectAddress; // Dirección del proyecto guardada al crear la rifa + address public platformAdmin; // Administrador de la plataforma + + // Pyth Entropy + IEntropyV2 public entropy; + address public entropyProvider; + uint64 public entropySequenceNumber; + + // Eventos + event TicketPurchased(address indexed buyer, uint256 amount, uint256 ticketCount); + event EntropyRequested(uint64 sequenceNumber); + event DrawExecuted(address indexed winner, uint256 ticketNumber); + event FundsDistributed( + address indexed projectAddress, + address indexed owner, + address indexed winner, + uint256 projectAmount, + uint256 ownerAmount, + uint256 winnerAmount + ); + + /** + * @notice Constructor del contrato + * @param _projectName Nombre del proyecto + * @param _projectDescription Descripción del proyecto + * @param _projectPercentage Porcentaje del proyecto en basis points (0-10000) + * @param _entropyAddress Dirección del contrato de Pyth Entropy + * @param _initialOwner Dirección del owner inicial + * @param _platformAdmin Dirección del administrador de la plataforma + * @param _projectAddress Dirección del proyecto que recibirá fondos + * @param _raffleDuration Duración de la rifa en segundos + */ + constructor( + string memory _projectName, + string memory _projectDescription, + uint256 _projectPercentage, + address _entropyAddress, + address _initialOwner, + address _platformAdmin, + address _projectAddress, + uint256 _raffleDuration + ) { + require(_projectPercentage > 0, "Project percentage must be > 0"); + require(_projectPercentage <= BASIS_POINTS, "Project percentage cannot exceed 100%"); + require(_entropyAddress != address(0), "Invalid Entropy address"); + require(_projectAddress != address(0), "Invalid project address"); + require(_platformAdmin != address(0), "Invalid admin address"); + require(_raffleDuration > 0, "Duration must be > 0"); + require(_initialOwner != address(0), "Invalid owner address"); + + projectName = _projectName; + projectDescription = _projectDescription; + projectPercentage = _projectPercentage; + entropy = IEntropyV2(_entropyAddress); + projectAddress = _projectAddress; + platformAdmin = _platformAdmin; + raffleDuration = _raffleDuration; + raffleStartTime = block.timestamp; + + // Obtener proveedor por defecto de Pyth + entropyProvider = entropy.getDefaultProvider(); + require(entropyProvider != address(0), "No default provider available"); + + state = RaffleState.Active; + _transferOwnership(_initialOwner); + } + + /** + * @notice Permite a los usuarios comprar tickets + * @dev 1 wei = 1 ticket + */ + function buyTickets() external payable { + require(state == RaffleState.Active, "Raffle not active"); + require(block.timestamp < raffleStartTime + raffleDuration, "Raffle ended"); + require(msg.value >= MIN_TICKET_PRICE, "Minimum ticket price is 0.0001 ETH"); + + totalTickets += msg.value; + + // Guardar rango de tickets para este usuario + // El límite superior es el nuevo total de tickets + participants.push(TicketRange({ + owner: msg.sender, + upperBound: totalTickets + })); + + emit TicketPurchased(msg.sender, msg.value, totalTickets); // Emitimos total acumulado + } + + modifier onlyOwnerOrAdmin() { + require(msg.sender == owner() || msg.sender == platformAdmin, "Not authorized"); + _; + } + + /** + * @notice Solicita entropía a Pyth para ejecutar el sorteo + * @dev Solo el owner o admin puede ejecutar esta función + * @param userRandomNumber Número aleatorio generado por el usuario + */ + function requestEntropy(bytes32 userRandomNumber) external payable onlyOwnerOrAdmin { + require(state == RaffleState.Active, "Raffle not active"); + // require(block.timestamp >= raffleStartTime + raffleDuration, "Raffle still active"); // Comentado para permitir cierre anticipado + require(totalTickets > 0, "No tickets sold"); + require(participants.length > 0, "No participants"); + + state = RaffleState.EntropyRequested; + + // Obtener el fee necesario para la solicitud + uint256 fee = entropy.getFee(entropyProvider); + require(msg.value >= fee, "Insufficient fee"); + + // Solicitar entropía a Pyth + entropySequenceNumber = entropy.request{value: fee}( + entropyProvider, + userRandomNumber, + true // use blockhash + ); + + emit EntropyRequested(entropySequenceNumber); + + // Devolver exceso de fondos si los hay + if (msg.value > fee) { + payable(msg.sender).transfer(msg.value - fee); + } + } + + /** + * @notice Callback de Pyth con la entropía generada + * @dev Esta función será llamada por el contrato de Entropy + * @param sequenceNumber Número de secuencia de la solicitud + * @param provider Dirección del proveedor + * @param randomNumber Número aleatorio generado + */ + function entropyCallback( + uint64 sequenceNumber, + address provider, + bytes32 randomNumber + ) external override { + require(msg.sender == address(entropy), "Only entropy contract"); + require(state == RaffleState.EntropyRequested, "Entropy not requested"); + require(sequenceNumber == entropySequenceNumber, "Invalid sequence number"); + require(provider == entropyProvider, "Invalid provider"); + + // Seleccionar ganador + winner = _selectWinner(randomNumber); + state = RaffleState.DrawExecuted; + + emit DrawExecuted(winner, uint256(randomNumber) % totalTickets); + } + + /** + * @notice Obtiene la dirección del contrato de Entropy + * @return Dirección del contrato de Entropy + */ + function getEntropy() external view override returns (address) { + return address(entropy); + } + + /** + * @notice Distribuye los fondos entre proyecto, owner y ganador usando PullPayment + * @dev Los beneficiarios deben llamar a withdrawPayments() para retirar sus fondos + */ + function distributeFunds() external onlyOwnerOrAdmin nonReentrant { + require(state == RaffleState.DrawExecuted, "Draw not executed"); + require(!fundsDistributed, "Funds already distributed"); + require(winner != address(0), "No winner selected"); + require(projectAddress != address(0), "Invalid project address"); + + fundsDistributed = true; + + uint256 totalBalance = address(this).balance; + + // Calcular distribución (Base 10000) + // Fee de plataforma fijo: 0.05% + uint256 platformAmount = (totalBalance * PLATFORM_FEE) / BASIS_POINTS; + + // El resto del pozo se divide entre proyecto y ganador + uint256 distributablePool = totalBalance - platformAmount; + + // Porcentaje para el proyecto sobre el pozo restante + uint256 projectAmount = (distributablePool * projectPercentage) / BASIS_POINTS; + + // El resto va al ganador + uint256 winnerAmount = distributablePool - projectAmount; + + // Registrar pagos pendientes (patrón pull payment - más seguro) + _asyncTransfer(projectAddress, projectAmount); + _asyncTransfer(platformAdmin, platformAmount); // Paga al admin de la plataforma + _asyncTransfer(winner, winnerAmount); + + emit FundsDistributed( + projectAddress, + platformAdmin, + winner, + projectAmount, + platformAmount, + winnerAmount + ); + } + + /** + * @notice Selecciona el ganador usando Binary Search - O(log n) + * @param entropySeed Entropía generada por Pyth + * @return Dirección del ganador + */ + function _selectWinner(bytes32 entropySeed) internal view returns (address) { + require(participants.length > 0, "No participants"); + + // Usar entropía de Pyth para seleccionar ticket ganador + // El randomTicket está entre 0 y totalTickets - 1 + uint256 randomTicket = uint256(entropySeed) % totalTickets; + + // Binary search en array de participants (buscando upperBound) - O(log n) + uint256 left = 0; + uint256 right = participants.length - 1; + + while (left < right) { + uint256 mid = (left + right) / 2; + + // Si randomTicket es menor que el límite superior de este rango, + // el ganador podría ser este o uno anterior. + if (participants[mid].upperBound > randomTicket) { + right = mid; + } else { + // Si randomTicket es >= upperBound, el ganador está después + left = mid + 1; + } + } + + return participants[left].owner; + } + + /** + * @notice Obtiene el array de participantes + * @return Array de direcciones de participantes + */ + function getParticipantsCount() external view returns (uint256) { + return participants.length; + } + + function getTicketRange(uint256 index) external view returns (address owner, uint256 upperBound) { + TicketRange memory range = participants[index]; + return (range.owner, range.upperBound); + } + + /** + * @notice Obtiene el balance total del contrato + * @return Balance en wei + */ + function getTotalBalance() external view returns (uint256) { + return address(this).balance; + } + + /** + * @notice Verifica si la rifa está activa para comprar tickets + * @return true si está activa y dentro del tiempo + */ + function isActive() external view returns (bool) { + return state == RaffleState.Active && + block.timestamp < raffleStartTime + raffleDuration; + } + + /** + * @notice Obtiene el tiempo restante de la rifa + * @return Segundos restantes, 0 si ya terminó + */ + function getTimeRemaining() external view returns (uint256) { + uint256 endTime = raffleStartTime + raffleDuration; + if (block.timestamp >= endTime) { + return 0; + } + return endTime - block.timestamp; + } + + /** + * @notice Obtiene información del ganador potencial sin ejecutar el sorteo + * @param entropySeed Entropía de prueba + * @return Dirección del potencial ganador + */ + function previewWinner(bytes32 entropySeed) external view returns (address) { + require(participants.length > 0, "No participants"); + return _selectWinner(entropySeed); + } + + /** + * @notice Función de emergencia para forzar la selección del ganador (solo owner/admin) + * @dev Permite al owner seleccionar el ganador sin esperar a Pyth (para testing/emergencias) + * @param randomNumber Número aleatorio a usar para la selección + */ + function forceSelectWinner(bytes32 randomNumber) external onlyOwnerOrAdmin { + require(state == RaffleState.Active || state == RaffleState.EntropyRequested, "Invalid state"); + require(participants.length > 0, "No participants"); + require(totalTickets > 0, "No tickets sold"); + + // Seleccionar ganador + winner = _selectWinner(randomNumber); + state = RaffleState.DrawExecuted; + + emit DrawExecuted(winner, uint256(randomNumber) % totalTickets); + } +} + diff --git a/entropy/raffle-for-good/contract/contracts/RaffleFactory.sol b/entropy/raffle-for-good/contract/contracts/RaffleFactory.sol new file mode 100644 index 00000000..42ff0d7d --- /dev/null +++ b/entropy/raffle-for-good/contract/contracts/RaffleFactory.sol @@ -0,0 +1,170 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./ProjectRaffle.sol"; + +/** + * @title RaffleFactory + * @notice Factory contract para crear múltiples rifas de proyectos + * @dev Solo el owner puede crear nuevas rifas + */ +contract RaffleFactory is Ownable { + // Array de todas las rifas creadas + ProjectRaffle[] public raffles; + + + // Mapping para verificar si una dirección es una rifa creada por este factory + mapping(address => bool) public isRaffle; + + // Configuración de Pyth Entropy + address public entropyAddress; + + // Eventos + event RaffleCreated( + address indexed raffleAddress, + string projectName, + uint256 projectPercentage, + address indexed creator + ); + event EntropyConfigUpdated(address entropyAddress); + + /** + * @notice Constructor del factory + * @param _entropyAddress Dirección del contrato de Pyth Entropy + * @param _initialOwner Dirección del owner inicial + */ + constructor( + address _entropyAddress, + address _initialOwner + ) { + require(_entropyAddress != address(0), "Invalid Entropy address"); + require(_initialOwner != address(0), "Invalid owner address"); + entropyAddress = _entropyAddress; + _transferOwnership(_initialOwner); + } + + /** + * @notice Crea una nueva rifa + * @param name Nombre del proyecto + * @param description Descripción del proyecto + * @param projectPercentage Porcentaje para el proyecto (Basis Points: 5000 = 50%) + * @param projectAddress Dirección del proyecto que recibirá fondos + * @param raffleDuration Duración de la rifa en segundos + * @return Dirección del contrato de rifa creado + */ + function createRaffle( + string memory name, + string memory description, + uint256 projectPercentage, + address projectAddress, + uint256 raffleDuration + ) external returns (address) { + require(bytes(name).length > 0, "Name cannot be empty"); + require(projectPercentage <= 10000, "Project percentage cannot exceed 100%"); + require(projectPercentage > 0, "Project percentage must be > 0"); + require(projectAddress != address(0), "Invalid project address"); + require(raffleDuration > 0, "Duration must be > 0"); + + // Crear nueva instancia de ProjectRaffle + ProjectRaffle raffle = new ProjectRaffle( + name, + description, + projectPercentage, + entropyAddress, + msg.sender, // El creador de la rifa es el owner + owner(), // El admin de la plataforma es el owner del factory + projectAddress, + raffleDuration + ); + + // Registrar la rifa + raffles.push(raffle); + isRaffle[address(raffle)] = true; + + emit RaffleCreated( + address(raffle), + name, + projectPercentage, + msg.sender + ); + + return address(raffle); + } + + /** + * @notice Actualiza la configuración de Entropy + * @param _entropyAddress Nueva dirección del contrato de Entropy + */ + function updateEntropyConfig( + address _entropyAddress + ) external onlyOwner { + require(_entropyAddress != address(0), "Invalid Entropy address"); + entropyAddress = _entropyAddress; + + emit EntropyConfigUpdated(_entropyAddress); + } + + /** + * @notice Obtiene el número total de rifas creadas + * @return Cantidad de rifas + */ + function getRaffleCount() external view returns (uint256) { + return raffles.length; + } + + /** + * @notice Obtiene todas las direcciones de las rifas creadas + * @return Array de direcciones de rifas + */ + function getAllRaffles() external view returns (address[] memory) { + address[] memory result = new address[](raffles.length); + for (uint256 i = 0; i < raffles.length; i++) { + result[i] = address(raffles[i]); + } + return result; + } + + /** + * @notice Obtiene información de una rifa específica + * @param index Índice de la rifa + * @return raffleAddress Dirección del contrato de rifa + * @return projectName Nombre del proyecto + * @return state Estado actual de la rifa + * @return totalTickets Total de tickets vendidos + * @return participantCount Número de participantes + */ + function getRaffleInfo(uint256 index) external view returns ( + address raffleAddress, + string memory projectName, + ProjectRaffle.RaffleState state, + uint256 totalTickets, + uint256 participantCount + ) { + require(index < raffles.length, "Invalid index"); + + ProjectRaffle raffle = raffles[index]; + raffleAddress = address(raffle); + projectName = raffle.projectName(); + state = raffle.state(); + totalTickets = raffle.totalTickets(); + participantCount = raffle.getParticipantsCount(); + } + + /** + * @notice Obtiene las últimas N rifas creadas + * @param count Número de rifas a obtener + * @return Array de direcciones de las últimas rifas + */ + function getLatestRaffles(uint256 count) external view returns (address[] memory) { + uint256 actualCount = count > raffles.length ? raffles.length : count; + address[] memory result = new address[](actualCount); + + for (uint256 i = 0; i < actualCount; i++) { + result[i] = address(raffles[raffles.length - 1 - i]); + } + + return result; + } +} + diff --git a/entropy/raffle-for-good/contract/contracts/interfaces/IEntropyConsumer.sol b/entropy/raffle-for-good/contract/contracts/interfaces/IEntropyConsumer.sol new file mode 100644 index 00000000..1e3229bf --- /dev/null +++ b/entropy/raffle-for-good/contract/contracts/interfaces/IEntropyConsumer.sol @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @title IEntropyConsumer + * @notice Minimal interface for contracts that consume Pyth Entropy randomness. + * @dev Only the functions required by the project have been declared. + */ +interface IEntropyConsumer { + /** + * @notice Callback invoked by the Entropy contract when randomness is ready. + */ + function entropyCallback( + uint64 sequenceNumber, + address provider, + bytes32 randomNumber + ) external; + + /** + * @notice Returns the address of the Entropy contract being used. + */ + function getEntropy() external view returns (address); +} + diff --git a/entropy/raffle-for-good/contract/contracts/interfaces/IEntropyV2.sol b/entropy/raffle-for-good/contract/contracts/interfaces/IEntropyV2.sol new file mode 100644 index 00000000..f75db350 --- /dev/null +++ b/entropy/raffle-for-good/contract/contracts/interfaces/IEntropyV2.sol @@ -0,0 +1,32 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +/** + * @title IEntropyV2 + * @notice Minimal subset of the Pyth Entropy V2 interface used by ProjectRaffle. + */ +interface IEntropyV2 { + /** + * @notice Returns the default randomness provider configured in the Entropy contract. + */ + function getDefaultProvider() external view returns (address); + + /** + * @notice Returns the fee required to request randomness from a provider. + */ + function getFee(address provider) external view returns (uint256); + + /** + * @notice Requests randomness from the Entropy contract. + * @param provider Randomness provider address. + * @param userRandomNumber User provided commitment (bytes32). + * @param useBlockhash Flag to mix in the blockhash. + * @return sequenceNumber Unique sequence identifier for the request. + */ + function request( + address provider, + bytes32 userRandomNumber, + bool useBlockhash + ) external payable returns (uint64 sequenceNumber); +} + diff --git a/entropy/raffle-for-good/contract/hardhat.config.ts b/entropy/raffle-for-good/contract/hardhat.config.ts new file mode 100644 index 00000000..6415e760 --- /dev/null +++ b/entropy/raffle-for-good/contract/hardhat.config.ts @@ -0,0 +1,40 @@ +import { defineConfig } from "hardhat/config"; +import hardhatIgnition from "@nomicfoundation/hardhat-ignition"; +import hardhatEthers from "@nomicfoundation/hardhat-ethers"; +import hardhatVerify from "@nomicfoundation/hardhat-verify"; +import * as dotenv from "dotenv"; + +dotenv.config(); + +export default defineConfig({ + solidity: { + version: "0.8.28", + settings: { + optimizer: { + enabled: true, + runs: 200, + }, + viaIR: true, + }, + }, + networks: { + // Base Sepolia Testnet + baseSepolia: { + type: "http" as const, + url: process.env.BASE_SEPOLIA_RPC_URL || "https://sepolia.base.org", + chainId: 84532, + accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], + }, + // Hardhat Network local (para desarrollo) + hardhat: { + type: "edr-simulated" as const, + chainId: 1337, + }, + }, + etherscan: { + apiKey: { + baseSepolia: process.env.BASESCAN_API_KEY || "", + }, + }, + plugins: [hardhatIgnition, hardhatEthers, hardhatVerify], +}); diff --git a/entropy/raffle-for-good/contract/package.json b/entropy/raffle-for-good/contract/package.json new file mode 100644 index 00000000..fd10dc36 --- /dev/null +++ b/entropy/raffle-for-good/contract/package.json @@ -0,0 +1,44 @@ +{ + "name": "eth-global-hackathon", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "compile": "hardhat compile --show-stack-traces", + "test": "hardhat test", + "deploy:baseSepolia": "hardhat ignition deploy ignition/modules/RaffleFactoryModule.ts --network baseSepolia", + "deploy:local": "hardhat ignition deploy ignition/modules/RaffleFactoryModule.ts --network hardhat --show-stack-traces", + "interact": "hardhat run scripts/interact.ts", + "node": "hardhat node" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/NicoCaz/eth-global-hackathon.git" + }, + "keywords": [], + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/NicoCaz/eth-global-hackathon/issues" + }, + "homepage": "https://github.com/NicoCaz/eth-global-hackathon#readme", + "dependencies": { + "@openzeppelin/contracts": "^4.9.6", + "@pythnetwork/entropy-sdk-solidity": "^2.2.0", + "hardhat": "^3.0.15" + }, + "type": "module", + "devDependencies": { + "@nomicfoundation/hardhat-ethers": "^4.0.3", + "@nomicfoundation/hardhat-ignition": "^3.0.5", + "@nomicfoundation/hardhat-network-helpers": "^3.0.3", + "@nomicfoundation/hardhat-verify": "^3.0.7", + "@types/chai": "^5.2.3", + "@types/mocha": "^10.0.10", + "@types/node": "^22.19.1", + "chai": "^6.2.1", + "dotenv": "^17.2.3", + "ethers": "^6.15.0", + "typescript": "~5.8.0" + } +} diff --git a/entropy/raffle-for-good/contract/scripts/buyTickets.ts b/entropy/raffle-for-good/contract/scripts/buyTickets.ts new file mode 100644 index 00000000..d9849c8c --- /dev/null +++ b/entropy/raffle-for-good/contract/scripts/buyTickets.ts @@ -0,0 +1,49 @@ +import { JsonRpcProvider, Wallet, Contract, parseEther } from "ethers"; +import * as dotenv from "dotenv"; +import ProjectRaffleArtifact from "../artifacts/contracts/ProjectRaffle.sol/ProjectRaffle.json" assert { type: "json" }; + +const RAFFLE_ADDRESS = "0x77F9eBe8872D6844C4c1f404dE40E274AB76708d"; +const AMOUNT_ETH = "0.001"; + +dotenv.config(); + +async function main() { + const rpcUrl = + process.env.BASE_SEPOLIA_RPC_URL ?? + process.env.SEPOLIA_RPC_URL ?? + process.env.RPC_URL; + if (!rpcUrl) { + throw new Error("Missing BASE_SEPOLIA_RPC_URL/SEPOLIA_RPC_URL/RPC_URL"); + } + + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error("Missing PRIVATE_KEY in environment"); + } + + const provider = new JsonRpcProvider(rpcUrl); + const wallet = new Wallet(privateKey, provider); + + console.log( + `Buying tickets on ${RAFFLE_ADDRESS} from ${wallet.address} with ${AMOUNT_ETH} ETH` + ); + + const raffle = new Contract( + RAFFLE_ADDRESS, + ProjectRaffleArtifact.abi, + wallet + ); + + const tx = await raffle.buyTickets({ + value: parseEther(AMOUNT_ETH), + }); + console.log(`Submitted tx: ${tx.hash}`); + const receipt = await tx.wait(); + console.log(`Confirmed in block ${receipt?.blockNumber}`); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); + diff --git a/entropy/raffle-for-good/contract/scripts/closeRaffle.ts b/entropy/raffle-for-good/contract/scripts/closeRaffle.ts new file mode 100644 index 00000000..ebf66a99 --- /dev/null +++ b/entropy/raffle-for-good/contract/scripts/closeRaffle.ts @@ -0,0 +1,147 @@ +import { JsonRpcProvider, Wallet, Contract, randomBytes, hexlify } from "ethers"; +import * as dotenv from "dotenv"; +import ProjectRaffleArtifact from "../artifacts/contracts/ProjectRaffle.sol/ProjectRaffle.json" assert { type: "json" }; + +// Dirección de la rifa que queremos cerrar +const RAFFLE_ADDRESS = "0x3374974DDE6eA5faAa5165cB21784279943C81a2"; + +dotenv.config(); + +// ABI mínima de IEntropyV2 para llamar a getFee +const ENTROPY_ABI = [ + "function getFee(address provider) external view returns (uint256)", + "function getDefaultProvider() external view returns (address)" +]; + +async function main() { + // 1. Setup de conexión + const rpcUrl = + process.env.BASE_SEPOLIA_RPC_URL ?? + process.env.SEPOLIA_RPC_URL ?? + process.env.RPC_URL; + if (!rpcUrl) { + throw new Error("Missing BASE_SEPOLIA_RPC_URL/SEPOLIA_RPC_URL/RPC_URL"); + } + + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error("Missing PRIVATE_KEY in environment"); + } + + const provider = new JsonRpcProvider(rpcUrl); + const wallet = new Wallet(privateKey, provider); + + console.log(`🎯 Cerrando rifa en: ${RAFFLE_ADDRESS}`); + console.log(`👤 Desde cuenta: ${wallet.address}`); + console.log(""); + + // 2. Conectar al contrato de la rifa + const raffle = new Contract( + RAFFLE_ADDRESS, + ProjectRaffleArtifact.abi, + wallet + ); + + // 3. Verificar estado de la rifa + const state = await raffle.state(); + const timeRemaining = await raffle.getTimeRemaining(); + const totalTickets = await raffle.totalTickets(); + const participantsCount = await raffle.getParticipantsCount(); + + console.log("📊 Estado actual de la rifa:"); + console.log(` Estado: ${state === 0n ? "Active" : state === 1n ? "EntropyRequested" : "DrawExecuted"}`); + console.log(` Tiempo restante: ${timeRemaining} segundos`); + console.log(` Total tickets vendidos: ${totalTickets.toString()} wei`); + console.log(` Total participantes: ${participantsCount.toString()}`); + console.log(""); + + // 4. Validaciones + if (state !== 0n) { + throw new Error(`❌ La rifa no está en estado Active (estado actual: ${state})`); + } + + // Comentado para permitir cierre anticipado + // if (timeRemaining > 0n) { + // throw new Error(`❌ La rifa aún está activa. Espera ${timeRemaining} segundos`); + // } + + if (totalTickets === 0n) { + throw new Error("❌ No hay tickets vendidos"); + } + + if (participantsCount === 0n) { + throw new Error("❌ No hay participantes"); + } + + // 5. Obtener información de Entropy + const entropyAddress = await raffle.entropy(); + const entropyProvider = await raffle.entropyProvider(); + + console.log("🔮 Información de Pyth Entropy:"); + console.log(` Entropy Contract: ${entropyAddress}`); + console.log(` Entropy Provider: ${entropyProvider}`); + console.log(""); + + // 6. Obtener el fee necesario + const entropyContract = new Contract( + entropyAddress, + ENTROPY_ABI, + provider + ); + + const fee = await entropyContract.getFee(entropyProvider); + console.log(`💰 Fee de Pyth Entropy: ${fee.toString()} wei (${Number(fee) / 1e18} ETH)`); + console.log(""); + + // 7. Generar número aleatorio del usuario + const userRandomNumber = hexlify(randomBytes(32)); + console.log(`🎲 Número aleatorio generado: ${userRandomNumber}`); + console.log(""); + + // 8. Solicitar entropía (cerrar la rifa) + console.log("🚀 Enviando transacción para cerrar la rifa..."); + + const tx = await raffle.requestEntropy(userRandomNumber, { + value: fee, + }); + + console.log(`📝 Transacción enviada: ${tx.hash}`); + console.log("⏳ Esperando confirmación..."); + + const receipt = await tx.wait(); + console.log(`✅ Confirmada en el bloque ${receipt?.blockNumber}`); + console.log(""); + + // 9. Buscar el evento EntropyRequested + const entropyRequestedEvent = receipt?.logs + .map((log: any) => { + try { + return raffle.interface.parseLog({ + topics: log.topics as string[], + data: log.data, + }); + } catch { + return null; + } + }) + .find((event: any) => event?.name === "EntropyRequested"); + + if (entropyRequestedEvent) { + console.log("🎉 ¡Rifa cerrada exitosamente!"); + console.log(` Sequence Number: ${entropyRequestedEvent.args.sequenceNumber}`); + console.log(""); + console.log("⏳ Ahora espera a que Pyth Entropy responda automáticamente..."); + console.log(" Esto puede tomar algunos segundos o minutos."); + console.log(""); + console.log("📋 Próximos pasos:"); + console.log(" 1. Pyth llamará automáticamente a entropyCallback()"); + console.log(" 2. Se seleccionará el ganador"); + console.log(" 3. Luego deberás llamar a distributeFunds()"); + } +} + +main().catch((error) => { + console.error("❌ Error:", error.message); + process.exitCode = 1; +}); + diff --git a/entropy/raffle-for-good/contract/scripts/createNewRaffle.ts b/entropy/raffle-for-good/contract/scripts/createNewRaffle.ts new file mode 100644 index 00000000..4ed35263 --- /dev/null +++ b/entropy/raffle-for-good/contract/scripts/createNewRaffle.ts @@ -0,0 +1,116 @@ +import { JsonRpcProvider, Wallet, Contract } from "ethers"; +import * as dotenv from "dotenv"; +import RaffleFactoryArtifact from "../artifacts/contracts/RaffleFactory.sol/RaffleFactory.json" assert { type: "json" }; + +// Factory address desplegado +const FACTORY_ADDRESS = "0x104032d5377be9b78441551e169f3C8a3d520672"; + +// Parámetros de la nueva rifa +const RAFFLE_NAME = "Test Raffle Quick Close"; +const RAFFLE_DESCRIPTION = "Rifa de prueba que se puede cerrar inmediatamente"; +const PROJECT_PERCENTAGE = 3000; // 30% para el proyecto +const PROJECT_ADDRESS = "0x611a9571F763952605cA631d3B0F346a568ab3e1"; // Tu dirección +const RAFFLE_DURATION = 7 * 24 * 60 * 60; // 7 días (aunque ya no importa con la modificación) + +dotenv.config(); + +async function main() { + // 1. Setup de conexión + const rpcUrl = + process.env.BASE_SEPOLIA_RPC_URL ?? + process.env.SEPOLIA_RPC_URL ?? + process.env.RPC_URL; + if (!rpcUrl) { + throw new Error("Missing BASE_SEPOLIA_RPC_URL/SEPOLIA_RPC_URL/RPC_URL"); + } + + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error("Missing PRIVATE_KEY in environment"); + } + + const provider = new JsonRpcProvider(rpcUrl); + const wallet = new Wallet(privateKey, provider); + + console.log(`🏭 Creando nueva rifa con Factory: ${FACTORY_ADDRESS}`); + console.log(`👤 Desde cuenta: ${wallet.address}`); + console.log(""); + + // 2. Conectar al Factory + const factory = new Contract( + FACTORY_ADDRESS, + RaffleFactoryArtifact.abi, + wallet + ); + + console.log("📋 Parámetros de la rifa:"); + console.log(` Nombre: ${RAFFLE_NAME}`); + console.log(` Descripción: ${RAFFLE_DESCRIPTION}`); + console.log(` % Proyecto: ${PROJECT_PERCENTAGE / 100}%`); + console.log(` Dirección Proyecto: ${PROJECT_ADDRESS}`); + console.log(` Duración: ${RAFFLE_DURATION} segundos`); + console.log(""); + + // 3. Crear la rifa + console.log("🚀 Creando rifa..."); + + const tx = await factory.createRaffle( + RAFFLE_NAME, + RAFFLE_DESCRIPTION, + PROJECT_PERCENTAGE, + PROJECT_ADDRESS, + RAFFLE_DURATION + ); + + console.log(`📝 Transacción enviada: ${tx.hash}`); + console.log("⏳ Esperando confirmación..."); + + const receipt = await tx.wait(); + console.log(`✅ Confirmada en el bloque ${receipt?.blockNumber}`); + console.log(""); + + // 4. Buscar el evento RaffleCreated + const raffleCreatedEvent = receipt?.logs + .map((log: any) => { + try { + return factory.interface.parseLog({ + topics: log.topics as string[], + data: log.data, + }); + } catch { + return null; + } + }) + .find((event: any) => event?.name === "RaffleCreated"); + + if (raffleCreatedEvent) { + const newRaffleAddress = raffleCreatedEvent.args.raffleAddress; + console.log("🎉 ¡Rifa creada exitosamente!"); + console.log(""); + console.log("📍 NUEVA DIRECCIÓN DE LA RIFA:"); + console.log(` ${newRaffleAddress}`); + console.log(""); + console.log("📋 Próximos pasos:"); + console.log(` 1. Comprar tickets: modifica buyTickets.ts con la dirección ${newRaffleAddress}`); + console.log(` 2. Cerrar rifa: modifica closeRaffle.ts con la dirección ${newRaffleAddress}`); + console.log(` 3. Distribuir fondos: modifica distributeFunds.ts con la dirección ${newRaffleAddress}`); + } else { + console.log("⚠️ No se encontró el evento RaffleCreated"); + + // Obtener el último raffle creado + const raffleCount = await factory.getRaffleCount(); + console.log(`📊 Total de raffles: ${raffleCount}`); + + if (raffleCount > 0n) { + const lastRaffleIndex = raffleCount - 1n; + const raffleInfo = await factory.getRaffleInfo(lastRaffleIndex); + console.log(`📍 Última rifa creada: ${raffleInfo[0]}`); + } + } +} + +main().catch((error) => { + console.error("❌ Error:", error.message); + process.exitCode = 1; +}); + diff --git a/entropy/raffle-for-good/contract/scripts/distributeFunds.ts b/entropy/raffle-for-good/contract/scripts/distributeFunds.ts new file mode 100644 index 00000000..977c787e --- /dev/null +++ b/entropy/raffle-for-good/contract/scripts/distributeFunds.ts @@ -0,0 +1,133 @@ +import { JsonRpcProvider, Wallet, Contract } from "ethers"; +import * as dotenv from "dotenv"; +import ProjectRaffleArtifact from "../artifacts/contracts/ProjectRaffle.sol/ProjectRaffle.json" assert { type: "json" }; + +// Dirección de la rifa donde distribuir fondos +const RAFFLE_ADDRESS = "0xAed632c4bF95AbA7550B6Dfb2E0E4072A3fB34e0"; + +dotenv.config(); + +async function main() { + // 1. Setup de conexión + const rpcUrl = + process.env.BASE_SEPOLIA_RPC_URL ?? + process.env.SEPOLIA_RPC_URL ?? + process.env.RPC_URL; + if (!rpcUrl) { + throw new Error("Missing BASE_SEPOLIA_RPC_URL/SEPOLIA_RPC_URL/RPC_URL"); + } + + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error("Missing PRIVATE_KEY in environment"); + } + + const provider = new JsonRpcProvider(rpcUrl); + const wallet = new Wallet(privateKey, provider); + + console.log(`💰 Distribuyendo fondos en: ${RAFFLE_ADDRESS}`); + console.log(`👤 Desde cuenta: ${wallet.address}`); + console.log(""); + + // 2. Conectar al contrato de la rifa + const raffle = new Contract( + RAFFLE_ADDRESS, + ProjectRaffleArtifact.abi, + wallet + ); + + // 3. Verificar estado de la rifa + const state = await raffle.state(); + const fundsDistributed = await raffle.fundsDistributed(); + const winner = await raffle.winner(); + const totalBalance = await raffle.getTotalBalance(); + + console.log("📊 Estado actual de la rifa:"); + console.log(` Estado: ${state === 0n ? "Active" : state === 1n ? "EntropyRequested" : "DrawExecuted"}`); + console.log(` Fondos distribuidos: ${fundsDistributed}`); + console.log(` Ganador: ${winner}`); + console.log(` Balance total: ${totalBalance.toString()} wei (${Number(totalBalance) / 1e18} ETH)`); + console.log(""); + + // 4. Validaciones + if (state !== 2n) { + throw new Error(`❌ El sorteo no ha sido ejecutado (estado: ${state})`); + } + + if (fundsDistributed) { + throw new Error("❌ Los fondos ya han sido distribuidos"); + } + + if (winner === "0x0000000000000000000000000000000000000000") { + throw new Error("❌ No hay ganador seleccionado"); + } + + // 5. Obtener información de distribución + const projectAddress = await raffle.projectAddress(); + const platformAdmin = await raffle.platformAdmin(); + const projectPercentage = await raffle.projectPercentage(); + const platformFee = await raffle.PLATFORM_FEE(); + const basisPoints = await raffle.BASIS_POINTS(); + + console.log("📋 Información de distribución:"); + console.log(` Proyecto: ${projectAddress}`); + console.log(` Administrador: ${platformAdmin}`); + console.log(` Ganador: ${winner}`); + console.log(` % Proyecto: ${(Number(projectPercentage) / Number(basisPoints)) * 100}%`); + console.log(` % Plataforma: ${(Number(platformFee) / Number(basisPoints)) * 100}%`); + console.log(""); + + // Calcular montos (igual que en el contrato) + const platformAmount = (totalBalance * platformFee) / basisPoints; + const distributablePool = totalBalance - platformAmount; + const projectAmount = (distributablePool * projectPercentage) / basisPoints; + const winnerAmount = distributablePool - projectAmount; + + console.log("💵 Montos a distribuir:"); + console.log(` Proyecto: ${projectAmount.toString()} wei (${Number(projectAmount) / 1e18} ETH)`); + console.log(` Plataforma: ${platformAmount.toString()} wei (${Number(platformAmount) / 1e18} ETH)`); + console.log(` Ganador: ${winnerAmount.toString()} wei (${Number(winnerAmount) / 1e18} ETH)`); + console.log(""); + + // 6. Distribuir fondos + console.log("🚀 Enviando transacción para distribuir fondos..."); + + const tx = await raffle.distributeFunds(); + + console.log(`📝 Transacción enviada: ${tx.hash}`); + console.log("⏳ Esperando confirmación..."); + + const receipt = await tx.wait(); + console.log(`✅ Confirmada en el bloque ${receipt?.blockNumber}`); + console.log(""); + + // 7. Buscar el evento FundsDistributed + const fundsDistributedEvent = receipt?.logs + .map((log: any) => { + try { + return raffle.interface.parseLog({ + topics: log.topics as string[], + data: log.data, + }); + } catch { + return null; + } + }) + .find((event: any) => event?.name === "FundsDistributed"); + + if (fundsDistributedEvent) { + console.log("🎉 ¡Fondos distribuidos exitosamente!"); + console.log(""); + console.log("📋 Próximo paso:"); + console.log(" Los beneficiarios deben llamar a withdrawPayments() para retirar sus fondos:"); + console.log(` - Proyecto (${projectAddress})`); + console.log(` - Plataforma (${platformAdmin})`); + console.log(` - Ganador (${winner})`); + } +} + +main().catch((error) => { + console.error("❌ Error:", error.message); + process.exitCode = 1; +}); + diff --git a/entropy/raffle-for-good/contract/scripts/listRaffles.ts b/entropy/raffle-for-good/contract/scripts/listRaffles.ts new file mode 100644 index 00000000..182929d6 --- /dev/null +++ b/entropy/raffle-for-good/contract/scripts/listRaffles.ts @@ -0,0 +1,90 @@ +import { JsonRpcProvider, Wallet, Contract } from "ethers"; +import * as dotenv from "dotenv"; +import RaffleFactoryArtifact from "../artifacts/contracts/RaffleFactory.sol/RaffleFactory.json" assert { type: "json" }; + +// Factory address actualizado +const FACTORY_ADDRESS = "0x104032d5377be9b78441551e169f3C8a3d520672"; + +dotenv.config(); + +async function main() { + // 1. Setup de conexión + const rpcUrl = + process.env.BASE_SEPOLIA_RPC_URL ?? + process.env.SEPOLIA_RPC_URL ?? + process.env.RPC_URL; + if (!rpcUrl) { + throw new Error("Missing BASE_SEPOLIA_RPC_URL/SEPOLIA_RPC_URL/RPC_URL"); + } + + const privateKey = process.env.PRIVATE_KEY; + if (!privateKey) { + throw new Error("Missing PRIVATE_KEY in environment"); + } + + const provider = new JsonRpcProvider(rpcUrl); + const wallet = new Wallet(privateKey, provider); + + console.log(`🏭 Factory Address: ${FACTORY_ADDRESS}`); + console.log(`👤 Tu cuenta: ${wallet.address}`); + console.log(""); + + // 2. Conectar al Factory + const factory = new Contract( + FACTORY_ADDRESS, + RaffleFactoryArtifact.abi, + provider + ); + + // 3. Obtener información del factory + const raffleCount = await factory.getRaffleCount(); + const owner = await factory.owner(); + const entropyAddress = await factory.entropyAddress(); + + console.log("📊 Información del Factory:"); + console.log(` Owner: ${owner}`); + console.log(` Entropy Address: ${entropyAddress}`); + console.log(` Total Rifas: ${raffleCount.toString()}`); + console.log(""); + + if (raffleCount === 0n) { + console.log("⚠️ No hay rifas creadas aún"); + return; + } + + // 4. Listar todas las rifas + console.log("📋 Lista de Rifas:"); + console.log("━".repeat(80)); + + for (let i = 0; i < Number(raffleCount); i++) { + const raffleInfo = await factory.getRaffleInfo(i); + const raffleAddress = raffleInfo[0]; + const projectName = raffleInfo[1]; + const state = raffleInfo[2]; + const totalTickets = raffleInfo[3]; + const participantCount = raffleInfo[4]; + + const stateText = state === 0n ? "🟢 Active" : state === 1n ? "🟡 EntropyRequested" : "🔴 DrawExecuted"; + + console.log(""); + console.log(`#${i} - ${projectName}`); + console.log(` 📍 Dirección: ${raffleAddress}`); + console.log(` ${stateText}`); + console.log(` 🎫 Tickets vendidos: ${totalTickets.toString()} wei (${Number(totalTickets) / 1e18} ETH)`); + console.log(` 👥 Participantes: ${participantCount.toString()}`); + } + + console.log(""); + console.log("━".repeat(80)); + console.log(""); + console.log("💡 Comandos útiles:"); + console.log(" Ver detalles: modifica showRaffle.ts con la dirección que desees"); + console.log(" Comprar tickets: modifica buyTickets.ts con la dirección"); + console.log(" Cerrar rifa: modifica closeRaffle.ts con la dirección"); +} + +main().catch((error) => { + console.error("❌ Error:", error.message); + process.exitCode = 1; +}); + diff --git a/entropy/raffle-for-good/contract/tsconfig.json b/entropy/raffle-for-good/contract/tsconfig.json new file mode 100644 index 00000000..9b1380cc --- /dev/null +++ b/entropy/raffle-for-good/contract/tsconfig.json @@ -0,0 +1,13 @@ +/* Based on https://github.com/tsconfig/bases/blob/501da2bcd640cf95c95805783e1012b992338f28/bases/node22.json */ +{ + "compilerOptions": { + "lib": ["es2023"], + "module": "node16", + "target": "es2022", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "moduleResolution": "node16", + "outDir": "dist" + } +}