diff --git a/abi/ParanetIncentivesPoolFactory.json b/abi/ParanetIncentivesPoolFactory.json index c276757b..ef433cee 100644 --- a/abi/ParanetIncentivesPoolFactory.json +++ b/abi/ParanetIncentivesPoolFactory.json @@ -75,6 +75,11 @@ }, { "inputs": [ + { + "internalType": "bool", + "name": "isNativeReward", + "type": "bool" + }, { "internalType": "address", "name": "paranetKAStorageContract", diff --git a/abi/ParanetNeuroIncentivesPool.json b/abi/ParanetNeuroIncentivesPool.json index 288bf3eb..8f9dfa63 100644 --- a/abi/ParanetNeuroIncentivesPool.json +++ b/abi/ParanetNeuroIncentivesPool.json @@ -6,6 +6,11 @@ "name": "hubAddress", "type": "address" }, + { + "internalType": "address", + "name": "rewardTokenAddress", + "type": "address" + }, { "internalType": "address", "name": "paranetsRegistryAddress", @@ -81,19 +86,19 @@ "anonymous": false, "inputs": [ { - "indexed": false, - "internalType": "uint256", - "name": "oldMultiplier", - "type": "uint256" + "indexed": true, + "internalType": "address", + "name": "sender", + "type": "address" }, { "indexed": false, "internalType": "uint256", - "name": "newMultiplier", + "name": "amount", "type": "uint256" } ], - "name": "NeuroEmissionMultiplierUpdateFinalized", + "name": "NativeNeuroRewardDeposit", "type": "event" }, { @@ -110,34 +115,34 @@ "internalType": "uint256", "name": "newMultiplier", "type": "uint256" - }, - { - "indexed": false, - "internalType": "uint256", - "name": "timestamp", - "type": "uint256" } ], - "name": "NeuroEmissionMultiplierUpdateInitiated", + "name": "NeuroEmissionMultiplierUpdateFinalized", "type": "event" }, { "anonymous": false, "inputs": [ { - "indexed": true, - "internalType": "address", - "name": "sender", - "type": "address" + "indexed": false, + "internalType": "uint256", + "name": "oldMultiplier", + "type": "uint256" }, { "indexed": false, "internalType": "uint256", - "name": "amount", + "name": "newMultiplier", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "timestamp", "type": "uint256" } ], - "name": "NeuroRewardDeposit", + "name": "NeuroEmissionMultiplierUpdateInitiated", "type": "event" }, { @@ -213,7 +218,7 @@ } ], "internalType": "struct ParanetStructs.ParanetIncentivizationProposalVoterInput[]", - "name": "voters_", + "name": "newVoters", "type": "tuple[]" } ], @@ -715,6 +720,19 @@ "stateMutability": "view", "type": "function" }, + { + "inputs": [], + "name": "isNativeNeuro", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [ { @@ -924,6 +942,19 @@ "stateMutability": "nonpayable", "type": "function" }, + { + "inputs": [], + "name": "token", + "outputs": [ + { + "internalType": "contract IERC20", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, { "inputs": [], "name": "totalMinersClaimedNeuro", diff --git a/contracts/v2/constants/ParanetIncentivesPoolConstants.sol b/contracts/v2/constants/ParanetIncentivesPoolConstants.sol index 0b27b99a..5ca3f5b2 100644 --- a/contracts/v2/constants/ParanetIncentivesPoolConstants.sol +++ b/contracts/v2/constants/ParanetIncentivesPoolConstants.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.16; -uint24 constant TOKENS_DIGITS_DIFF = 10 ** 6; +uint24 constant NATIVE_NEURO_DECIMALS_DIFF = 10 ** 6; uint64 constant EMISSION_MULTIPLIER_SCALING_FACTOR = 10 ** 18; uint16 constant PERCENTAGE_SCALING_FACTOR = 10 ** 4; uint16 constant MAX_CUMULATIVE_VOTERS_WEIGHT = 10 ** 4; diff --git a/contracts/v2/paranets/ParanetIncentivesPoolFactory.sol b/contracts/v2/paranets/ParanetIncentivesPoolFactory.sol index 89be712a..c1706c77 100644 --- a/contracts/v2/paranets/ParanetIncentivesPoolFactory.sol +++ b/contracts/v2/paranets/ParanetIncentivesPoolFactory.sol @@ -46,6 +46,7 @@ contract ParanetIncentivesPoolFactory is Named, Versioned, ContractStatusV2, Ini } function deployNeuroIncentivesPool( + bool isNativeReward, address paranetKAStorageContract, uint256 paranetKATokenId, uint256 tracToNeuroEmissionMultiplier, @@ -54,26 +55,28 @@ contract ParanetIncentivesPoolFactory is Named, Versioned, ContractStatusV2, Ini ) external onlyKnowledgeAssetOwner(paranetKAStorageContract, paranetKATokenId) returns (address) { HubV2 h = hub; ParanetsRegistry pr = paranetsRegistry; + string memory incentivesPoolType = isNativeReward ? "Neuroweb" : "NeurowebERC20"; if ( pr.hasIncentivesPoolByType( keccak256(abi.encodePacked(paranetKAStorageContract, paranetKATokenId)), - "Neuroweb" + incentivesPoolType ) ) { revert ParanetErrors.ParanetIncentivesPoolAlreadyExists( paranetKAStorageContract, paranetKATokenId, - "Neuroweb", + incentivesPoolType, pr.getIncentivesPoolAddress( keccak256(abi.encodePacked(paranetKAStorageContract, paranetKATokenId)), - "Neuroweb" + incentivesPoolType ) ); } ParanetNeuroIncentivesPool incentivesPool = new ParanetNeuroIncentivesPool( address(h), + isNativeReward ? address(0) : h.getContractAddress(incentivesPoolType), h.getContractAddress("ParanetsRegistry"), h.getContractAddress("ParanetKnowledgeMinersRegistry"), keccak256(abi.encodePacked(paranetKAStorageContract, paranetKATokenId)), @@ -84,14 +87,14 @@ contract ParanetIncentivesPoolFactory is Named, Versioned, ContractStatusV2, Ini pr.setIncentivesPoolAddress( keccak256(abi.encodePacked(paranetKAStorageContract, paranetKATokenId)), - "Neuroweb", + incentivesPoolType, address(incentivesPool) ); emit ParanetIncetivesPoolDeployed( paranetKAStorageContract, paranetKATokenId, - ParanetStructs.IncentivesPool({poolType: "Neuroweb", addr: address(incentivesPool)}) + ParanetStructs.IncentivesPool({poolType: incentivesPoolType, addr: address(incentivesPool)}) ); return address(incentivesPool); diff --git a/contracts/v2/paranets/ParanetNeuroIncentivesPool.sol b/contracts/v2/paranets/ParanetNeuroIncentivesPool.sol index de332580..3a613855 100644 --- a/contracts/v2/paranets/ParanetNeuroIncentivesPool.sol +++ b/contracts/v2/paranets/ParanetNeuroIncentivesPool.sol @@ -9,17 +9,18 @@ import {Named} from "../../v1/interface/Named.sol"; import {Versioned} from "../../v1/interface/Versioned.sol"; import {ParanetErrors} from "../errors/paranets/ParanetErrors.sol"; import {ParanetStructs} from "../structs/paranets/ParanetStructs.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; import {IERC721} from "@openzeppelin/contracts/token/ERC721/IERC721.sol"; import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; import { EMISSION_MULTIPLIER_SCALING_FACTOR, PERCENTAGE_SCALING_FACTOR, - TOKENS_DIGITS_DIFF, + NATIVE_NEURO_DECIMALS_DIFF, MAX_CUMULATIVE_VOTERS_WEIGHT } from "../constants/ParanetIncentivesPoolConstants.sol"; contract ParanetNeuroIncentivesPool is Named, Versioned { - event NeuroRewardDeposit(address indexed sender, uint256 amount); + event NativeNeuroRewardDeposit(address indexed sender, uint256 amount); event NeuroEmissionMultiplierUpdateInitiated(uint256 oldMultiplier, uint256 newMultiplier, uint256 timestamp); event NeuroEmissionMultiplierUpdateFinalized(uint256 oldMultiplier, uint256 newMultiplier); event ParanetKnowledgeMinerRewardClaimed(address indexed miner, uint256 amount); @@ -27,9 +28,10 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { event ParanetIncentivizationProposalVoterRewardClaimed(address indexed voter, uint256 amount); string private constant _NAME = "ParanetNeuroIncentivesPool"; - string private constant _VERSION = "2.1.3"; + string private constant _VERSION = "2.2.0"; HubV2 public hub; + IERC20 public token; ParanetsRegistry public paranetsRegistry; ParanetKnowledgeMinersRegistry public paranetKnowledgeMinersRegistry; @@ -73,6 +75,7 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { // solhint-disable-next-line no-empty-blocks constructor( address hubAddress, + address rewardTokenAddress, address paranetsRegistryAddress, address knowledgeMinersRegistryAddress, bytes32 paranetId, @@ -87,6 +90,9 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { ); hub = HubV2(hubAddress); + if (rewardTokenAddress != address(0)) { + token = IERC20(rewardTokenAddress); + } paranetsRegistry = ParanetsRegistry(paranetsRegistryAddress); paranetKnowledgeMinersRegistry = ParanetKnowledgeMinersRegistry(knowledgeMinersRegistryAddress); @@ -147,15 +153,33 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { } receive() external payable { - emit NeuroRewardDeposit(msg.sender, msg.value); + emit NativeNeuroRewardDeposit(msg.sender, msg.value); + } + + function isNativeNeuro() public view returns (bool) { + return address(token) == address(0); } function totalNeuroReceived() external view returns (uint256) { - return address(this).balance + totalMinersClaimedNeuro + totalOperatorsClaimedNeuro + totalVotersClaimedNeuro; + if (isNativeNeuro()) { + return (address(this).balance + + totalMinersClaimedNeuro + + totalOperatorsClaimedNeuro + + totalVotersClaimedNeuro); + } else { + return (token.balanceOf(address(this)) + + totalMinersClaimedNeuro + + totalOperatorsClaimedNeuro + + totalVotersClaimedNeuro); + } } function getNeuroBalance() external view returns (uint256) { - return address(this).balance; + if (isNativeNeuro()) { + return address(this).balance; + } else { + return token.balanceOf(address(this)); + } } function updateNeuroEmissionMultiplierUpdateDelay(uint256 newDelay) external onlyHubOwner { @@ -195,19 +219,21 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { } function addVoters( - ParanetStructs.ParanetIncentivizationProposalVoterInput[] calldata voters_ + ParanetStructs.ParanetIncentivizationProposalVoterInput[] calldata newVoters ) external onlyVotersRegistrar { - for (uint i; i < voters_.length; ) { - votersIndexes[voters_[i].addr] = voters.length; + require(totalVotersClaimedNeuro == 0, "Cannot modify voters list"); + + for (uint i; i < newVoters.length; ) { + votersIndexes[newVoters[i].addr] = voters.length; voters.push( ParanetStructs.ParanetIncentivizationProposalVoter({ - addr: voters_[i].addr, - weight: voters_[i].weight, + addr: newVoters[i].addr, + weight: newVoters[i].weight, claimedNeuro: 0 }) ); - cumulativeVotersWeight += uint16(voters_[i].weight); + cumulativeVotersWeight += uint16(newVoters[i].weight); unchecked { i++; @@ -220,6 +246,7 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { function getVoter( address voterAddress ) external view returns (ParanetStructs.ParanetIncentivizationProposalVoter memory) { + require(isProposalVoter(voterAddress), "Given addr isn't a voter"); return voters[votersIndexes[voterAddress]]; } @@ -233,11 +260,12 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { function removeVoters(uint256 limit) external onlyVotersRegistrar { require(voters.length >= limit, "Limit exceeds the num of voters"); + require(totalVotersClaimedNeuro == 0, "Cannot modify voters list"); for (uint256 i; i < limit; ) { - cumulativeVotersWeight -= uint16(voters[voters.length - 1 - i].weight); + cumulativeVotersWeight -= uint16(voters[voters.length - 1].weight); - delete votersIndexes[voters[voters.length - 1 - i].addr]; + delete votersIndexes[voters[voters.length - 1].addr]; voters.pop(); unchecked { @@ -320,7 +348,7 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { function getTotalKnowledgeMinerIncentiveEstimation() public view returns (uint256) { uint96 unrewardedTracSpent = paranetKnowledgeMinersRegistry.getUnrewardedTracSpent(msg.sender, parentParanetId); - if (unrewardedTracSpent < TOKENS_DIGITS_DIFF) { + if (isNativeNeuro() && unrewardedTracSpent < NATIVE_NEURO_DECIMALS_DIFF) { return 0; } @@ -360,7 +388,7 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { // and total NEURO received by the contract, so that Miners don't get tokens belonging to Operator/Voters // Following the example from the above, if we have 100 NEURO as a total reward, Miners should never get // more than 80 NEURO. minersRewardLimit = 80 NEURO - uint256 minersRewardLimit = ((address(this).balance + + uint256 minersRewardLimit = (((isNativeNeuro() ? address(this).balance : token.balanceOf(address(this))) + totalMinersClaimedNeuro + totalOperatorsClaimedNeuro + totalVotersClaimedNeuro) * @@ -377,7 +405,7 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { function getClaimableAllKnowledgeMinersRewardAmount() public view returns (uint256) { uint256 neuroReward = getTotalAllKnowledgeMinersIncentiveEstimation(); - uint256 minersRewardLimit = ((address(this).balance + + uint256 minersRewardLimit = (((isNativeNeuro() ? address(this).balance : token.balanceOf(address(this))) + totalMinersClaimedNeuro + totalOperatorsClaimedNeuro + totalVotersClaimedNeuro) * @@ -442,7 +470,11 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { } totalMinersClaimedNeuro += claimableNeuroReward; - payable(msg.sender).transfer(claimableNeuroReward); + if (isNativeNeuro()) { + payable(msg.sender).transfer(claimableNeuroReward); + } else { + token.transfer(msg.sender, claimableNeuroReward); + } emit ParanetKnowledgeMinerRewardClaimed(msg.sender, claimableNeuroReward); } @@ -454,7 +486,7 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { function getClaimableParanetOperatorRewardAmount() public view returns (uint256) { uint256 neuroReward = getTotalParanetOperatorIncentiveEstimation(); - uint256 operatorRewardLimit = ((address(this).balance + + uint256 operatorRewardLimit = (((isNativeNeuro() ? address(this).balance : token.balanceOf(address(this))) + totalMinersClaimedNeuro + totalOperatorsClaimedNeuro + totalVotersClaimedNeuro) * paranetOperatorRewardPercentage) / PERCENTAGE_SCALING_FACTOR; @@ -488,7 +520,11 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { } totalOperatorsClaimedNeuro += claimableNeuroReward; - payable(msg.sender).transfer(claimableNeuroReward); + if (isNativeNeuro()) { + payable(msg.sender).transfer(claimableNeuroReward); + } else { + token.transfer(msg.sender, claimableNeuroReward); + } emit ParanetOperatorRewardClaimed(msg.sender, claimableNeuroReward); } @@ -504,7 +540,10 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { effectiveNeuroEmissionMultiplier ); - if (cumulativeKnowledgeValueSingleVoterPart - rewardedTracSpentSingleVoterPart < TOKENS_DIGITS_DIFF) { + if ( + isNativeNeuro() && + (cumulativeKnowledgeValueSingleVoterPart - rewardedTracSpentSingleVoterPart < NATIVE_NEURO_DECIMALS_DIFF) + ) { return 0; } @@ -524,7 +563,7 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { uint256 neuroReward = getTotalProposalVoterIncentiveEstimation(); - uint256 voterRewardLimit = ((((address(this).balance + + uint256 voterRewardLimit = (((((isNativeNeuro() ? address(this).balance : token.balanceOf(address(this))) + totalMinersClaimedNeuro + totalOperatorsClaimedNeuro + totalVotersClaimedNeuro) * paranetIncentivizationProposalVotersRewardPercentage) / @@ -539,7 +578,7 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { function getClaimableAllProposalVotersRewardAmount() public view returns (uint256) { uint256 neuroReward = getTotalAllProposalVotersIncentiveEstimation(); - uint256 votersRewardLimit = ((address(this).balance + + uint256 votersRewardLimit = (((isNativeNeuro() ? address(this).balance : token.balanceOf(address(this))) + totalMinersClaimedNeuro + totalOperatorsClaimedNeuro + totalVotersClaimedNeuro) * paranetIncentivizationProposalVotersRewardPercentage) / @@ -569,7 +608,11 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { voters[votersIndexes[msg.sender]].claimedNeuro += claimableNeuroReward; totalVotersClaimedNeuro += claimableNeuroReward; - payable(msg.sender).transfer(claimableNeuroReward); + if (isNativeNeuro()) { + payable(msg.sender).transfer(claimableNeuroReward); + } else { + token.transfer(msg.sender, claimableNeuroReward); + } emit ParanetIncentivizationProposalVoterRewardClaimed(msg.sender, claimableNeuroReward); } @@ -585,7 +628,7 @@ contract ParanetNeuroIncentivesPool is Named, Versioned { (totalClaimedNeuro * EMISSION_MULTIPLIER_SCALING_FACTOR) / effectiveNeuroEmissionMultiplier ); - if (cumulativeKnowledgeValuePart - rewardedTracSpentPart < TOKENS_DIGITS_DIFF) { + if (isNativeNeuro() && (cumulativeKnowledgeValuePart - rewardedTracSpentPart < NATIVE_NEURO_DECIMALS_DIFF)) { return 0; } diff --git a/deploy/001_deploy_hub_v2.ts b/deploy/001_deploy_hub_v2.ts index a1ecf231..b6c8f68c 100644 --- a/deploy/001_deploy_hub_v2.ts +++ b/deploy/001_deploy_hub_v2.ts @@ -20,7 +20,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const Hub = await hre.deployments.deploy('Hub', { contract: 'HubV2', from: deployer, log: true }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - hre.helpers.updateDeploymentsJson('Hub', Hub.address, Hub.receipt!.blockNumber); + hre.helpers.updateDeploymentsJson('Hub', 'Hub', Hub.address, Hub.receipt!.blockNumber); } } diff --git a/deploy/002_deploy_hub.ts b/deploy/002_deploy_hub.ts index a4196ae8..586063fb 100644 --- a/deploy/002_deploy_hub.ts +++ b/deploy/002_deploy_hub.ts @@ -20,7 +20,7 @@ const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { const Hub = await hre.deployments.deploy('Hub', { from: deployer, log: true }); // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - hre.helpers.updateDeploymentsJson('Hub', Hub.address, Hub.receipt!.blockNumber); + hre.helpers.updateDeploymentsJson('Hub', 'Hub', Hub.address, Hub.receipt!.blockNumber); } } diff --git a/deploy/046_deploy_paranet_incentives_pool_factory.ts b/deploy/046_deploy_paranet_incentives_pool_factory.ts index 909877d3..3fcb4c0c 100644 --- a/deploy/046_deploy_paranet_incentives_pool_factory.ts +++ b/deploy/046_deploy_paranet_incentives_pool_factory.ts @@ -2,7 +2,7 @@ import { DeployFunction } from 'hardhat-deploy/types'; import { HardhatRuntimeEnvironment } from 'hardhat/types'; const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { - if (!hre.network.name.startsWith('otp') && !hre.network.name.startsWith('hardhat')) { + if (hre.network.name.startsWith('gnosis')) { return; } diff --git a/deploy/100_set_neuroweb_erc20.ts b/deploy/100_set_neuroweb_erc20.ts new file mode 100644 index 00000000..da8db55c --- /dev/null +++ b/deploy/100_set_neuroweb_erc20.ts @@ -0,0 +1,48 @@ +import { DeployFunction } from 'hardhat-deploy/types'; +import { HardhatRuntimeEnvironment } from 'hardhat/types'; + +const func: DeployFunction = async function (hre: HardhatRuntimeEnvironment) { + const { deployer } = await hre.getNamedAccounts(); + + const neuroERC20Exists = hre.helpers.inConfig('NeurowebERC20'); + + if (neuroERC20Exists) { + const hubAddress = hre.helpers.contractDeployments.contracts['Hub'].evmAddress; + const Hub = await hre.ethers.getContractAt('Hub', hubAddress, deployer); + + const tokenInHub = await Hub['isContract(string)']('NeurowebERC20'); + + if (!tokenInHub && hre.network.config.environment !== 'development') { + hre.helpers.newContracts.push([ + 'NeurowebERC20', + hre.helpers.contractDeployments.contracts['NeurowebERC20'].evmAddress, + ]); + } + } else if (hre.network.config.environment === 'development') { + const Token = await hre.helpers.deploy({ + newContractName: 'Token', + newContractNameInHub: 'NeurowebERC20', + passHubInConstructor: false, + additionalArgs: ['NEURO TEST TOKEN', 'NEURO'], + }); + + const minterRole = await Token.MINTER_ROLE(); + if (!(await Token.hasRole(minterRole, deployer))) { + console.log(`Setting ERC20 Neuro minter role for ${deployer}.`); + const setupMinterRoleTx = await Token.setupRole(deployer, { from: deployer }); + await setupMinterRoleTx.wait(); + } + + const amountToMint = hre.ethers.utils.parseEther(`${10_000_000}`); + const accounts = await hre.ethers.getSigners(); + + for (const acc of accounts) { + const mintTx = await Token.mint(acc.address, amountToMint, { from: deployer, gasLimit: 80_000 }); + await mintTx.wait(); + } + } +}; + +export default func; +func.tags = ['Neuro', 'v1']; +func.dependencies = ['Hub']; diff --git a/deployments/base_mainnet_contracts.json b/deployments/base_mainnet_contracts.json index 9ae96b83..6b0c4e36 100644 --- a/deployments/base_mainnet_contracts.json +++ b/deployments/base_mainnet_contracts.json @@ -4,6 +4,10 @@ "deployed": true, "evmAddress": "0xa81a52b4dda010896cdd386c7fbdc5cdc835ba23" }, + "NeurowebERC20": { + "deployed": true, + "evmAddress": "0x2548c27A04e49B412DD887b08d062D34C72ad2B6" + }, "Hub": { "evmAddress": "0xaBfcf2ad1718828E7D3ec20435b0d0b5EAfbDf2c", "version": "2.0.0", @@ -318,6 +322,15 @@ "deploymentBlock": 16310756, "deploymentTimestamp": 1719410863148, "deployed": true + }, + "ParanetIncentivesPoolFactory": { + "evmAddress": "0x1295037627a42BA1d02Cb13e7c080009dF71C7D1", + "version": "2.0.0", + "gitBranch": "feature/neuro-erc20-incentives", + "gitCommitHash": "b5e57be38cfd094ccd52819fa2c458e4b4acf854", + "deploymentBlock": 19726096, + "deploymentTimestamp": 1726241547795, + "deployed": true } } } diff --git a/deployments/base_sepolia_dev_contracts.json b/deployments/base_sepolia_dev_contracts.json index ad1fa6c9..a47b7b37 100644 --- a/deployments/base_sepolia_dev_contracts.json +++ b/deployments/base_sepolia_dev_contracts.json @@ -4,6 +4,10 @@ "deployed": true, "evmAddress": "0x4ead53ee0aaeB0bE5920DC2DAA7AD93F11cA5207" }, + "NeurowebERC20": { + "deployed": true, + "evmAddress": "0x008Ea316ff593E82c592fccB6072f82d1393F1Ac" + }, "Hub": { "evmAddress": "0x6C861Cb69300C34DfeF674F7C00E734e840C29C0", "version": "2.0.0", @@ -318,6 +322,15 @@ "deploymentBlock": 11509127, "deploymentTimestamp": 1718786545806, "deployed": true + }, + "ParanetIncentivesPoolFactory": { + "evmAddress": "0x64769faeb5903279EFA59B5170BB6257AcCd2A71", + "version": "2.0.0", + "gitBranch": "feature/neuro-erc20-incentives", + "gitCommitHash": "b5e57be38cfd094ccd52819fa2c458e4b4acf854", + "deploymentBlock": 15227285, + "deploymentTimestamp": 1726222864380, + "deployed": true } } } diff --git a/deployments/base_sepolia_test_contracts.json b/deployments/base_sepolia_test_contracts.json index d460a92f..9b8f0dbf 100644 --- a/deployments/base_sepolia_test_contracts.json +++ b/deployments/base_sepolia_test_contracts.json @@ -4,6 +4,10 @@ "deployed": true, "evmAddress": "0x9b17032749aa066a2DeA40b746AA6aa09CdE67d9" }, + "NeurowebERC20": { + "deployed": true, + "evmAddress": "0x3d4f5831fcca588554125f1782dad85a4a235f00" + }, "Hub": { "evmAddress": "0x144eDa5cbf8926327cb2cceef168A121F0E4A299", "version": "2.0.0", @@ -318,6 +322,15 @@ "deploymentBlock": 11510567, "deploymentTimestamp": 1718789426637, "deployed": true + }, + "ParanetIncentivesPoolFactory": { + "evmAddress": "0x3cCCDD320c76030f760E60aa0E519fA3B1fC4b02", + "version": "2.0.0", + "gitBranch": "feature/neuro-erc20-incentives", + "gitCommitHash": "b5e57be38cfd094ccd52819fa2c458e4b4acf854", + "deploymentBlock": 15236554, + "deploymentTimestamp": 1726241399211, + "deployed": true } } } diff --git a/deployments/gnosis_chiado_dev_contracts.json b/deployments/gnosis_chiado_dev_contracts.json index be7029be..0fc43e9e 100644 --- a/deployments/gnosis_chiado_dev_contracts.json +++ b/deployments/gnosis_chiado_dev_contracts.json @@ -317,15 +317,6 @@ "deploymentBlock": 10357793, "deploymentTimestamp": 1718619642976, "deployed": true - }, - "ParanetIncentivesPoolFactory": { - "evmAddress": "0xEA7932aDfA759BA6D78c42bCE6De6258768F7b4e", - "version": "2.0.0", - "gitBranch": "improvement/paranet-incentives-additional-getters", - "gitCommitHash": "86a1d826db5485390b6fca1b13673261d0c4d89e", - "deploymentBlock": 10357795, - "deploymentTimestamp": 1718619653053, - "deployed": true } } } diff --git a/deployments/otp_mainnet_contracts.json b/deployments/otp_mainnet_contracts.json index f9f6b2be..52e7699b 100644 --- a/deployments/otp_mainnet_contracts.json +++ b/deployments/otp_mainnet_contracts.json @@ -307,13 +307,13 @@ "deployed": true }, "ParanetIncentivesPoolFactory": { - "evmAddress": "0x7749834AB8152de91B9b3252Da7750247F52F495", - "substrateAddress": "5EMjsczk5SaUrPcwJmAgXPbxqdWmWduN6z3JV6tSJAReRtJx", + "evmAddress": "0x079e0619161B8aa91F83E6Ed0470fa295F4715C2", + "substrateAddress": "5EMjsczMhexJhiduehcbGxhVwRAKBRX9VXDZ3NnHSnB3znva", "version": "2.0.0", - "gitBranch": "improvement/paranet-incentives-additional-getters", - "gitCommitHash": "4cef06a0b27b03e64d68215f5b3df091b73e31e2", - "deploymentBlock": 5162782, - "deploymentTimestamp": 1718631441728, + "gitBranch": "feature/neuro-erc20-incentives", + "gitCommitHash": "e29d4c314acdde185e8bf1c133ea812344d15af0", + "deploymentBlock": 5802358, + "deploymentTimestamp": 1726242413487, "deployed": true } }, diff --git a/deployments/otp_testnet_contracts.json b/deployments/otp_testnet_contracts.json index 70a72569..6d8a1992 100644 --- a/deployments/otp_testnet_contracts.json +++ b/deployments/otp_testnet_contracts.json @@ -296,13 +296,13 @@ "deployed": true }, "ParanetIncentivesPoolFactory": { - "evmAddress": "0x043F6aBBfd8882acb4f71C49E7f4Bd71271d5FD0", - "substrateAddress": "5EMjsczM2VgpRxsY2B7g23uW33fR21633UMZZUxA6owKPXt6", + "evmAddress": "0x15CDB0bB8eb782c9b21efD99084c15D2Dc44aa92", + "substrateAddress": "5EMjsczQYXEGTkiEhqAEzmC6Mgz8tuVzRiJ3szZpvNiS7u8P", "version": "2.0.0", - "gitBranch": "improvement/paranet-incentives-additional-getters", - "gitCommitHash": "bbf78d963b46a63c4c55fd6a82fc0bbdb59c40bc", - "deploymentBlock": 4108247, - "deploymentTimestamp": 1718630808590, + "gitBranch": "feature/neuro-erc20-incentives", + "gitCommitHash": "e29d4c314acdde185e8bf1c133ea812344d15af0", + "deploymentBlock": 4532480, + "deploymentTimestamp": 1726242358520, "deployed": true } }, diff --git a/hardhat.config.ts b/hardhat.config.ts index 38b6e967..8b5278e4 100644 --- a/hardhat.config.ts +++ b/hardhat.config.ts @@ -145,6 +145,9 @@ config.abiExporter = { 'IERC721Metadata.sol', 'IERC721Receiver.sol', 'IERC734Extended.sol', + 'IERC1155.sol', + 'IERC1155MetadataURI.sol', + 'IERC1155Receiver.sol', 'IERC4906.sol', 'Ownable.sol', 'CommitManagerErrorsV2.sol', diff --git a/test/v2/integration/Paranet.test.ts b/test/v2/integration/Paranet.test.ts index 9ed9a413..d601b798 100644 --- a/test/v2/integration/Paranet.test.ts +++ b/test/v2/integration/Paranet.test.ts @@ -123,6 +123,7 @@ describe('@v2 @integration Paranet', function () { await tx1.wait(); const tx2 = await ParanetIncentivesPoolFactory.connect(operator).deployNeuroIncentivesPool( + true, ContentAssetStorage.address, paranetKATokenId, tracToNeuroEmissionMultiplier, diff --git a/test/v2/unit/Paranet.test.ts b/test/v2/unit/Paranet.test.ts index 9a2b8b4d..95f55249 100644 --- a/test/v2/unit/Paranet.test.ts +++ b/test/v2/unit/Paranet.test.ts @@ -960,6 +960,7 @@ describe('@v2 @unit ParanetKnowledgeMinersRegistry contract', function () { paranetDescription, ); await ParanetIncentivesPoolFactory.connect(accounts[100 + number]).deployNeuroIncentivesPool( + true, paranetKAStorageContract, paranetKATokenId, tracToNeuroEmissionMultiplier, diff --git a/test/v2/unit/ParanetNeuroIncentivesPool.test.ts b/test/v2/unit/ParanetNeuroIncentivesPool.test.ts new file mode 100644 index 00000000..bda6d681 --- /dev/null +++ b/test/v2/unit/ParanetNeuroIncentivesPool.test.ts @@ -0,0 +1,1086 @@ +/* eslint-disable @typescript-eslint/no-unused-vars */ +import { loadFixture, time } from '@nomicfoundation/hardhat-network-helpers'; +import { expect } from 'chai'; +import { BigNumberish, Event } from 'ethers'; +import hre from 'hardhat'; +import { SignerWithAddress } from 'hardhat-deploy-ethers/signers'; + +import { + HubController, + Paranet, + ContentAssetStorageV2, + ContentAssetV2, + ParanetsRegistry, + ParanetServicesRegistry, + ParanetKnowledgeMinersRegistry, + ParanetKnowledgeAssetsRegistry, + HashingProxy, + ServiceAgreementStorageProxy, + Token, + ServiceAgreementV1, + ParanetIncentivesPoolFactory, + Hub, +} from '../../../typechain'; +import { IERC721 } from '../../../typechain/@openzeppelin/contracts/token/ERC721/IERC721'; + +type deployParanetFixture = { + accounts: SignerWithAddress[]; + Paranet: Paranet; + HubController: HubController; + ContentAssetV2: ContentAssetV2; + ContentAssetStorageV2: ContentAssetStorageV2; + ParanetsRegistry: ParanetsRegistry; + ParanetServicesRegistry: ParanetServicesRegistry; + ParanetKnowledgeMinersRegistry: ParanetKnowledgeMinersRegistry; + ParanetKnowledgeAssetsRegistry: ParanetKnowledgeAssetsRegistry; + ParanetIncentivesPoolFactory: ParanetIncentivesPoolFactory; + HashingProxy: HashingProxy; + ServiceAgreementStorageProxy: ServiceAgreementStorageProxy; + Token: Token; + NeuroERC20: Token; + ServiceAgreementV1: ServiceAgreementV1; +}; + +type IncentivizationPoolParameters = { + paranetKAStorageContract: string; + paranetKATokenId: BigNumberish; + tracToNeuroEmissionMultiplier: BigNumberish; + paranetOperatorRewardPercentage: BigNumberish; + paranetIncentivizationProposalVotersRewardPercentage: BigNumberish; +}; + +describe('@v2 @unit ParanetNeuroIncentivesPool contract', function () { + let accounts: SignerWithAddress[]; + let Paranet: Paranet; + let HubController: HubController; + let ContentAssetV2: ContentAssetV2; + let ContentAssetStorageV2: ContentAssetStorageV2; + let ParanetsRegistry: ParanetsRegistry; + let ParanetServicesRegistry: ParanetServicesRegistry; + let ParanetKnowledgeMinersRegistry: ParanetKnowledgeMinersRegistry; + let ParanetKnowledgeAssetsRegistry: ParanetKnowledgeAssetsRegistry; + let ParanetIncentivesPoolFactory: ParanetIncentivesPoolFactory; + let HashingProxy: HashingProxy; + let ServiceAgreementStorageProxy: ServiceAgreementStorageProxy; + let Token: Token; + let NeuroERC20: Token; + let ServiceAgreementV1: ServiceAgreementV1; + let Hub: Hub; + + const EMISSION_MULTIPLIER_SCALING_FACTOR = hre.ethers.constants.WeiPerEther; // 1e18 + const PERCENTAGE_SCALING_FACTOR = 10_000; // as per the contract + const MAX_CUMULATIVE_VOTERS_WEIGHT = 10_000; + + async function deployParanetFixture(): Promise { + await hre.deployments.fixture( + [ + 'HubV2', + 'HubController', + 'Paranet', + 'ContentAssetStorageV2', + 'ContentAssetV2', + 'ParanetsRegistry', + 'ParanetServicesRegistry', + 'ParanetKnowledgeMinersRegistry', + 'ParanetKnowledgeAssetsRegistry', + 'ParanetIncentivesPoolFactory', + 'HashingProxy', + 'ServiceAgreementStorageProxy', + 'Token', + 'Neuro', + 'ServiceAgreementV1', + ], + { keepExistingDeployments: false }, + ); + + Hub = await hre.ethers.getContract('Hub'); + HubController = await hre.ethers.getContract('HubController'); + Paranet = await hre.ethers.getContract('Paranet'); + ContentAssetV2 = await hre.ethers.getContract('ContentAsset'); + ContentAssetStorageV2 = await hre.ethers.getContract('ContentAssetStorage'); + ParanetsRegistry = await hre.ethers.getContract('ParanetsRegistry'); + ParanetServicesRegistry = await hre.ethers.getContract('ParanetServicesRegistry'); + ParanetKnowledgeMinersRegistry = await hre.ethers.getContract( + 'ParanetKnowledgeMinersRegistry', + ); + ParanetKnowledgeAssetsRegistry = await hre.ethers.getContract( + 'ParanetKnowledgeAssetsRegistry', + ); + ParanetIncentivesPoolFactory = await hre.ethers.getContract( + 'ParanetIncentivesPoolFactory', + ); + ServiceAgreementStorageProxy = await hre.ethers.getContract( + 'ServiceAgreementStorageProxy', + ); + HashingProxy = await hre.ethers.getContract('HashingProxy'); + Token = await hre.ethers.getContract('Token'); + const neuroERC20Address = await Hub.getContractAddress('NeurowebERC20'); + NeuroERC20 = await hre.ethers.getContractAt('Token', neuroERC20Address); + ServiceAgreementV1 = await hre.ethers.getContract('ServiceAgreementV1'); + + accounts = await hre.ethers.getSigners(); + await HubController.setContractAddress('HubOwner', accounts[0].address); + + return { + accounts, + Paranet, + HubController, + ContentAssetV2, + ContentAssetStorageV2, + ParanetsRegistry, + ParanetServicesRegistry, + ParanetKnowledgeMinersRegistry, + ParanetKnowledgeAssetsRegistry, + ParanetIncentivesPoolFactory, + HashingProxy, + ServiceAgreementStorageProxy, + Token, + NeuroERC20, + ServiceAgreementV1, + }; + } + + beforeEach(async () => { + hre.helpers.resetDeploymentsJson(); + ({ + accounts, + Paranet, + HubController, + ContentAssetV2, + ContentAssetStorageV2, + ParanetsRegistry, + ParanetServicesRegistry, + ParanetKnowledgeMinersRegistry, + ParanetKnowledgeAssetsRegistry, + ParanetIncentivesPoolFactory, + HashingProxy, + ServiceAgreementStorageProxy, + Token, + NeuroERC20, + ServiceAgreementV1, + } = await loadFixture(deployParanetFixture)); + }); + + it('The contract is named "ParanetNeuroIncentivesPool"', async () => { + const { paranetKAStorageContract, paranetKATokenId } = await registerParanet(accounts, Paranet, 1); + + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), + paranetOperatorRewardPercentage: 1_000, + paranetIncentivizationProposalVotersRewardPercentage: 1_000, + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 1); + expect(await IncentivesPool.name()).to.equal('ParanetNeuroIncentivesPool'); + }); + + it('The contract is version "2.2.0"', async () => { + const { paranetKAStorageContract, paranetKATokenId } = await registerParanet(accounts, Paranet, 1); + + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), + paranetOperatorRewardPercentage: 1_000, + paranetIncentivizationProposalVotersRewardPercentage: 1_000, + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 1); + expect(await IncentivesPool.version()).to.equal('2.2.0'); + }); + + it('Should accept ERC20 Neuro and update the balance', async () => { + const { paranetKAStorageContract, paranetKATokenId } = await registerParanet(accounts, Paranet, 1); + + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), + paranetOperatorRewardPercentage: 1_000, + paranetIncentivizationProposalVotersRewardPercentage: 1_000, + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 1); + + const neuroAmount = hre.ethers.utils.parseEther('100000'); + await NeuroERC20.transfer(IncentivesPool.address, neuroAmount); + + expect(await IncentivesPool.getNeuroBalance()).to.be.equal(neuroAmount); + }); + + it('Should become a Knowledge miner when creating an asset on paranet', async () => { + // register paranet + const { paranetKAStorageContract, paranetKATokenId, paranetId } = await registerParanet(accounts, Paranet, 1); + + // create a Knowledge Asset + const knowledgeMiner = accounts[2]; + await createParanetKnowledgeAsset(knowledgeMiner, paranetKAStorageContract, paranetKATokenId, 1, '10'); + + expect(await ParanetsRegistry.isKnowledgeMinerRegistered(paranetId, knowledgeMiner.address)).to.be.true; + }); + + it('Check paranet operator after transfer', async () => { + // register paranet + const number = 1; + const { paranetKAStorageContract, paranetKATokenId } = await registerParanet(accounts, Paranet, number); + + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), + paranetOperatorRewardPercentage: 1_000, + paranetIncentivizationProposalVotersRewardPercentage: 1_000, + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 1); + + // create ERC721 contract instance + const owner = accounts[100 + number]; + const erc721 = (await hre.ethers.getContractAt('IERC721', paranetKAStorageContract, owner)) as IERC721; + + // transfer to new operator + const newOwner = accounts[200 + number]; + const tx = await erc721.transferFrom(owner.address, newOwner.address, paranetKATokenId); + await tx.wait(); + + // check transfer + const currentOwner = await erc721.ownerOf(paranetKATokenId); + expect(currentOwner).to.be.equal(newOwner.address); + + // check if paranet operator + const isParanetOperator = await IncentivesPool.isParanetOperator(newOwner.address); + expect(isParanetOperator).to.be.true; + }); + + it('votersRegistrar can add voters, voters data can be returned and added voters are proposal voters', async function () { + const number = 1; + const { paranetKAStorageContract, paranetKATokenId } = await registerParanet(accounts, Paranet, number); + + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), + paranetOperatorRewardPercentage: 1_000, + paranetIncentivizationProposalVotersRewardPercentage: 1_000, + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 1); + + // Add voters (voter1 and voter2) to the contract + const votersRegistrar = accounts[0]; + const voter1 = accounts[1]; + const voter2 = accounts[2]; + await IncentivesPool.connect(votersRegistrar).addVoters([ + { addr: voter1.address, weight: 500 }, + { addr: voter2.address, weight: 1000 }, + ]); + + // Retrieve the voters data + const firstVoterData = await IncentivesPool.getVoter(voter1.address); + const secondVoterData = await IncentivesPool.getVoter(voter2.address); + + // Check voter1 data + expect(firstVoterData.addr).to.equal(voter1.address); + expect(firstVoterData.weight).to.equal(500); + expect(firstVoterData.claimedNeuro).to.equal(0); + expect(await IncentivesPool.isProposalVoter(voter1.address)).to.be.true; + + // Check voter2 data + expect(secondVoterData.addr).to.equal(voter2.address); + expect(secondVoterData.weight).to.equal(1000); + expect(secondVoterData.claimedNeuro).to.equal(0); + expect(await IncentivesPool.isProposalVoter(voter2.address)).to.be.true; + }); + + it('Get a total Incentives Pool NEURO balance', async function () { + // create a paranet + const number = 1; + const { paranetKAStorageContract, paranetKATokenId } = await registerParanet(accounts, Paranet, number); + + // create an incentive pool + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), + paranetOperatorRewardPercentage: 1_000, + paranetIncentivizationProposalVotersRewardPercentage: 1_000, + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 1); + + // transfer tokens to the incentives pool + const neuroAmount = hre.ethers.utils.parseEther('1000'); + await NeuroERC20.transfer(IncentivesPool.address, neuroAmount); + + expect(await IncentivesPool.getNeuroBalance()).to.be.equal(neuroAmount); + }); + + it('Get the total received Incentive Pool NEURO', async function () { + // create a paranet + const number = 1; + const { paranetKAStorageContract, paranetKATokenId } = await registerParanet(accounts, Paranet, number); + + // create an incentive pool + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), + paranetOperatorRewardPercentage: 1_000, + paranetIncentivizationProposalVotersRewardPercentage: 1_000, + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 1); + + // transfer tokens to the incentives pool + const neuroAmount = hre.ethers.utils.parseEther('1000'); + await NeuroERC20.transfer(IncentivesPool.address, neuroAmount); + + // Simulate a knowledge miner minting a knowledge asset and claiming the reward + const knowledgeMiner = accounts[1]; + const tokenAmount = '10'; + await createParanetKnowledgeAsset(knowledgeMiner, paranetKAStorageContract, paranetKATokenId, 2, tokenAmount); + await IncentivesPool.connect(knowledgeMiner).claimKnowledgeMinerReward(); + + // Claim operator reward + const operator = accounts[100 + number]; + await IncentivesPool.connect(operator).claimParanetOperatorReward(); + + // Add a voter and claim the reward + const votersRegistrar = accounts[0]; + const voter = accounts[2]; + await IncentivesPool.connect(votersRegistrar).addVoters([{ addr: voter.address, weight: 10000 }]); + await IncentivesPool.connect(voter).claimIncentivizationProposalVoterReward(); + + expect(await IncentivesPool.totalNeuroReceived()).to.be.equal(neuroAmount); + }); + + it('Get the right NEURO Emission Multiplier based on a particular timestamp', async function () { + // create a paranet + const number = 1; + const { paranetKAStorageContract, paranetKATokenId } = await registerParanet(accounts, Paranet, number); + + // create an incentive pool + const initialMultiplier = hre.ethers.utils.parseEther('1'); + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: initialMultiplier, + paranetOperatorRewardPercentage: 1_000, + paranetIncentivizationProposalVotersRewardPercentage: 1_000, + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 1); + + // initiate the multiplier update and fetch the emitted timestamp + const newMultiplier = hre.ethers.utils.parseEther('2'); + const votersRegistrar = accounts[0]; + const tx = await IncentivesPool.connect(votersRegistrar).initiateNeuroEmissionMultiplierUpdate(newMultiplier); + const receipt = await tx.wait(); + const event = receipt.events?.find((e: Event) => e.event === 'NeuroEmissionMultiplierUpdateInitiated'); + const emittedTimestamp = event?.args?.timestamp; + + // jump 7 days in time + const seconds = 7 * 86400; + await time.increase(seconds); + + // finalize the update + await IncentivesPool.connect(votersRegistrar).finalizeNeuroEmissionMultiplierUpdate(); + + const initialNeuroEmissionMultiplier = await IncentivesPool.getEffectiveNeuroEmissionMultiplier( + emittedTimestamp - seconds, + ); + const newNeuroEmissionMultiplier = await IncentivesPool.getEffectiveNeuroEmissionMultiplier(emittedTimestamp); + + expect(initialNeuroEmissionMultiplier).to.be.equal(initialMultiplier); + expect(newNeuroEmissionMultiplier).to.be.equal(newMultiplier); + }); + + it('Knowledge miner can claim the correct NEURO reward', async () => { + const { paranetKAStorageContract, paranetKATokenId, paranetId } = await registerParanet(accounts, Paranet, 1); + + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('0.5'), // 0.5 NEURO per 1 TRAC + paranetOperatorRewardPercentage: 1_000, // 10% + paranetIncentivizationProposalVotersRewardPercentage: 1_000, // 10% + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 1); + + const neuroAmount = hre.ethers.utils.parseEther('1000'); + await NeuroERC20.transfer(IncentivesPool.address, neuroAmount); + + const knowledgeMiner = accounts[2]; + + // Simulate the knowledge miner minting a knowledge asset (spending TRAC) + const tokenAmount = '10'; + await createParanetKnowledgeAsset(knowledgeMiner, paranetKAStorageContract, paranetKATokenId, 2, tokenAmount); + + // Get unrewardedTracSpent + const unrewardedTracSpent = await ParanetKnowledgeMinersRegistry.getUnrewardedTracSpent( + knowledgeMiner.address, + paranetId, + ); + + // Act + const claimableReward = await IncentivesPool.connect(knowledgeMiner).getClaimableKnowledgeMinerRewardAmount(); + + // Assert + const minersRewardPercentage = + PERCENTAGE_SCALING_FACTOR - + incentivesPoolParams.paranetOperatorRewardPercentage - + incentivesPoolParams.paranetIncentivizationProposalVotersRewardPercentage; + + const expectedReward = unrewardedTracSpent + .mul(incentivesPoolParams.tracToNeuroEmissionMultiplier) + .div(EMISSION_MULTIPLIER_SCALING_FACTOR) + .mul(minersRewardPercentage) + .div(PERCENTAGE_SCALING_FACTOR); + + expect(claimableReward).to.equal(expectedReward); + + const initialNeuroBalance = await NeuroERC20.balanceOf(knowledgeMiner.address); + + // Claim the reward + await IncentivesPool.connect(knowledgeMiner).claimKnowledgeMinerReward(); + + // Check balances + const finalNeuroBalance = await NeuroERC20.balanceOf(knowledgeMiner.address); + expect(finalNeuroBalance.sub(initialNeuroBalance)).to.equal(expectedReward); + + const claimedNeuro = await IncentivesPool.minerClaimedNeuro(knowledgeMiner.address); + expect(claimedNeuro).to.equal(expectedReward); + + const totalMinersClaimedNeuro = await IncentivesPool.totalMinersClaimedNeuro(); + expect(totalMinersClaimedNeuro).to.equal(expectedReward); + + const newUnrewardedTracSpent = await ParanetKnowledgeMinersRegistry.getUnrewardedTracSpent( + knowledgeMiner.address, + paranetId, + ); + expect(newUnrewardedTracSpent).to.equal(0); + }); + + it('Knowledge miner cannot claim more NEURO than their share', async () => { + const { paranetKAStorageContract, paranetKATokenId, paranetId } = await registerParanet(accounts, Paranet, 1); + + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), // 1 NEURO per 1 TRAC + paranetOperatorRewardPercentage: 2_000, // 20% + paranetIncentivizationProposalVotersRewardPercentage: 2_000, // 20% + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 1); + + const neuroAmount = hre.ethers.utils.parseEther('50'); // Less NEURO in the pool + await NeuroERC20.transfer(IncentivesPool.address, neuroAmount); + + const knowledgeMiner = accounts[2]; + + // Simulate the knowledge miner minting a knowledge asset (spending TRAC) + const tokenAmount = '100'; // 100 TRAC + await createParanetKnowledgeAsset(knowledgeMiner, paranetKAStorageContract, paranetKATokenId, 2, tokenAmount); + + // Act + const claimableReward = await IncentivesPool.connect(knowledgeMiner).getClaimableKnowledgeMinerRewardAmount(); + + // Assert + const minersRewardPercentage = + PERCENTAGE_SCALING_FACTOR - + incentivesPoolParams.paranetOperatorRewardPercentage - + incentivesPoolParams.paranetIncentivizationProposalVotersRewardPercentage; + + // Expected reward based on NEURO balance and miners' percentage + const minersRewardLimit = neuroAmount.mul(minersRewardPercentage).div(PERCENTAGE_SCALING_FACTOR); + expect(claimableReward).to.equal(minersRewardLimit); + + const initialNeuroBalance = await NeuroERC20.balanceOf(knowledgeMiner.address); + + // Claim the reward + await IncentivesPool.connect(knowledgeMiner).claimKnowledgeMinerReward(); + + // Check balances + const finalNeuroBalance = await NeuroERC20.balanceOf(knowledgeMiner.address); + expect(finalNeuroBalance.sub(initialNeuroBalance)).to.equal(minersRewardLimit); + + const claimedNeuro = await IncentivesPool.minerClaimedNeuro(knowledgeMiner.address); + expect(claimedNeuro).to.equal(minersRewardLimit); + + const totalMinersClaimedNeuro = await IncentivesPool.totalMinersClaimedNeuro(); + expect(totalMinersClaimedNeuro).to.equal(minersRewardLimit); + + // Unrewarded TRAC should not be zero since they couldn't claim the full amount + const newUnrewardedTracSpent = await ParanetKnowledgeMinersRegistry.getUnrewardedTracSpent( + knowledgeMiner.address, + paranetId, + ); + expect(newUnrewardedTracSpent).to.be.gt(0); + }); + + it('Only authorized users can claim rewards', async () => { + const { paranetKAStorageContract, paranetKATokenId } = await registerParanet(accounts, Paranet, 1); + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), + paranetOperatorRewardPercentage: 1_000, + paranetIncentivizationProposalVotersRewardPercentage: 1_000, + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 1); + + const unauthorizedUser = accounts[5]; + + await expect(IncentivesPool.connect(unauthorizedUser).claimKnowledgeMinerReward()).to.be.revertedWith( + 'Fn can only be used by K-Miners', + ); + + await expect(IncentivesPool.connect(unauthorizedUser).claimParanetOperatorReward()).to.be.revertedWith( + 'Fn can only be used by operator', + ); + + await expect(IncentivesPool.connect(unauthorizedUser).claimIncentivizationProposalVoterReward()).to.be.revertedWith( + 'Fn can only be used by voter', + ); + }); + + it('Emission multiplier update process works correctly', async () => { + const { paranetKAStorageContract, paranetKATokenId } = await registerParanet(accounts, Paranet, 1); + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), + paranetOperatorRewardPercentage: 1_000, + paranetIncentivizationProposalVotersRewardPercentage: 1_000, + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 1); + + // Initiate an emission multiplier update + const newMultiplier = hre.ethers.utils.parseEther('2'); // New multiplier + await IncentivesPool.connect(accounts[0]).initiateNeuroEmissionMultiplierUpdate(newMultiplier); + + // Check that the update is scheduled + const neuroEmissionMultipliers = await IncentivesPool.getNeuroEmissionMultipliers(); + expect(neuroEmissionMultipliers.length).to.equal(2); + expect(neuroEmissionMultipliers[1].multiplier).to.equal(newMultiplier); + expect(neuroEmissionMultipliers[1].finalized).to.equal(false); + + // Try to finalize before delay period + await expect(IncentivesPool.connect(accounts[0]).finalizeNeuroEmissionMultiplierUpdate()).to.be.revertedWith( + 'Delay period not yet passed', + ); + + // Increase time to pass the delay + const delay = await IncentivesPool.neuroEmissionMultiplierUpdateDelay(); + await time.increase(delay.toNumber() + 1); + + // Finalize the update + await IncentivesPool.connect(accounts[0]).finalizeNeuroEmissionMultiplierUpdate(); + + // Check that the multiplier is updated + const updatedMultipliers = await IncentivesPool.getNeuroEmissionMultipliers(); + expect(updatedMultipliers[1].finalized).to.equal(true); + }); + + it('Cannot add voters exceeding MAX_CUMULATIVE_VOTERS_WEIGHT', async () => { + const { paranetKAStorageContract, paranetKATokenId } = await registerParanet(accounts, Paranet, 1); + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), + paranetOperatorRewardPercentage: 1_000, + paranetIncentivizationProposalVotersRewardPercentage: 1_000, + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 1); + + // Try to add voters exceeding the maximum cumulative weight + const voters = []; + for (let i = 0; i < 11; i++) { + voters.push({ addr: accounts[i].address, weight: 1_000 }); + } + + await expect(IncentivesPool.connect(accounts[0]).addVoters(voters)).to.be.revertedWith( + 'Cumulative weight is too big', + ); + }); + + it('Emission multiplier change impacts rewards correctly', async () => { + const { paranetKAStorageContract, paranetKATokenId, paranetId } = await registerParanet(accounts, Paranet, 10); + + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), // Initial multiplier + paranetOperatorRewardPercentage: 1_000, // 10% + paranetIncentivizationProposalVotersRewardPercentage: 1_000, // 10% + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 10); + + const neuroAmount = hre.ethers.utils.parseEther('1000'); + await NeuroERC20.transfer(IncentivesPool.address, neuroAmount); + + const knowledgeMiner = accounts[20]; + + // Knowledge miner mints a knowledge asset before emission multiplier change + await createParanetKnowledgeAsset(knowledgeMiner, paranetKAStorageContract, paranetKATokenId, 20, '50'); + + // Initiate and finalize emission multiplier update + const newMultiplier = hre.ethers.utils.parseEther('2'); // New multiplier + await IncentivesPool.connect(accounts[0]).initiateNeuroEmissionMultiplierUpdate(newMultiplier); + const delay = await IncentivesPool.neuroEmissionMultiplierUpdateDelay(); + await time.increase(delay.toNumber() + 1); + await IncentivesPool.connect(accounts[0]).finalizeNeuroEmissionMultiplierUpdate(); + + // Knowledge miner mints another knowledge asset after emission multiplier change + await createParanetKnowledgeAsset(knowledgeMiner, paranetKAStorageContract, paranetKATokenId, 21, '50'); + + // Act + const claimableReward = await IncentivesPool.connect(knowledgeMiner).getClaimableKnowledgeMinerRewardAmount(); + + // Assert + const minersRewardPercentage = + PERCENTAGE_SCALING_FACTOR - + incentivesPoolParams.paranetOperatorRewardPercentage - + incentivesPoolParams.paranetIncentivizationProposalVotersRewardPercentage; + + // Expected reward is the sum of rewards calculated with new multiplier + const rewardBeforeChange = hre.ethers.utils + .parseEther('50') // TRAC spent before change + .mul(newMultiplier) // New multiplier + .div(EMISSION_MULTIPLIER_SCALING_FACTOR) + .mul(minersRewardPercentage) + .div(PERCENTAGE_SCALING_FACTOR); + + const rewardAfterChange = hre.ethers.utils + .parseEther('50') // TRAC spent after change + .mul(newMultiplier) // New multiplier + .div(EMISSION_MULTIPLIER_SCALING_FACTOR) + .mul(minersRewardPercentage) + .div(PERCENTAGE_SCALING_FACTOR); + + const expectedReward = rewardBeforeChange.add(rewardAfterChange); + expect(claimableReward).to.equal(expectedReward); + + const initialNeuroBalance = await NeuroERC20.balanceOf(knowledgeMiner.address); + + // Claim the reward + await IncentivesPool.connect(knowledgeMiner).claimKnowledgeMinerReward(); + + // Check balances + const finalNeuroBalance = await NeuroERC20.balanceOf(knowledgeMiner.address); + expect(finalNeuroBalance.sub(initialNeuroBalance)).to.equal(expectedReward); + + const newUnrewardedTracSpent = await ParanetKnowledgeMinersRegistry.getUnrewardedTracSpent( + knowledgeMiner.address, + paranetId, + ); + expect(newUnrewardedTracSpent).to.equal(0); + }); + + it('Multiple knowledge miners receive correct rewards proportionally', async () => { + const { paranetKAStorageContract, paranetKATokenId, paranetId } = await registerParanet(accounts, Paranet, 11); + + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), + paranetOperatorRewardPercentage: 1_000, + paranetIncentivizationProposalVotersRewardPercentage: 1_000, + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 11); + + const neuroAmount = hre.ethers.utils.parseEther('2000'); + await NeuroERC20.transfer(IncentivesPool.address, neuroAmount); + + const knowledgeMiner1 = accounts[21]; + const knowledgeMiner2 = accounts[22]; + const knowledgeMiner3 = accounts[23]; + + // Knowledge miners mint knowledge assets with different TRAC amounts + await createParanetKnowledgeAsset(knowledgeMiner1, paranetKAStorageContract, paranetKATokenId, 30, '100'); // 100 TRAC + await createParanetKnowledgeAsset(knowledgeMiner2, paranetKAStorageContract, paranetKATokenId, 31, '200'); // 200 TRAC + await createParanetKnowledgeAsset(knowledgeMiner3, paranetKAStorageContract, paranetKATokenId, 32, '300'); // 300 TRAC + + // Act & Assert for each miner + for (const miner of [knowledgeMiner1, knowledgeMiner2, knowledgeMiner3]) { + const claimableReward = await IncentivesPool.connect(miner).getClaimableKnowledgeMinerRewardAmount(); + + const minersRewardPercentage = + PERCENTAGE_SCALING_FACTOR - + incentivesPoolParams.paranetOperatorRewardPercentage - + incentivesPoolParams.paranetIncentivizationProposalVotersRewardPercentage; + + const unrewardedTracSpent = await ParanetKnowledgeMinersRegistry.getUnrewardedTracSpent(miner.address, paranetId); + + const expectedReward = unrewardedTracSpent + .mul(incentivesPoolParams.tracToNeuroEmissionMultiplier) + .div(EMISSION_MULTIPLIER_SCALING_FACTOR) + .mul(minersRewardPercentage) + .div(PERCENTAGE_SCALING_FACTOR); + + expect(claimableReward).to.equal(expectedReward); + + const initialNeuroBalance = await NeuroERC20.balanceOf(miner.address); + + // Claim the reward + await IncentivesPool.connect(miner).claimKnowledgeMinerReward(); + + // Check balances + const finalNeuroBalance = await NeuroERC20.balanceOf(miner.address); + expect(finalNeuroBalance.sub(initialNeuroBalance)).to.equal(expectedReward); + + const newUnrewardedTracSpent = await ParanetKnowledgeMinersRegistry.getUnrewardedTracSpent( + miner.address, + paranetId, + ); + expect(newUnrewardedTracSpent).to.equal(0); + } + }); + + it('Cannot adjust voters weights after rewards have been claimed', async () => { + const { paranetKAStorageContract, paranetKATokenId, paranetId } = await registerParanet(accounts, Paranet, 12); + + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), + paranetOperatorRewardPercentage: 1_000, + paranetIncentivizationProposalVotersRewardPercentage: 2_000, + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 12); + + const neuroAmount = hre.ethers.utils.parseEther('1000'); + await NeuroERC20.transfer(IncentivesPool.address, neuroAmount); + + // Simulate knowledge miners minting knowledge assets + const knowledgeMiner = accounts[24]; + await createParanetKnowledgeAsset(knowledgeMiner, paranetKAStorageContract, paranetKATokenId, 40, '100'); + + // Add voters with initial weights + const voter1 = accounts[25]; + const voter2 = accounts[26]; + const voters = [ + { addr: voter1.address, weight: 5_000 }, // 50% + { addr: voter2.address, weight: 5_000 }, // 50% + ]; + await IncentivesPool.connect(accounts[0]).addVoters(voters); + + // Voters claim rewards + await IncentivesPool.connect(voter1).claimIncentivizationProposalVoterReward(); + await IncentivesPool.connect(voter2).claimIncentivizationProposalVoterReward(); + + // Attempt to adjust weights after voters have claimed rewards + await expect(IncentivesPool.connect(accounts[0]).removeVoters(2)).to.be.revertedWith('Cannot modify voters list'); + + const adjustedVoters = [ + { addr: voter1.address, weight: 7_000 }, // 70% + { addr: voter2.address, weight: 3_000 }, // 30% + ]; + await expect(IncentivesPool.connect(accounts[0]).addVoters(adjustedVoters)).to.be.revertedWith( + 'Cannot modify voters list', + ); + + // Knowledge miner mints more knowledge assets + await createParanetKnowledgeAsset(knowledgeMiner, paranetKAStorageContract, paranetKATokenId, 41, '100'); + + // Voters claim rewards again + const initialVoter1Balance = await NeuroERC20.balanceOf(voter1.address); + const initialVoter2Balance = await NeuroERC20.balanceOf(voter2.address); + + await IncentivesPool.connect(voter1).claimIncentivizationProposalVoterReward(); + await IncentivesPool.connect(voter2).claimIncentivizationProposalVoterReward(); + + const finalVoter1Balance = await NeuroERC20.balanceOf(voter1.address); + const finalVoter2Balance = await NeuroERC20.balanceOf(voter2.address); + + const additionalVoter1Reward = finalVoter1Balance.sub(initialVoter1Balance); + const additionalVoter2Reward = finalVoter2Balance.sub(initialVoter2Balance); + + // Expected rewards based on initial weights (since weights cannot be adjusted) + const votersRewardPercentage = incentivesPoolParams.paranetIncentivizationProposalVotersRewardPercentage; + const additionalKnowledgeValue = hre.ethers.utils.parseEther('100'); // Additional TRAC spent + const totalVotersReward = additionalKnowledgeValue + .mul(incentivesPoolParams.tracToNeuroEmissionMultiplier) + .div(EMISSION_MULTIPLIER_SCALING_FACTOR) + .mul(votersRewardPercentage) + .div(PERCENTAGE_SCALING_FACTOR); + + const expectedVoter1AdditionalReward = totalVotersReward.mul(5_000).div(MAX_CUMULATIVE_VOTERS_WEIGHT); + const expectedVoter2AdditionalReward = totalVotersReward.mul(5_000).div(MAX_CUMULATIVE_VOTERS_WEIGHT); + + expect(additionalVoter1Reward).to.equal(expectedVoter1AdditionalReward); + expect(additionalVoter2Reward).to.equal(expectedVoter2AdditionalReward); + }); + + it('Cannot set invalid reward percentages', async () => { + const { paranetKAStorageContract, paranetKATokenId } = await registerParanet(accounts, Paranet, 13); + + // Sum of percentages exceeds 100% + const invalidIncentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), + paranetOperatorRewardPercentage: 6_000, // 60% + paranetIncentivizationProposalVotersRewardPercentage: 5_000, // 50% + }; + + await expect(deployERC20NeuroIncentivesPool(accounts, invalidIncentivesPoolParams, 13)).to.be.revertedWith( + 'Invalid rewards ratio', + ); + + // Valid percentages + const validIncentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), + paranetOperatorRewardPercentage: 4_000, // 40% + paranetIncentivizationProposalVotersRewardPercentage: 5_000, // 50% + }; + + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, validIncentivesPoolParams, 13); + expect(IncentivesPool.address).to.be.properAddress; + }); + + it('Total NEURO received calculation is accurate', async () => { + const { paranetKAStorageContract, paranetKATokenId } = await registerParanet(accounts, Paranet, 14); + + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), + paranetOperatorRewardPercentage: 2_000, + paranetIncentivizationProposalVotersRewardPercentage: 1_000, + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 14); + + const neuroAmount1 = hre.ethers.utils.parseEther('500'); + const neuroAmount2 = hre.ethers.utils.parseEther('700'); + await NeuroERC20.transfer(IncentivesPool.address, neuroAmount1); + await NeuroERC20.transfer(IncentivesPool.address, neuroAmount2); + + const totalNeuroReceived = await IncentivesPool.totalNeuroReceived(); + expect(totalNeuroReceived).to.equal(neuroAmount1.add(neuroAmount2)); + + // Knowledge miner claims reward + const knowledgeMiner = accounts[30]; + await createParanetKnowledgeAsset(knowledgeMiner, paranetKAStorageContract, paranetKATokenId, 50, '100'); + + await IncentivesPool.connect(knowledgeMiner).claimKnowledgeMinerReward(); + + const totalNeuroReceivedAfterClaim = await IncentivesPool.totalNeuroReceived(); + expect(totalNeuroReceivedAfterClaim).to.equal(neuroAmount1.add(neuroAmount2)); + }); + + it('Operator can claim the correct NEURO reward', async () => { + const { paranetKAStorageContract, paranetKATokenId, paranetId } = await registerParanet(accounts, Paranet, 1); + + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), // 1 NEURO per 1 TRAC + paranetOperatorRewardPercentage: 2_000, // 20% + paranetIncentivizationProposalVotersRewardPercentage: 1_000, // 10% + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 1); + + const neuroAmount = hre.ethers.utils.parseEther('1000'); + await NeuroERC20.transfer(IncentivesPool.address, neuroAmount); + + // Simulate knowledge miners minting knowledge assets + const knowledgeMiner1 = accounts[2]; + await createParanetKnowledgeAsset(knowledgeMiner1, paranetKAStorageContract, paranetKATokenId, 2, '100'); + + const knowledgeMiner2 = accounts[3]; + await createParanetKnowledgeAsset(knowledgeMiner2, paranetKAStorageContract, paranetKATokenId, 3, '50'); + + // Act + const claimableOperatorReward = await IncentivesPool.connect( + accounts[100 + 1], + ).getClaimableParanetOperatorRewardAmount(); + + // Assert + const operatorRewardPercentage = incentivesPoolParams.paranetOperatorRewardPercentage; + const totalKnowledgeValue = await ParanetsRegistry.getCumulativeKnowledgeValue(paranetId); + + const expectedOperatorReward = totalKnowledgeValue + .mul(incentivesPoolParams.tracToNeuroEmissionMultiplier) + .div(EMISSION_MULTIPLIER_SCALING_FACTOR) + .mul(operatorRewardPercentage) + .div(PERCENTAGE_SCALING_FACTOR); + + expect(claimableOperatorReward).to.equal(expectedOperatorReward); + + const initialNeuroBalance = await NeuroERC20.balanceOf(accounts[100 + 1].address); + + // Claim the reward + await IncentivesPool.connect(accounts[100 + 1]).claimParanetOperatorReward(); + + // Check balances + const finalNeuroBalance = await NeuroERC20.balanceOf(accounts[100 + 1].address); + expect(finalNeuroBalance.sub(initialNeuroBalance)).to.equal(expectedOperatorReward); + + const claimedOperatorNeuro = await IncentivesPool.operatorClaimedNeuro(accounts[100 + 1].address); + expect(claimedOperatorNeuro).to.equal(expectedOperatorReward); + + const totalOperatorsClaimedNeuro = await IncentivesPool.totalOperatorsClaimedNeuro(); + expect(totalOperatorsClaimedNeuro).to.equal(expectedOperatorReward); + }); + + it('Voters can claim rewards proportional to their weights', async () => { + const { paranetKAStorageContract, paranetKATokenId, paranetId } = await registerParanet(accounts, Paranet, 1); + + const incentivesPoolParams = { + paranetKAStorageContract, + paranetKATokenId, + tracToNeuroEmissionMultiplier: hre.ethers.utils.parseEther('1'), // 1 NEURO per 1 TRAC + paranetOperatorRewardPercentage: 1_000, // 10% + paranetIncentivizationProposalVotersRewardPercentage: 2_000, // 20% + }; + const IncentivesPool = await deployERC20NeuroIncentivesPool(accounts, incentivesPoolParams, 1); + + const neuroAmount = hre.ethers.utils.parseEther('1000'); + await NeuroERC20.transfer(IncentivesPool.address, neuroAmount); + + // Simulate knowledge miners minting knowledge assets + const knowledgeMiner1 = accounts[2]; + await createParanetKnowledgeAsset(knowledgeMiner1, paranetKAStorageContract, paranetKATokenId, 2, '100'); + + const knowledgeMiner2 = accounts[3]; + await createParanetKnowledgeAsset(knowledgeMiner2, paranetKAStorageContract, paranetKATokenId, 3, '50'); + + // Add voters + const voter1 = accounts[4]; + const voter2 = accounts[5]; + const voters = [ + { addr: voter1.address, weight: 6_000 }, // 60% + { addr: voter2.address, weight: 4_000 }, // 40% + ]; + await IncentivesPool.connect(accounts[0]).addVoters(voters); + + // Act + const claimableRewardVoter1 = await IncentivesPool.connect(voter1).getClaimableProposalVoterRewardAmount(); + const claimableRewardVoter2 = await IncentivesPool.connect(voter2).getClaimableProposalVoterRewardAmount(); + + // Assert + const votersRewardPercentage = incentivesPoolParams.paranetIncentivizationProposalVotersRewardPercentage; + const totalKnowledgeValue = await ParanetsRegistry.getCumulativeKnowledgeValue(paranetId); + + const totalVotersReward = totalKnowledgeValue + .mul(incentivesPoolParams.tracToNeuroEmissionMultiplier) + .div(EMISSION_MULTIPLIER_SCALING_FACTOR) + .mul(votersRewardPercentage) + .div(PERCENTAGE_SCALING_FACTOR); + + const expectedRewardVoter1 = totalVotersReward.mul(6_000).div(MAX_CUMULATIVE_VOTERS_WEIGHT); + const expectedRewardVoter2 = totalVotersReward.mul(4_000).div(MAX_CUMULATIVE_VOTERS_WEIGHT); + + expect(claimableRewardVoter1).to.equal(expectedRewardVoter1); + expect(claimableRewardVoter2).to.equal(expectedRewardVoter2); + + const voter1InitialBalance = await NeuroERC20.balanceOf(voter1.address); + const voter2InitialBalance = await NeuroERC20.balanceOf(voter2.address); + + // Claim rewards + await IncentivesPool.connect(voter1).claimIncentivizationProposalVoterReward(); + await IncentivesPool.connect(voter2).claimIncentivizationProposalVoterReward(); + + // Check balances + const voter1FinalBalance = await NeuroERC20.balanceOf(voter1.address); + const voter2FinalBalance = await NeuroERC20.balanceOf(voter2.address); + + expect(voter1FinalBalance.sub(voter1InitialBalance)).to.equal(expectedRewardVoter1); + expect(voter2FinalBalance.sub(voter2InitialBalance)).to.equal(expectedRewardVoter2); + }); + + // Helper functions + async function registerParanet(accounts: SignerWithAddress[], Paranet: Paranet, number: number) { + const assetInputArgs = { + assertionId: getHashFromNumber(number), + size: 3, + triplesNumber: 1, + chunksNumber: 1, + epochsNumber: 5, + tokenAmount: hre.ethers.utils.parseEther('105'), + scoreFunctionId: 2, + immutable_: false, + }; + + await Token.connect(accounts[100 + number]).increaseAllowance( + ServiceAgreementV1.address, + assetInputArgs.tokenAmount, + ); + const tx = await ContentAssetV2.connect(accounts[100 + number]).createAsset(assetInputArgs); + const receipt = await tx.wait(); + + const paranetKAStorageContract = ContentAssetStorageV2.address; + const paranetKATokenId = Number(receipt.logs[0].topics[3]); + const paranetName = 'Test paranet 1'; + const paranetDescription = 'Description of Test Paranet'; + + await Paranet.connect(accounts[100 + number]).registerParanet( + paranetKAStorageContract, + paranetKATokenId, + paranetName, + paranetDescription, + ); + + return { + paranetKAStorageContract, + paranetKATokenId, + paranetId: hre.ethers.utils.keccak256( + hre.ethers.utils.solidityPack(['address', 'uint256'], [paranetKAStorageContract, paranetKATokenId]), + ), + }; + } + + async function deployERC20NeuroIncentivesPool( + accounts: SignerWithAddress[], + incentivesPoolParams: IncentivizationPoolParameters, + number: number, + ) { + const tx = await ParanetIncentivesPoolFactory.connect(accounts[100 + number]).deployNeuroIncentivesPool( + false, + incentivesPoolParams.paranetKAStorageContract, + incentivesPoolParams.paranetKATokenId, + incentivesPoolParams.tracToNeuroEmissionMultiplier, + incentivesPoolParams.paranetOperatorRewardPercentage, + incentivesPoolParams.paranetIncentivizationProposalVotersRewardPercentage, + ); + const receipt = await tx.wait(); + + const IncentivesPool = await hre.ethers.getContractAt( + 'ParanetNeuroIncentivesPool', + receipt.events?.[0].args?.incentivesPool.addr, + ); + + return IncentivesPool; + } + + async function createParanetKnowledgeAsset( + knowledgeMinerAccount: SignerWithAddress, + paranetKAStorageContract: string, + paranetKATokenId: number, + assertionIdNumber: number, + tokenAmount: string, + ) { + const assetInputArgs = { + assertionId: getHashFromNumber(assertionIdNumber), + size: 3, + triplesNumber: 1, + chunksNumber: 1, + epochsNumber: 5, + tokenAmount: hre.ethers.utils.parseEther(tokenAmount), + scoreFunctionId: 2, + immutable_: false, + }; + + await Token.connect(knowledgeMinerAccount).increaseAllowance( + ServiceAgreementV1.address, + assetInputArgs.tokenAmount, + ); + + await Paranet.connect(knowledgeMinerAccount).mintKnowledgeAsset( + paranetKAStorageContract, + paranetKATokenId, + assetInputArgs, + ); + } + + function getHashFromNumber(number: number) { + return hre.ethers.utils.keccak256(hre.ethers.utils.solidityPack(['uint256'], [number])); + } +}); diff --git a/utils/helpers.ts b/utils/helpers.ts index 09d25909..56d87d4d 100644 --- a/utils/helpers.ts +++ b/utils/helpers.ts @@ -155,7 +155,7 @@ export class Helpers { deployer, ); - if (this.hasFunction(nameInHub, 'initialize')) { + if (this.hasFunction(newContractName, 'initialize')) { // TODO: Reinitialize only if any dependency contract was redeployed this.contractsForReinitialization.push(contractInstance.address); } @@ -211,9 +211,9 @@ export class Helpers { } } - if (this.hasFunction(nameInHub, 'initialize')) { + if (this.hasFunction(newContractName, 'initialize')) { if ((setContractInHub || setAssetStorageInHub) && this.hre.network.config.environment === 'development') { - const newContractInterface = new this.hre.ethers.utils.Interface(this.getAbi(nameInHub)); + const newContractInterface = new this.hre.ethers.utils.Interface(this.getAbi(newContractName)); const initializeTx = await HubController.forwardCall( newContract.address, newContractInterface.encodeFunctionData('initialize'), @@ -224,7 +224,7 @@ export class Helpers { } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion - await this.updateDeploymentsJson(nameInHub, newContract.address, newContract.receipt!.blockNumber); + await this.updateDeploymentsJson(newContractName, nameInHub, newContract.address, newContract.receipt!.blockNumber); if (this.hre.network.config.environment !== 'development') { this.saveDeploymentsJson('deployments'); @@ -338,8 +338,13 @@ export class Helpers { this.contractDeployments = { contracts: {} }; } - public async updateDeploymentsJson(newContractName: string, newContractAddress: string, deploymentBlock: number) { - const contractABI = this.getAbi(newContractName); + public async updateDeploymentsJson( + abiName: string, + newContractName: string, + newContractAddress: string, + deploymentBlock: number, + ) { + const contractABI = this.getAbi(abiName); const isVersionedContract = contractABI.some( (abiEntry) => abiEntry.type === 'function' && abiEntry.name === 'version', ); @@ -347,7 +352,7 @@ export class Helpers { let contractVersion; if (isVersionedContract) { - const VersionedContract = await this.hre.ethers.getContractAt(newContractName, newContractAddress); + const VersionedContract = await this.hre.ethers.getContractAt(abiName, newContractAddress); contractVersion = await VersionedContract.version(); } else { contractVersion = null;