diff --git a/entropy/param_protocol/Read b/entropy/param_protocol/Read new file mode 100644 index 0000000..f50817a --- /dev/null +++ b/entropy/param_protocol/Read @@ -0,0 +1,62 @@ +# Param Protocol: A Hybrid "Base + Bonus" Incentive Model + +This project is an example of an innovative incentive model for a data monetization platform, using Pyth Entropy to provide provably-fair, tiered bonus rewards. + +**Live Contract (Base Sepolia):** `0xbF26F622e0322cc7eC12561f897f397B390F97b7` +**Test Token (Base Sepolia):** `0xaA27192Cb967Ca6c0C2e1F3c044874E5a73Fdb4B` + +--- + +## Project Overview + +Param Protocol is a data monetization platform where users can contribute data (represented by an IPFS CID) and mint it as a Data NFT. + +### The Problem + +How do you properly incentivize users to contribute high-quality data? + +1. A **fixed reward** is predictable but boring. +2. A **pure lottery** is exciting but risky, as users may get nothing for their contribution. + +### Our Pyth Entropy Innovation: The Hybrid "Base + Bonus" Model + +We use Pyth Entropy to power a **hybrid "Base + Bonus" reward system** that solves this problem. This model provides the best of both worlds: the **security** of a guaranteed reward and the **excitement** of a probabilistic jackpot. + +1. **Base Reward (Instant):** When a user calls `mintDataNFT`, they *instantly* receive their NFT and a **guaranteed 10 PTK base reward**. This builds trust and ensures no contribution is ever wasted. +2. **Bonus Reward (Asynchronous):** The *exact same transaction* also pays the Pyth fee and requests a random number. About 1-2 minutes later, the `entropyCallback` function is securely called by the Pyth network, minting a **probabilistic bonus** (Common, Rare, or Legendary) to the user. + +This approach is innovative because it moves beyond simple lotteries and uses Pyth Entropy as a core component of a sophisticated, on-chain economic incentive system. + +--- + +## How It Works: The On-Chain Flow + +1. A user (the `onlyOwner` in this example) calls `mintDataNFT(cid, contributorAddress)` and attaches the Pyth fee (via `msg.value`). +2. The contract *instantly* mints the Data NFT to the contributor. +3. The contract *instantly* mints the `baseRewardAmount` (10 PTK) to the contributor. +4. The contract *simultaneously* calls `pyth.requestV2()`, forwarding the fee and storing the `sequenceNumber`. +5. (1-2 minutes later) The Pyth network calls `entropyCallback` on our contract. +6. `entropyCallback` securely calculates the bonus, mints the bonus tokens (if > 0), and saves the bonus amount in `s_sequenceToBonusAmount` for public tracking. + +--- + +## How to Test This Example + +This file contains both the `MockPTK` (ERC20 token) and the `ParamProtocol` (NFT + Rewards) contracts. + +1. **Deploy `MockPTK`:** Deploy the `MockPTK.sol` contract first. +2. **Deploy `ParamProtocol`:** + * Copy the deployed `MockPTK` address. + * Deploy the `ParamProtocol` contract, passing the `MockPTK` address and the Base Sepolia Pyth Entropy address (`0x41c9e39574F40Ad34c79f1C99B66A45eFB830d4c`) to the constructor. +3. **Grant Permission:** + * Call the `grantRole` function on your deployed `MockPTK` contract. + * `role`: `0x9f2df0fed2c77648de5860a4cc508cd0818c85b8b8a1ab4ceeef8d981c8956a6` (MINTER_ROLE) + * `account`: The address of your deployed `ParamProtocol` contract. +4. **Test the Flow:** + * Call `getPythFee()` on the `ParamProtocol` contract to get the current fee. + * Copy the fee and paste it into the `VALUE` field in Remix (set to `Wei`). + * Call `mintDataNFT` with a sample CID (e.g., `"Q..._CID"`) and a contributor's address. +5. **Verify Results:** + * **Instantly:** Call `getPtkBalance` for the contributor. It will show **10 PTK**. + * **After 1-2 mins:** Call `getPtkBalance` again. The balance will now be **10 PTK + Bonus** (e.g., 15, 60, or 1010 PTK). + * You can also find the `sequenceNumber` from the `RandomnessRequested` event and check the specific bonus by calling `s_sequenceToBonusAmount(sequenceNumber)`. diff --git a/entropy/param_protocol/paramprotocol.sol b/entropy/param_protocol/paramprotocol.sol new file mode 100644 index 0000000..cd23620 --- /dev/null +++ b/entropy/param_protocol/paramprotocol.sol @@ -0,0 +1,213 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.20; + +// OpenZeppelin Imports +import "@openzeppelin/contracts/token/ERC721/extensions/ERC721URIStorage.sol"; +import "@openzeppelin/contracts/access/Ownable.sol"; +import "@openzeppelin/contracts/token/ERC20/ERC20.sol"; +import "@openzeppelin/contracts/access/AccessControl.sol"; + +// Pyth Imports +import { IEntropyV2 } from "@pythnetwork/entropy-sdk-solidity/IEntropyV2.sol"; +import { IEntropyConsumer } from "@pythnetwork/entropy-sdk-solidity/IEntropyConsumer.sol"; + +/** + * @title MockPTK + * @dev This is your sample PTK token for deployment and testing. + */ +contract MockPTK is ERC20, AccessControl { + bytes32 public constant MINTER_ROLE = keccak256("MINTER_ROLE"); + + constructor() ERC20("Mock Param Token", "mPTK") { + _grantRole(DEFAULT_ADMIN_ROLE, msg.sender); + _grantRole(MINTER_ROLE, msg.sender); + } + + function mint(address to, uint256 amount) external onlyRole(MINTER_ROLE) { + _mint(to, amount); + } +} + +/** + * @title IMintablePTK + * @dev The interface our main contract will use to talk to the token. + */ +interface IMintablePTK { + function mint(address to, uint256 amount) external; + function balanceOf(address account) external view returns (uint256); +} + + +/** + * @title ParamProtocol + * @dev This is your FINAL, UNIFIED contract. + * It mints NFTs, gives a base reward, AND requests a Pyth bonus. + */ +contract ParamProtocol is ERC721URIStorage, Ownable, IEntropyConsumer { + + // --- State Variables --- + IMintablePTK public ptkToken; + IEntropyV2 private pyth; + + // NFT and Base Reward + uint256 public tokenIdCounter; + uint256 public baseRewardAmount = 10 * 10**18; // 10 PTK + + // Pyth Bonus Reward + mapping(uint64 => address) public s_sequenceToContributor; + mapping(uint64 => bool) public s_requestFulfilled; + mapping(uint64 => uint256) public s_sequenceToBonusAmount; + + // --- THIS IS THE FIX --- + // These constants were missing from the previous version + uint256 public constant BONUS_REWARD_COMMON = 5 * 10**18; + uint256 public constant BONUS_REWARD_RARE = 50 * 10**18; + uint256 public constant BONUS_REWARD_LEGENDARY = 1000 * 10**18; + // ------------------------ + + // NFT Contributor Tracking + mapping(uint256 => address) private _nftContributor; + mapping(address => uint256[]) private _contributorNFTs; + + // --- Events --- + event DataNFTMinted(address indexed contributor, uint256 tokenId, string cid); + event BaseRewardMinted(address indexed contributor, uint256 amount); + event RandomnessRequested(uint64 indexed sequenceNumber, address indexed contributor); + event BonusRewardMinted(address indexed contributor, uint256 amount, uint64 indexed sequenceNumber); + + // --- Constructor --- + constructor( + address _ptkTokenAddress, + address _pythAddress + ) + ERC721("ParamProtocolDataNFT", "PPDNFT") + Ownable(msg.sender) + { + ptkToken = IMintablePTK(_ptkTokenAddress); + pyth = IEntropyV2(_pythAddress); + } + + // ========================================================== + // --- CORE LOGIC --- + // ========================================================== + + function mintDataNFT(string calldata _cid, address _contributor) + external + payable + onlyOwner + { + // 1. Check for Pyth Fee + uint256 fee = pyth.getFeeV2(); + require(msg.value >= fee, "Not enough fee for Pyth"); + + // 2. Mint NFT Logic + require(_contributor != address(0), "Invalid contributor"); + uint256 newTokenId = tokenIdCounter + 1; + _safeMint(_contributor, newTokenId); + _setTokenURI(newTokenId, _cid); + tokenIdCounter = newTokenId; + _nftContributor[newTokenId] = _contributor; + _contributorNFTs[_contributor].push(newTokenId); + emit DataNFTMinted(_contributor, newTokenId, _cid); + + // 3. Mint BASE Reward (Instant) + ptkToken.mint(_contributor, baseRewardAmount); + emit BaseRewardMinted(_contributor, baseRewardAmount); + + // 4. Request BONUS Reward (Asynchronous) + uint64 sequenceNumber = pyth.requestV2{value: msg.value}(); + s_sequenceToContributor[sequenceNumber] = _contributor; + emit RandomnessRequested(sequenceNumber, _contributor); + } + + /** + * @notice The Pyth callback function for the bonus reward. + */ + function entropyCallback( + uint64 sequenceNumber, + address provider, + bytes32 randomNumber + ) internal override { + (provider); // Mark as used + + address contributor = s_sequenceToContributor[sequenceNumber]; + require(contributor != address(0), "Invalid sequence"); + require(!s_requestFulfilled[sequenceNumber], "Request already fulfilled"); + + s_requestFulfilled[sequenceNumber] = true; + + uint256 bonusAmount = _calculateBonusReward(uint64(uint256(randomNumber))); + + s_sequenceToBonusAmount[sequenceNumber] = bonusAmount; + + if (bonusAmount > 0) { + ptkToken.mint(contributor, bonusAmount); + emit BonusRewardMinted(contributor, bonusAmount, sequenceNumber); + } + } + + /** + * @notice Internal logic to determine bonus reward tier. + */ + function _calculateBonusReward(uint64 _randomNumber) + internal pure returns (uint256) + { + uint256 chance = _randomNumber % 100; // 0-99 + // These calls are now valid + if (chance == 0) return BONUS_REWARD_LEGENDARY; // 1% + if (chance < 10) return BONUS_REWARD_RARE; // 9% + if (chance < 30) return BONUS_REWARD_COMMON; // 20% + return 0; // 70% + } + + // ========================================================== + // --- Admin & Getter Functions (All Included) --- + // ========================================================== + + /** + * @notice Required by IEntropyConsumer interface. + */ + function getEntropy() internal view override returns (address) { + return address(pyth); + } + + function setBaseRewardAmount(uint256 _newAmount) external onlyOwner { + baseRewardAmount = _newAmount; + } + + function setPythAddress(address _newAddress) external onlyOwner { + pyth = IEntropyV2(_newAddress); + } + + function getPythFee() external view returns (uint256) { + return pyth.getFeeV2(); + } + + function getPtkBalance(address _user) external view returns (uint256) { + return ptkToken.balanceOf(_user); + } + + function getBaseRewardAmount() external view returns (uint256) { + return baseRewardAmount; + } + + function getNFTOwner(uint256 tokenId) external view returns (address) { + return ownerOf(tokenId); + } + + function getContributor(uint256 tokenId) external view returns (address) { + return _nftContributor[tokenId]; + } + + function getNFTsByContributor(address contributor) external view returns (uint256[] memory) { + return _contributorNFTs[contributor]; + } + + function getCID(uint256 tokenId) external view returns (string memory) { + return tokenURI(tokenId); + } + + function getTotalNFTs() external view returns (uint256) { + return tokenIdCounter; + } +}