From 30ea378eaf782284834054f27f8f35afbd5dc884 Mon Sep 17 00:00:00 2001 From: bill-makes Date: Sun, 23 Nov 2025 08:57:37 -0300 Subject: [PATCH] Add Hyperlinkgrid Entropy example --- entropy/hyperlinkgrid/README.md | 103 +++++++++ .../contracts/HyperlinkgridEntropy.sol | 196 ++++++++++++++++++ entropy/hyperlinkgrid/contracts/MockUSDC.sol | 16 ++ .../contracts/mocks/EntropyMock.sol | 121 +++++++++++ entropy/hyperlinkgrid/hardhat.config.ts | 20 ++ entropy/hyperlinkgrid/package.json | 20 ++ entropy/hyperlinkgrid/scripts/deploy.ts | 44 ++++ .../test/HyperlinkgridEntropy.test.ts | 95 +++++++++ entropy/hyperlinkgrid/tsconfig.json | 13 ++ 9 files changed, 628 insertions(+) create mode 100644 entropy/hyperlinkgrid/README.md create mode 100644 entropy/hyperlinkgrid/contracts/HyperlinkgridEntropy.sol create mode 100644 entropy/hyperlinkgrid/contracts/MockUSDC.sol create mode 100644 entropy/hyperlinkgrid/contracts/mocks/EntropyMock.sol create mode 100644 entropy/hyperlinkgrid/hardhat.config.ts create mode 100644 entropy/hyperlinkgrid/package.json create mode 100644 entropy/hyperlinkgrid/scripts/deploy.ts create mode 100644 entropy/hyperlinkgrid/test/HyperlinkgridEntropy.test.ts create mode 100644 entropy/hyperlinkgrid/tsconfig.json diff --git a/entropy/hyperlinkgrid/README.md b/entropy/hyperlinkgrid/README.md new file mode 100644 index 00000000..36b05b4d --- /dev/null +++ b/entropy/hyperlinkgrid/README.md @@ -0,0 +1,103 @@ +# Hyperlinkgrid x Pyth Entropy Example + +This project demonstrates how to use **Pyth Entropy** to generate secure, verifiable on-chain randomness in a Solidity smart contract. + +The example is based on **Hyperlinkgrid**, a decentralized grid where users purchase tiles. Once the grid is full (sold out), Pyth Entropy is used to fairly select beneficiaries who split the accumulated pot. + +## Features + +- **ERC-721 NFT Grid**: Users buy tiles (NFTs) with USDC. +- **Pyth Entropy Integration**: Secure randomness generation for selecting winners. +- **Automated Payouts**: Winners are automatically paid out in USDC. + +## Prerequisites + +- Node.js (v18 or later) +- npm or pnpm + +## Installation + +1. Clone this repository and navigate to the directory. +2. Install dependencies: + +```bash +npm install +``` + +## Project Structure + +- `contracts/HyperlinkgridEntropy.sol`: The main contract integrating Pyth Entropy. +- `contracts/MockUSDC.sol`: A simple ERC20 token for testing payments. +- `contracts/mocks/EntropyMock.sol`: A mock of the Pyth Entropy contract for local testing. +- `test/HyperlinkgridEntropy.test.ts`: Hardhat tests demonstrating the full flow. + +## How it Works + +### 1. Purchase Phase +Users purchase tiles on the grid by paying 100 USDC. The USDC is held in the contract. + +### 2. Triggering Randomness +Once `MAX_SUPPLY` (10 in this example) is reached, any user can call `triggerEndGame`. +This function: +- Requests a random number from the Pyth Entropy contract. +- Pays the required fee (in native ETH/GAS token). +- Stores the sequence number. + +```solidity +uint64 seq = entropy.requestWithCallback{value: fee}( + entropyProvider, + userRandomNumber +); +``` + +### 3. Entropy Callback +Pyth's off-chain provider generates a random number and submits it back to the Entropy contract, which verifies it and calls `entropyCallback` on our contract. + +```solidity +function entropyCallback( + uint64 sequenceNumber, + address provider, + bytes32 randomNumber +) internal override { + // Use randomNumber to select winners +} +``` + +We use the generated `randomNumber` to perform a Fisher-Yates shuffle and select unique winners from the pool of tile owners. + +## Running Tests + +To see the example in action, run the test suite: + +```bash +npx hardhat test +``` + +This will: +1. Deploy the contracts (including mocks). +2. Simulate users buying all tiles. +3. Trigger the Pyth Entropy request. +4. Mock the provider callback. +5. Verify that winners were selected and funds distributed. + +## Deployment (Base Sepolia) + +To deploy to a live network like Base Sepolia: + +1. Set up your `.env` file: + ``` + PRIVATE_KEY=your_private_key + ``` + +2. Deploy using Hardhat: + ```bash + npx hardhat run scripts/deploy.ts --network baseSepolia + ``` + + *Note: You will need to pass the actual Pyth Entropy addresses for the target network in the constructor.* + + **Base Sepolia Pyth Entropy Address**: `0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c` + +## License + +MIT diff --git a/entropy/hyperlinkgrid/contracts/HyperlinkgridEntropy.sol b/entropy/hyperlinkgrid/contracts/HyperlinkgridEntropy.sol new file mode 100644 index 00000000..b8761cb2 --- /dev/null +++ b/entropy/hyperlinkgrid/contracts/HyperlinkgridEntropy.sol @@ -0,0 +1,196 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +import "@openzeppelin/contracts/token/ERC721/ERC721.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import "@pythnetwork/entropy-sdk-solidity/IEntropy.sol"; +import "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol"; + +/** + * @title HyperlinkgridEntropy + * @notice A simplified example of using Pyth Entropy to select winners in a decentralized lottery-style mechanic. + * This contract represents a grid of tiles that can be purchased. Once the grid is full, + * Pyth Entropy is used to generate a verifiable random number to select beneficiaries who split the pot. + */ +contract HyperlinkgridEntropy is ERC721, Ownable, IEntropyConsumer { + // ============================================================= + // CONSTANTS + // ============================================================= + + uint256 public constant MAX_SUPPLY = 10; // Reduced for example purposes (original was 10000) + uint256 public constant TILE_PRICE = 100 * 10**6; // 100 USDC + uint256 public constant NUM_BENEFICIARIES = 2; // Reduced for example purposes + + // ============================================================= + // STATE + // ============================================================= + + uint256 public nextId = 1; + IERC20 public usdc; + + // Pyth Entropy + IEntropy public entropy; + address public entropyProvider; + + struct Tile { + string url; + uint24 color; + } + mapping(uint256 => Tile) public tiles; + + // End Game State + bool public endGameTriggered; + bool public endGameCompleted; + uint64 public endGameSequenceNumber; + + // Winners (Beneficiaries) + uint256[] public beneficiaries; + + // ============================================================= + // EVENTS + // ============================================================= + + event TilePurchased(uint256 indexed id, address indexed owner, uint24 color, string url); + event EndGameRequested(uint64 sequenceNumber); + event EndGameCompleted(uint256[] beneficiaryIds); + event PayoutDistributed(address beneficiary, uint256 amount); + + // ============================================================= + // CONSTRUCTOR + // ============================================================= + + constructor( + address _usdcAddress, + address _entropyAddress, + address _entropyProvider + ) + ERC721("Hyperlinkgrid", "GRID") + Ownable(msg.sender) + { + usdc = IERC20(_usdcAddress); + entropy = IEntropy(_entropyAddress); + entropyProvider = _entropyProvider; + } + + // ============================================================= + // CORE FUNCTIONS + // ============================================================= + + function buyNextTile(uint24 _color, string calldata _url) external { + require(nextId <= MAX_SUPPLY, "Grid is full"); + uint256 tileId = nextId; + + // Transfer USDC to THIS contract (Holding for End Game) + bool success = usdc.transferFrom(msg.sender, address(this), TILE_PRICE); + require(success, "USDC transfer failed"); + + _mint(msg.sender, tileId); + tiles[tileId] = Tile({url: _url, color: _color}); + + emit TilePurchased(tileId, msg.sender, _color, _url); + nextId++; + } + + // ============================================================= + // PYTH ENTROPY SDK + // ============================================================= + + function getEntropy() internal view override returns (address) { + return address(entropy); + } + + // ============================================================= + // END GAME (PYTH) + // ============================================================= + + // Step 1: Trigger the random number request + // Requires a small ETH fee for Pyth + function triggerEndGame(bytes32 userRandomNumber) external payable { + require(nextId > MAX_SUPPLY, "Grid not full yet"); + require(!endGameTriggered, "End game already triggered"); + + uint256 fee = entropy.getFee(entropyProvider); + require(msg.value >= fee, "Insufficient ETH for Pyth fee"); + + uint64 seq = entropy.requestWithCallback{value: fee}( + entropyProvider, + userRandomNumber + ); + + endGameSequenceNumber = seq; + endGameTriggered = true; + emit EndGameRequested(seq); + } + + // Step 2: Pyth calls this back with randomness + function entropyCallback( + uint64 sequenceNumber, + address provider, + bytes32 randomNumber + ) internal override { + require(sequenceNumber == endGameSequenceNumber, "Invalid sequence"); + require(provider == entropyProvider, "Invalid provider"); + require(!endGameCompleted, "Already completed"); + + // Use randomness to pick unique winners + // Simple shuffle-like selection + uint256[] memory pool = new uint256[](MAX_SUPPLY); + for(uint256 i=0; i 0 && totalBalance > 0) { + uint256 payoutPerWinner = totalBalance / NUM_BENEFICIARIES; + + for(uint256 i=0; i uint128) public providerFees; + mapping(uint64 => address) public requests; + mapping(uint64 => address) public requestProviders; + uint64 public nextSequenceNumber = 1; + + // Implement all interface methods to satisfy the compiler + // Most are just stubs that revert or return default values + + function register( + uint128 feeInWei, + bytes32 commitment, + bytes calldata commitmentMetadata, + uint64 chainLength, + bytes calldata uri + ) external override {} + + function withdraw(uint128 amount) external override {} + + function withdrawAsFeeManager(address provider, uint128 amount) external override {} + + function request( + address provider, + bytes32 userCommitment, + bool useBlockHash + ) external payable override returns (uint64 assignedSequenceNumber) { + return 0; + } + + function requestWithCallback( + address provider, + bytes32 userRandomNumber + ) external payable override returns (uint64 assignedSequenceNumber) { + uint64 seq = nextSequenceNumber++; + requests[seq] = msg.sender; + requestProviders[seq] = provider; + return seq; + } + + function reveal( + address provider, + uint64 sequenceNumber, + bytes32 userRevelation, + bytes32 providerRevelation + ) external override returns (bytes32 randomNumber) { + return bytes32(0); + } + + function revealWithCallback( + address provider, + uint64 sequenceNumber, + bytes32 userRandomNumber, + bytes32 providerRevelation + ) external override { + address consumer = requests[sequenceNumber]; + require(consumer != address(0), "Request not found"); + + bytes32 randomNumber = keccak256(abi.encodePacked(userRandomNumber, providerRevelation)); + + // Call the wrapper function in IEntropyConsumer + IEntropyConsumer(consumer)._entropyCallback(sequenceNumber, provider, randomNumber); + } + + function getProviderInfo( + address provider + ) external view override returns (EntropyStructs.ProviderInfo memory info) {} + + function getDefaultProvider() external view override returns (address provider) { + return address(0); + } + + function getRequest( + address provider, + uint64 sequenceNumber + ) external view override returns (EntropyStructs.Request memory req) {} + + function getFee(address provider) external view override returns (uint128 feeAmount) { + return providerFees[provider]; + } + + function getAccruedPythFees() + external + view + override + returns (uint128 accruedPythFeesInWei) {} + + function setProviderFee(uint128 newFeeInWei) external override { + providerFees[msg.sender] = newFeeInWei; + } + + function setProviderFeeAsFeeManager( + address provider, + uint128 newFeeInWei + ) external override { + providerFees[provider] = newFeeInWei; + } + + function setProviderUri(bytes calldata newUri) external override {} + + function setFeeManager(address manager) external override {} + + function constructUserCommitment( + bytes32 userRandomness + ) external pure override returns (bytes32 userCommitment) { + return keccak256(abi.encodePacked(userRandomness)); + } + + function combineRandomValues( + bytes32 userRandomness, + bytes32 providerRandomness, + bytes32 blockHash + ) external pure override returns (bytes32 combinedRandomness) { + return keccak256(abi.encodePacked(userRandomness, providerRandomness, blockHash)); + } +} diff --git a/entropy/hyperlinkgrid/hardhat.config.ts b/entropy/hyperlinkgrid/hardhat.config.ts new file mode 100644 index 00000000..0346f955 --- /dev/null +++ b/entropy/hyperlinkgrid/hardhat.config.ts @@ -0,0 +1,20 @@ +import { HardhatUserConfig } from "hardhat/config"; +import "@nomicfoundation/hardhat-toolbox"; +import * as dotenv from "dotenv"; + +dotenv.config(); + +const config: HardhatUserConfig = { + solidity: "0.8.20", + networks: { + hardhat: { + }, + // Example configuration for Base Sepolia + baseSepolia: { + url: "https://sepolia.base.org", + accounts: process.env.PRIVATE_KEY ? [process.env.PRIVATE_KEY] : [], + }, + }, +}; + +export default config; diff --git a/entropy/hyperlinkgrid/package.json b/entropy/hyperlinkgrid/package.json new file mode 100644 index 00000000..280872c5 --- /dev/null +++ b/entropy/hyperlinkgrid/package.json @@ -0,0 +1,20 @@ +{ + "name": "hyperlinkgrid-pyth-entropy-example", + "version": "1.0.0", + "description": "Example of using Pyth Entropy for on-chain randomness", + "scripts": { + "test": "hardhat test", + "compile": "hardhat compile" + }, + "devDependencies": { + "@nomicfoundation/hardhat-toolbox": "^5.0.0", + "hardhat": "^2.22.0", + "typescript": "^5.0.0", + "ts-node": "^10.9.0" + }, + "dependencies": { + "@openzeppelin/contracts": "^5.0.0", + "@pythnetwork/entropy-sdk-solidity": "^1.5.0", + "dotenv": "^16.4.5" + } +} diff --git a/entropy/hyperlinkgrid/scripts/deploy.ts b/entropy/hyperlinkgrid/scripts/deploy.ts new file mode 100644 index 00000000..53ce180a --- /dev/null +++ b/entropy/hyperlinkgrid/scripts/deploy.ts @@ -0,0 +1,44 @@ +import { ethers } from "hardhat"; + +async function main() { + // 1. Setup arguments + // In a real deployment, you would use existing addresses: + // USDC on Base Sepolia: 0x036CbD53842c5426634e7929541eC2318f3dCF7e (Example, please verify) + // Pyth Entropy on Base Sepolia: 0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c + // Entropy Provider: 0x6CC14824Ea2918f5De5C2f75A9Da963ad443d51E (Example Pyth Provider) + + // For this script, we will deploy a Mock USDC if on localhost/hardhat + let usdcAddress = process.env.USDC_ADDRESS; + const entropyAddress = process.env.ENTROPY_ADDRESS || "0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c"; + const entropyProvider = process.env.ENTROPY_PROVIDER || "0x6CC14824Ea2918f5De5C2f75A9Da963ad443d51E"; + + if (!usdcAddress) { + console.log("Deploying MockUSDC..."); + const MockUSDC = await ethers.getContractFactory("MockUSDC"); + const usdc = await MockUSDC.deploy(); + await usdc.waitForDeployment(); + usdcAddress = await usdc.getAddress(); + console.log(`MockUSDC deployed to: ${usdcAddress}`); + } + + console.log("Deploying HyperlinkgridEntropy..."); + console.log(`USDC: ${usdcAddress}`); + console.log(`Entropy: ${entropyAddress}`); + console.log(`Provider: ${entropyProvider}`); + + const HyperlinkgridEntropy = await ethers.getContractFactory("HyperlinkgridEntropy"); + const grid = await HyperlinkgridEntropy.deploy( + usdcAddress, + entropyAddress, + entropyProvider + ); + + await grid.waitForDeployment(); + + console.log(`HyperlinkgridEntropy deployed to: ${await grid.getAddress()}`); +} + +main().catch((error) => { + console.error(error); + process.exitCode = 1; +}); diff --git a/entropy/hyperlinkgrid/test/HyperlinkgridEntropy.test.ts b/entropy/hyperlinkgrid/test/HyperlinkgridEntropy.test.ts new file mode 100644 index 00000000..dbf092b7 --- /dev/null +++ b/entropy/hyperlinkgrid/test/HyperlinkgridEntropy.test.ts @@ -0,0 +1,95 @@ +import { expect } from "chai"; +import { ethers } from "hardhat"; +import { HyperlinkgridEntropy, MockUSDC, EntropyMock } from "../typechain-types"; + +describe("HyperlinkgridEntropy", function () { + let hyperlinkgrid: HyperlinkgridEntropy; + let usdc: MockUSDC; + let entropy: EntropyMock; + let owner: any; + let otherAccount: any; + let users: any[]; + + const TILE_PRICE = ethers.parseUnits("100", 6); + const ENTROPY_FEE = ethers.parseEther("0.01"); + + beforeEach(async function () { + [owner, otherAccount, ...users] = await ethers.getSigners(); + + // Deploy Mock USDC + const MockUSDCFactory = await ethers.getContractFactory("MockUSDC"); + usdc = await MockUSDCFactory.deploy(); + await usdc.waitForDeployment(); + + // Deploy Entropy Mock + const EntropyMockFactory = await ethers.getContractFactory("EntropyMock"); + entropy = await EntropyMockFactory.deploy(); + await entropy.waitForDeployment(); + + // Set mock fee (as owner) + await entropy.connect(owner).setProviderFee(ENTROPY_FEE); + + // Deploy HyperlinkgridEntropy + const HyperlinkgridFactory = await ethers.getContractFactory("HyperlinkgridEntropy"); + hyperlinkgrid = await HyperlinkgridFactory.deploy( + await usdc.getAddress(), + await entropy.getAddress(), + owner.address // Using owner as entropy provider for simplicity + ); + await hyperlinkgrid.waitForDeployment(); + }); + + it("Should complete the full flow: Buy tiles -> Trigger End Game -> Select Winners", async function () { + const maxSupply = Number(await hyperlinkgrid.MAX_SUPPLY()); + + // 1. Buy all tiles + for (let i = 0; i < maxSupply; i++) { + const user = users[i]; + // Mint USDC to user + await usdc.mint(user.address, TILE_PRICE); + // Approve contract + await usdc.connect(user).approve(await hyperlinkgrid.getAddress(), TILE_PRICE); + // Buy tile + await hyperlinkgrid.connect(user).buyNextTile(0xFF0000, `https://example.com/${i}`); + } + + expect(await usdc.balanceOf(await hyperlinkgrid.getAddress())).to.equal(TILE_PRICE * BigInt(maxSupply)); + + // 2. Trigger End Game + const userRandom = ethers.hexlify(ethers.randomBytes(32)); + await expect(hyperlinkgrid.triggerEndGame(userRandom, { value: ENTROPY_FEE })) + .to.emit(hyperlinkgrid, "EndGameRequested"); + + const seqNumber = await hyperlinkgrid.endGameSequenceNumber(); + + // 3. Simulate Entropy Callback (Provider reveals) + const providerRandom = ethers.hexlify(ethers.randomBytes(32)); + + // In a real scenario, the provider would call revealWithCallback on the Entropy contract + // We verify the EndGameCompleted event + await expect(entropy.revealWithCallback( + owner.address, + seqNumber, + userRandom, + providerRandom + )).to.emit(hyperlinkgrid, "EndGameCompleted"); + + // 4. Verify Winners + const beneficiaries = await hyperlinkgrid.getBeneficiaries(); + expect(beneficiaries.length).to.equal(2); + + // Verify Payout + // Each winner should receive TotalPot / 2 + const totalPot = TILE_PRICE * BigInt(maxSupply); + const payout = totalPot / 2n; + + // Check balance of one of the winners + const winnerId = beneficiaries[0]; + const winnerOwner = await hyperlinkgrid.ownerOf(winnerId); + + // The winner should have their initial balance (0 after purchase) + payout + // Wait, we minted exactly TILE_PRICE to them and they spent it. So balance should be payout. + const winnerBalance = await usdc.balanceOf(winnerOwner); + expect(winnerBalance).to.equal(payout); + }); +}); diff --git a/entropy/hyperlinkgrid/tsconfig.json b/entropy/hyperlinkgrid/tsconfig.json new file mode 100644 index 00000000..3a21d29c --- /dev/null +++ b/entropy/hyperlinkgrid/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "es2020", + "module": "commonjs", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["./hardhat.config.ts", "./scripts", "./test", "./typechain-types"], + "files": ["./hardhat.config.ts"] +}