Skip to content

Commit 30ea378

Browse files
committed
Add Hyperlinkgrid Entropy example
1 parent b48bc74 commit 30ea378

File tree

9 files changed

+628
-0
lines changed

9 files changed

+628
-0
lines changed

entropy/hyperlinkgrid/README.md

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Hyperlinkgrid x Pyth Entropy Example
2+
3+
This project demonstrates how to use **Pyth Entropy** to generate secure, verifiable on-chain randomness in a Solidity smart contract.
4+
5+
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.
6+
7+
## Features
8+
9+
- **ERC-721 NFT Grid**: Users buy tiles (NFTs) with USDC.
10+
- **Pyth Entropy Integration**: Secure randomness generation for selecting winners.
11+
- **Automated Payouts**: Winners are automatically paid out in USDC.
12+
13+
## Prerequisites
14+
15+
- Node.js (v18 or later)
16+
- npm or pnpm
17+
18+
## Installation
19+
20+
1. Clone this repository and navigate to the directory.
21+
2. Install dependencies:
22+
23+
```bash
24+
npm install
25+
```
26+
27+
## Project Structure
28+
29+
- `contracts/HyperlinkgridEntropy.sol`: The main contract integrating Pyth Entropy.
30+
- `contracts/MockUSDC.sol`: A simple ERC20 token for testing payments.
31+
- `contracts/mocks/EntropyMock.sol`: A mock of the Pyth Entropy contract for local testing.
32+
- `test/HyperlinkgridEntropy.test.ts`: Hardhat tests demonstrating the full flow.
33+
34+
## How it Works
35+
36+
### 1. Purchase Phase
37+
Users purchase tiles on the grid by paying 100 USDC. The USDC is held in the contract.
38+
39+
### 2. Triggering Randomness
40+
Once `MAX_SUPPLY` (10 in this example) is reached, any user can call `triggerEndGame`.
41+
This function:
42+
- Requests a random number from the Pyth Entropy contract.
43+
- Pays the required fee (in native ETH/GAS token).
44+
- Stores the sequence number.
45+
46+
```solidity
47+
uint64 seq = entropy.requestWithCallback{value: fee}(
48+
entropyProvider,
49+
userRandomNumber
50+
);
51+
```
52+
53+
### 3. Entropy Callback
54+
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.
55+
56+
```solidity
57+
function entropyCallback(
58+
uint64 sequenceNumber,
59+
address provider,
60+
bytes32 randomNumber
61+
) internal override {
62+
// Use randomNumber to select winners
63+
}
64+
```
65+
66+
We use the generated `randomNumber` to perform a Fisher-Yates shuffle and select unique winners from the pool of tile owners.
67+
68+
## Running Tests
69+
70+
To see the example in action, run the test suite:
71+
72+
```bash
73+
npx hardhat test
74+
```
75+
76+
This will:
77+
1. Deploy the contracts (including mocks).
78+
2. Simulate users buying all tiles.
79+
3. Trigger the Pyth Entropy request.
80+
4. Mock the provider callback.
81+
5. Verify that winners were selected and funds distributed.
82+
83+
## Deployment (Base Sepolia)
84+
85+
To deploy to a live network like Base Sepolia:
86+
87+
1. Set up your `.env` file:
88+
```
89+
PRIVATE_KEY=your_private_key
90+
```
91+
92+
2. Deploy using Hardhat:
93+
```bash
94+
npx hardhat run scripts/deploy.ts --network baseSepolia
95+
```
96+
97+
*Note: You will need to pass the actual Pyth Entropy addresses for the target network in the constructor.*
98+
99+
**Base Sepolia Pyth Entropy Address**: `0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c`
100+
101+
## License
102+
103+
MIT
Lines changed: 196 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,196 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
5+
import "@openzeppelin/contracts/access/Ownable.sol";
6+
import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
7+
import "@pythnetwork/entropy-sdk-solidity/IEntropy.sol";
8+
import "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol";
9+
10+
/**
11+
* @title HyperlinkgridEntropy
12+
* @notice A simplified example of using Pyth Entropy to select winners in a decentralized lottery-style mechanic.
13+
* This contract represents a grid of tiles that can be purchased. Once the grid is full,
14+
* Pyth Entropy is used to generate a verifiable random number to select beneficiaries who split the pot.
15+
*/
16+
contract HyperlinkgridEntropy is ERC721, Ownable, IEntropyConsumer {
17+
// =============================================================
18+
// CONSTANTS
19+
// =============================================================
20+
21+
uint256 public constant MAX_SUPPLY = 10; // Reduced for example purposes (original was 10000)
22+
uint256 public constant TILE_PRICE = 100 * 10**6; // 100 USDC
23+
uint256 public constant NUM_BENEFICIARIES = 2; // Reduced for example purposes
24+
25+
// =============================================================
26+
// STATE
27+
// =============================================================
28+
29+
uint256 public nextId = 1;
30+
IERC20 public usdc;
31+
32+
// Pyth Entropy
33+
IEntropy public entropy;
34+
address public entropyProvider;
35+
36+
struct Tile {
37+
string url;
38+
uint24 color;
39+
}
40+
mapping(uint256 => Tile) public tiles;
41+
42+
// End Game State
43+
bool public endGameTriggered;
44+
bool public endGameCompleted;
45+
uint64 public endGameSequenceNumber;
46+
47+
// Winners (Beneficiaries)
48+
uint256[] public beneficiaries;
49+
50+
// =============================================================
51+
// EVENTS
52+
// =============================================================
53+
54+
event TilePurchased(uint256 indexed id, address indexed owner, uint24 color, string url);
55+
event EndGameRequested(uint64 sequenceNumber);
56+
event EndGameCompleted(uint256[] beneficiaryIds);
57+
event PayoutDistributed(address beneficiary, uint256 amount);
58+
59+
// =============================================================
60+
// CONSTRUCTOR
61+
// =============================================================
62+
63+
constructor(
64+
address _usdcAddress,
65+
address _entropyAddress,
66+
address _entropyProvider
67+
)
68+
ERC721("Hyperlinkgrid", "GRID")
69+
Ownable(msg.sender)
70+
{
71+
usdc = IERC20(_usdcAddress);
72+
entropy = IEntropy(_entropyAddress);
73+
entropyProvider = _entropyProvider;
74+
}
75+
76+
// =============================================================
77+
// CORE FUNCTIONS
78+
// =============================================================
79+
80+
function buyNextTile(uint24 _color, string calldata _url) external {
81+
require(nextId <= MAX_SUPPLY, "Grid is full");
82+
uint256 tileId = nextId;
83+
84+
// Transfer USDC to THIS contract (Holding for End Game)
85+
bool success = usdc.transferFrom(msg.sender, address(this), TILE_PRICE);
86+
require(success, "USDC transfer failed");
87+
88+
_mint(msg.sender, tileId);
89+
tiles[tileId] = Tile({url: _url, color: _color});
90+
91+
emit TilePurchased(tileId, msg.sender, _color, _url);
92+
nextId++;
93+
}
94+
95+
// =============================================================
96+
// PYTH ENTROPY SDK
97+
// =============================================================
98+
99+
function getEntropy() internal view override returns (address) {
100+
return address(entropy);
101+
}
102+
103+
// =============================================================
104+
// END GAME (PYTH)
105+
// =============================================================
106+
107+
// Step 1: Trigger the random number request
108+
// Requires a small ETH fee for Pyth
109+
function triggerEndGame(bytes32 userRandomNumber) external payable {
110+
require(nextId > MAX_SUPPLY, "Grid not full yet");
111+
require(!endGameTriggered, "End game already triggered");
112+
113+
uint256 fee = entropy.getFee(entropyProvider);
114+
require(msg.value >= fee, "Insufficient ETH for Pyth fee");
115+
116+
uint64 seq = entropy.requestWithCallback{value: fee}(
117+
entropyProvider,
118+
userRandomNumber
119+
);
120+
121+
endGameSequenceNumber = seq;
122+
endGameTriggered = true;
123+
emit EndGameRequested(seq);
124+
}
125+
126+
// Step 2: Pyth calls this back with randomness
127+
function entropyCallback(
128+
uint64 sequenceNumber,
129+
address provider,
130+
bytes32 randomNumber
131+
) internal override {
132+
require(sequenceNumber == endGameSequenceNumber, "Invalid sequence");
133+
require(provider == entropyProvider, "Invalid provider");
134+
require(!endGameCompleted, "Already completed");
135+
136+
// Use randomness to pick unique winners
137+
// Simple shuffle-like selection
138+
uint256[] memory pool = new uint256[](MAX_SUPPLY);
139+
for(uint256 i=0; i<MAX_SUPPLY; i++) {
140+
pool[i] = i + 1; // IDs 1 to MAX_SUPPLY
141+
}
142+
143+
// Fisher-Yates shuffle (partial) to pick NUM_BENEFICIARIES
144+
uint256 randomInt = uint256(randomNumber);
145+
146+
for(uint256 i=0; i<NUM_BENEFICIARIES; i++) {
147+
// Re-hash for each step to get "fresh" randomness
148+
randomInt = uint256(keccak256(abi.encode(randomInt, i)));
149+
150+
uint256 indexToPick = i + (randomInt % (MAX_SUPPLY - i));
151+
152+
// Swap
153+
uint256 temp = pool[i];
154+
pool[i] = pool[indexToPick];
155+
pool[indexToPick] = temp;
156+
157+
beneficiaries.push(pool[i]);
158+
}
159+
160+
endGameCompleted = true;
161+
distributePot();
162+
emit EndGameCompleted(beneficiaries);
163+
}
164+
165+
function distributePot() internal {
166+
uint256 totalBalance = usdc.balanceOf(address(this));
167+
if (NUM_BENEFICIARIES > 0 && totalBalance > 0) {
168+
uint256 payoutPerWinner = totalBalance / NUM_BENEFICIARIES;
169+
170+
for(uint256 i=0; i<beneficiaries.length; i++) {
171+
address owner = ownerOf(beneficiaries[i]);
172+
if(owner != address(0)) {
173+
usdc.transfer(owner, payoutPerWinner);
174+
emit PayoutDistributed(owner, payoutPerWinner);
175+
}
176+
}
177+
}
178+
}
179+
180+
// =============================================================
181+
// VIEW
182+
// =============================================================
183+
184+
function getTile(uint256 _id) external view returns (Tile memory) {
185+
return tiles[_id];
186+
}
187+
188+
function getBeneficiaries() external view returns (uint256[] memory) {
189+
return beneficiaries;
190+
}
191+
192+
// Returns the Pyth fee needed to call triggerEndGame
193+
function getEntropyFee() external view returns (uint256) {
194+
return entropy.getFee(entropyProvider);
195+
}
196+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: MIT
2+
pragma solidity ^0.8.20;
3+
4+
import "@openzeppelin/contracts/token/ERC20/ERC20.sol";
5+
6+
contract MockUSDC is ERC20 {
7+
constructor() ERC20("USD Coin", "USDC") {}
8+
9+
function decimals() public pure override returns (uint8) {
10+
return 6;
11+
}
12+
13+
function mint(address to, uint256 amount) external {
14+
_mint(to, amount);
15+
}
16+
}

0 commit comments

Comments
 (0)