From 8d2fbd45781a1c2bd0353ca8cebbe2cb28d6fa64 Mon Sep 17 00:00:00 2001 From: Alex Rea Date: Sat, 10 Jun 2023 21:10:57 +0100 Subject: [PATCH] Store complement of skill, allow mining reward scaling --- contracts/colony/Colony.sol | 16 ++ contracts/colony/ColonyAuthority.sol | 1 + contracts/colony/ColonyDataTypes.sol | 2 + contracts/colony/IMetaColony.sol | 5 + contracts/colonyNetwork/ColonyNetwork.sol | 13 +- .../colonyNetwork/ColonyNetworkDataTypes.sol | 7 +- .../colonyNetwork/ColonyNetworkMining.sol | 13 +- contracts/colonyNetwork/IColonyNetwork.sol | 5 + docs/interfaces/icolonynetwork.md | 13 ++ docs/interfaces/imetacolony.md | 15 +- test/contracts-network/colony.js | 6 +- .../root-hash-submissions.js | 141 ++++++++++++++++++ 12 files changed, 218 insertions(+), 19 deletions(-) diff --git a/contracts/colony/Colony.sol b/contracts/colony/Colony.sol index a172a49231..6619d228d0 100755 --- a/contracts/colony/Colony.sol +++ b/contracts/colony/Colony.sol @@ -209,6 +209,14 @@ contract Colony is BasicMetaTransaction, Multicall, ColonyStorage, PatriciaTreeP IColonyNetwork(colonyNetworkAddress).setReputationMiningCycleReward(_amount); } + function setReputationMiningCycle(uint256 _amount) public + stoppable + auth + { + IColonyNetwork(colonyNetworkAddress).setReputationMiningCycleReward(_amount); + } + + function addNetworkColonyVersion(uint256 _version, address _resolver) public stoppable auth @@ -334,6 +342,9 @@ contract Colony is BasicMetaTransaction, Multicall, ColonyStorage, PatriciaTreeP sig = bytes4(keccak256("setReputationDecayRate(uint256,uint256)")); colonyAuthority.setRoleCapability(uint8(ColonyRole.Root), address(this), sig, true); + + sig = bytes4(keccak256("setReputationMiningCycleRewardReputationScaling(uint256)")); + colonyAuthority.setRoleCapability(uint8(ColonyRole.Root), address(this), sig, true); } function setTokenReputationRate(address _token, uint256 _rate) public stoppable { @@ -423,4 +434,9 @@ contract Colony is BasicMetaTransaction, Multicall, ColonyStorage, PatriciaTreeP return tokenApprovalTotals[_token]; } + function setReputationMiningCycleRewardReputationScaling(uint256 _factor) public stoppable auth { + require(_factor <= WAD, "colony-invalid-scale-factor"); + IColonyNetwork(colonyNetworkAddress).setReputationMiningCycleRewardReputationScaling(_factor); + emit MiningReputationScalingSet(_factor); + } } diff --git a/contracts/colony/ColonyAuthority.sol b/contracts/colony/ColonyAuthority.sol index 8f635cb0ed..c349e580e1 100644 --- a/contracts/colony/ColonyAuthority.sol +++ b/contracts/colony/ColonyAuthority.sol @@ -133,6 +133,7 @@ contract ColonyAuthority is CommonAuthority { // Added in colony v xxxxx addRoleCapability(ROOT_ROLE, "setDomainReputationScaling(uint256,bool,uint256)"); addRoleCapability(ROOT_ROLE, "setReputationDecayRate(uint256,uint256)"); + addRoleCapability(ROOT_ROLE, "setReputationMiningCycleRewardReputationScaling(uint256)"); } diff --git a/contracts/colony/ColonyDataTypes.sol b/contracts/colony/ColonyDataTypes.sol index 203208abcd..fc79211cd8 100755 --- a/contracts/colony/ColonyDataTypes.sol +++ b/contracts/colony/ColonyDataTypes.sol @@ -351,6 +351,8 @@ interface ColonyDataTypes { event DomainReputationScalingSet(uint256 domainId, bool enabled, uint256 factor); + event MiningReputationScalingSet(uint256 factor); + struct RewardPayoutCycle { // Reputation root hash at the time of reward payout creation bytes32 reputationState; diff --git a/contracts/colony/IMetaColony.sol b/contracts/colony/IMetaColony.sol index 758f95ae95..770fe1ac3a 100644 --- a/contracts/colony/IMetaColony.sol +++ b/contracts/colony/IMetaColony.sol @@ -58,6 +58,11 @@ interface IMetaColony is IColony { /// @param _amount The CLNY awarded per mining cycle to the miners function setReputationMiningCycleReward(uint256 _amount) external; + /// @notice Called to set the total per-cycle reputation scaling factor for the tokens paid out + /// @dev Calls the corresponding function on the ColonyNetwork. + /// @param _factor The scale factor to apply to reputation mining rewards + function setReputationMiningCycleRewardReputationScaling(uint256 _factor) external; + /// @notice Add a new extension/version to the Extensions repository. /// @dev Calls `IColonyNetwork.addExtensionToNetwork`. /// @dev The extension version is queried from the resolver itself. diff --git a/contracts/colonyNetwork/ColonyNetwork.sol b/contracts/colonyNetwork/ColonyNetwork.sol index 80adcc6f67..95f47f4ee3 100644 --- a/contracts/colonyNetwork/ColonyNetwork.sol +++ b/contracts/colonyNetwork/ColonyNetwork.sol @@ -282,26 +282,25 @@ contract ColonyNetwork is ColonyDataTypes, BasicMetaTransaction, ColonyNetworkSt { require(_factor <= WAD, "colony-network-invalid-reputation-scale-factor"); uint256 skillId = IColony(msgSender()).getDomain(_domainId).skillId; - skills[skillId].earnedReputationScaling = _enabled; - skills[skillId].reputationScalingFactor = _factor; + skills[skillId].reputationScalingFactorComplement = WAD - _factor; } function getSkillReputationScaling(uint256 _skillId) public view returns (uint256) { uint256 factor; Skill storage s = skills[_skillId]; - factor = s.earnedReputationScaling ? s.reputationScalingFactor : WAD; + factor = WAD - s.reputationScalingFactorComplement; while (s.nParents > 0) { s = skills[s.parents[0]]; // If reputation scaling is in effect for this skill, then take the value for this skill in to // account. Otherwise, no effect and continue walking up the tree - if (s.earnedReputationScaling) { - if (s.reputationScalingFactor == 0){ - // If scaling is in effect and is 0, we can short circuit - regardless of the rest of the tree + if (s.reputationScalingFactorComplement > 0) { + if (s.reputationScalingFactorComplement == 1){ + // If scaling is in effect and is 0 (because factor = 1 - complement), we can short circuit - regardless of the rest of the tree // the scaling factor will be 0 return 0; } else { - factor = wmul(factor, s.reputationScalingFactor); + factor = wmul(factor, WAD - s.reputationScalingFactorComplement); } } } diff --git a/contracts/colonyNetwork/ColonyNetworkDataTypes.sol b/contracts/colonyNetwork/ColonyNetworkDataTypes.sol index 2663e19af2..a7d6a9a2bc 100755 --- a/contracts/colonyNetwork/ColonyNetworkDataTypes.sol +++ b/contracts/colonyNetwork/ColonyNetworkDataTypes.sol @@ -166,11 +166,8 @@ interface ColonyNetworkDataTypes { bool globalSkill; // `true` for a global skill that is deprecated bool deprecated; - // `true` if global scaling is in effect - bool earnedReputationScaling; - // NB extra storage space available here for more booleans etc - // scaling in effect for reputation earned in this skill - uint256 reputationScalingFactor; + // This is the complement of the reputaiton scaling factor. So the scaling factor is WAD-reputationScalingFactorComplement + uint256 reputationScalingFactorComplement; } struct ENSRecord { diff --git a/contracts/colonyNetwork/ColonyNetworkMining.sol b/contracts/colonyNetwork/ColonyNetworkMining.sol index 6b71be0f4a..99dfb7739f 100644 --- a/contracts/colonyNetwork/ColonyNetworkMining.sol +++ b/contracts/colonyNetwork/ColonyNetworkMining.sol @@ -21,12 +21,13 @@ pragma experimental "ABIEncoderV2"; import "./../common/ERC20Extended.sol"; import "./../common/EtherRouter.sol"; import "./../common/MultiChain.sol"; +import "./../common/ScaleReputation.sol"; import "./../reputationMiningCycle/IReputationMiningCycle.sol"; import "./../tokenLocking/ITokenLocking.sol"; import "./ColonyNetworkStorage.sol"; -contract ColonyNetworkMining is ColonyNetworkStorage, MultiChain { +contract ColonyNetworkMining is ColonyNetworkStorage, MultiChain, ScaleReputation { // TODO: Can we handle a dispute regarding the very first hash that should be set? modifier onlyReputationMiningCycle () { @@ -201,7 +202,8 @@ contract ColonyNetworkMining is ColonyNetworkStorage, MultiChain { stakers, minerWeights, metaColony, - totalMinerRewardPerCycle, + // totalMinerRewardPerCycle, + uint256(scaleReputation(int256(totalMinerRewardPerCycle), WAD - skills[reputationMiningSkillId].reputationScalingFactorComplement)), reputationMiningSkillId ); } @@ -275,6 +277,7 @@ contract ColonyNetworkMining is ColonyNetworkStorage, MultiChain { } function setReputationMiningCycleReward(uint256 _amount) public stoppable calledByMetaColony { + require(_amount < uint256(type(int256).max), "colony-network-too-large-reward"); totalMinerRewardPerCycle = _amount; emit ReputationMiningRewardSet(_amount); @@ -284,6 +287,12 @@ contract ColonyNetworkMining is ColonyNetworkStorage, MultiChain { return totalMinerRewardPerCycle; } + function setReputationMiningCycleRewardReputationScaling(uint256 _factor) public calledByMetaColony stoppable + { + require(_factor <= WAD, "colony-network-invalid-reputation-scale-factor"); + skills[reputationMiningSkillId].reputationScalingFactorComplement = WAD - _factor; + } + uint256 constant UINT192_MAX = 2**192 - 1; // Used for updating the stake timestamp function getNewTimestamp(uint256 _prevWeight, uint256 _currWeight, uint256 _prevTime, uint256 _currTime) internal pure returns (uint256) { diff --git a/contracts/colonyNetwork/IColonyNetwork.sol b/contracts/colonyNetwork/IColonyNetwork.sol index 87f8fcd96f..5992ce47da 100644 --- a/contracts/colonyNetwork/IColonyNetwork.sol +++ b/contracts/colonyNetwork/IColonyNetwork.sol @@ -487,4 +487,9 @@ interface IColonyNetwork is ColonyNetworkDataTypes, IRecovery, IBasicMetaTransac /// @return numerator The numerator of the fraction reputation does down by every reputation cycle /// @return denominator The denominator of the fraction reputation does down by every reputation cycle function getColonyReputationDecayRate(address _colony) external view returns (uint256 numerator, uint256 denominator); + + /// @notice Called to set the total per-cycle reputation scaling factor for the tokens paid out + /// @dev Calls the corresponding function on the ColonyNetwork. + /// @param _factor The scale factor to apply to reputation mining rewards + function setReputationMiningCycleRewardReputationScaling(uint256 _factor) external; } \ No newline at end of file diff --git a/docs/interfaces/icolonynetwork.md b/docs/interfaces/icolonynetwork.md index 572049ec7c..8a17bfc06b 100644 --- a/docs/interfaces/icolonynetwork.md +++ b/docs/interfaces/icolonynetwork.md @@ -1014,6 +1014,19 @@ Called to set the total per-cycle reputation reward, which will be split between |_amount|uint256|The CLNY awarded per mining cycle to the miners +### ▸ `setReputationMiningCycleRewardReputationScaling(uint256 _factor)` + +Called to set the total per-cycle reputation scaling factor for the tokens paid out + +*Note: Calls the corresponding function on the ColonyNetwork.* + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_factor|uint256|The scale factor to apply to reputation mining rewards + + ### ▸ `setReputationRootHash(bytes32 _newHash, uint256 _newNLeaves, address[] memory _stakers)` Set a new Reputation root hash and starts a new mining cycle. Can only be called by the ReputationMiningCycle contract. diff --git a/docs/interfaces/imetacolony.md b/docs/interfaces/imetacolony.md index 7e1a6c6889..19a65d3a8d 100644 --- a/docs/interfaces/imetacolony.md +++ b/docs/interfaces/imetacolony.md @@ -109,4 +109,17 @@ Called to set the total per-cycle reputation reward, which will be split between |Name|Type|Description| |---|---|---| -|_amount|uint256|The CLNY awarded per mining cycle to the miners \ No newline at end of file +|_amount|uint256|The CLNY awarded per mining cycle to the miners + + +### ▸ `setReputationMiningCycleRewardReputationScaling(uint256 _factor)` + +Called to set the total per-cycle reputation scaling factor for the tokens paid out + +*Note: Calls the corresponding function on the ColonyNetwork.* + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_factor|uint256|The scale factor to apply to reputation mining rewards \ No newline at end of file diff --git a/test/contracts-network/colony.js b/test/contracts-network/colony.js index 8348a2350d..254fa08e2b 100755 --- a/test/contracts-network/colony.js +++ b/test/contracts-network/colony.js @@ -555,14 +555,12 @@ contract("Colony", (accounts) => { const domain = await colony.getDomain(1); let skill = await colonyNetwork.getSkill(domain.skillId); - expect(skill.reputationScalingFactor).to.be.eq.BN(WAD.divn(2)); - expect(skill.earnedReputationScaling).to.be.true; + expect(skill.reputationScalingFactorComplement).to.be.eq.BN(WAD.divn(2)); await colony.setDomainReputationScaling(1, false, 0); skill = await colonyNetwork.getSkill(domain.skillId); - expect(skill.reputationScalingFactor).to.be.eq.BN(0); - expect(skill.earnedReputationScaling).to.be.false; + expect(skill.reputationScalingFactorComplement).to.be.eq.BN(WAD); }); it("setting domain reputation scaling to false with a nonzero scale factor fails", async () => { diff --git a/test/reputation-system/root-hash-submissions.js b/test/reputation-system/root-hash-submissions.js index 55b62a73db..592c4fa9c3 100644 --- a/test/reputation-system/root-hash-submissions.js +++ b/test/reputation-system/root-hash-submissions.js @@ -275,6 +275,70 @@ contract("Reputation mining - root hash submissions", (accounts) => { expect(reputationUpdateLogLength).to.eq.BN(2); }); + it("should respect reputation scaling for mining rewards", async () => { + const miningSkillId = 3; + + await metaColony.setReputationMiningCycleReward(WAD.muln(10)); + await metaColony.setReputationMiningCycleRewardReputationScaling(WAD.divn(2)); + const repCycle = await getActiveRepCycle(colonyNetwork); + await forwardTime(MINING_CYCLE_DURATION / 2, this); + + const entryNumber = await getValidEntryNumber(colonyNetwork, MINER1, "0x12345678"); + const entryNumber2 = await getValidEntryNumber(colonyNetwork, MINER1, "0x12345678", entryNumber + 1); + + await repCycle.submitRootHash("0x12345678", 10, "0x00", entryNumber, { from: MINER1 }); + await repCycle.submitRootHash("0x12345678", 10, "0x00", entryNumber2, { from: MINER1 }); + + const nUniqueSubmittedHashes = await repCycle.getNUniqueSubmittedHashes(); + expect(nUniqueSubmittedHashes).to.eq.BN(1); + + await forwardTime(MINING_CYCLE_DURATION / 2 + CHALLENGE_RESPONSE_WINDOW_DURATION + 1, this); + const lockedFor1 = await tokenLocking.getUserLock(clnyToken.address, MINER1); + + await repCycle.confirmNewHash(0, { from: MINER1 }); + const lockedFor1Updated = await tokenLocking.getUserLock(clnyToken.address, MINER1); + + const addr = await colonyNetwork.getReputationMiningCycle(false); + const inactiveRepCycle = await IReputationMiningCycle.at(addr); + + const blockTime = await currentBlockTime(); + const stake = await colonyNetwork.getMiningStake(MINER1); + const mw1 = await colonyNetwork.calculateMinerWeight(blockTime - stake.timestamp, 0); + const mw2 = await colonyNetwork.calculateMinerWeight(blockTime - stake.timestamp, 1); + + const r1 = await WAD.muln(10) + .mul(mw1.mul(WAD).div(mw1.add(mw2))) + .div(WAD); + + const r2 = await WAD.muln(10) + .mul(mw2.mul(WAD).div(mw1.add(mw2))) + .div(WAD); + + // Check they've been awarded the tokens + const m1Reward = new BN(lockedFor1Updated.balance).sub(new BN(lockedFor1.balance)); + expect(m1Reward, "Account was not rewarded properly").to.be.eq.BN(r1.add(r2)); + + // Check that they will be getting the reputation owed to them. + let repLogEntryMiner = await inactiveRepCycle.getReputationUpdateLogEntry(0); + expect(repLogEntryMiner.user).to.equal(MINER1); + expect(repLogEntryMiner.amount).to.eq.BN(r1.divn(2)); + expect(repLogEntryMiner.skillId).to.eq.BN(miningSkillId); + expect(repLogEntryMiner.colony).to.equal(metaColony.address); + expect(repLogEntryMiner.nUpdates).to.eq.BN(4); + expect(repLogEntryMiner.nPreviousUpdates).to.be.zero; + + repLogEntryMiner = await inactiveRepCycle.getReputationUpdateLogEntry(1); + expect(repLogEntryMiner.user).to.equal(MINER1); + expect(repLogEntryMiner.amount).to.eq.BN(r2.divn(2)); + expect(repLogEntryMiner.skillId).to.eq.BN(miningSkillId); + expect(repLogEntryMiner.colony).to.equal(metaColony.address); + expect(repLogEntryMiner.nUpdates).to.eq.BN(4); + expect(repLogEntryMiner.nPreviousUpdates).to.eq.BN(4); + + const reputationUpdateLogLength = await inactiveRepCycle.getReputationUpdateLogLength(); + expect(reputationUpdateLogLength).to.eq.BN(2); + }); + it("should only allow 12 entries to back a single hash in each cycle", async () => { const repCycle = await getActiveRepCycle(colonyNetwork); await forwardTime(MINING_CYCLE_DURATION - 600, this); @@ -767,6 +831,83 @@ contract("Reputation mining - root hash submissions", (accounts) => { expect(reputationUpdateLogLength).to.eq.BN(2); }); + it("should scale staking rewards by the scaling factor set for miners", async () => { + const miningSkillId = 3; + + await metaColony.setReputationMiningCycleReward(WAD.muln(10)); + await metaColony.setReputationMiningCycleRewardReputationScaling(WAD.divn(2)); + await advanceMiningCycleNoContest({ colonyNetwork, test: this }); + await clnyToken.burn(REWARD, { from: MINER1 }); + + const repCycle = await getActiveRepCycle(colonyNetwork); + await forwardTime(MINING_CYCLE_DURATION / 2, this); + + const entryNumber = await getValidEntryNumber(colonyNetwork, MINER1, "0x12345678"); + const entryNumber2 = await getValidEntryNumber(colonyNetwork, MINER2, "0x12345678"); + + await repCycle.submitRootHash("0x12345678", 10, "0x00", entryNumber, { from: MINER1 }); + await repCycle.submitRootHash("0x12345678", 10, "0x00", entryNumber2, { from: MINER2 }); + + const lockedFor1 = await tokenLocking.getUserLock(clnyToken.address, MINER1); + const lockedFor2 = await tokenLocking.getUserLock(clnyToken.address, MINER2); + + await forwardTime(MINING_CYCLE_DURATION / 2 + CHALLENGE_RESPONSE_WINDOW_DURATION, this); + await forwardTime(MINING_CYCLE_DURATION / 2 + CHALLENGE_RESPONSE_WINDOW_DURATION + 1, this); + + await repCycle.confirmNewHash(0, { from: MINER1 }); + + const blockTime = await currentBlockTime(); + const stake1 = await colonyNetwork.getMiningStake(MINER1); + const stake2 = await colonyNetwork.getMiningStake(MINER2); + const mw1 = await colonyNetwork.calculateMinerWeight(blockTime - stake1.timestamp, 0); + const mw2 = await colonyNetwork.calculateMinerWeight(blockTime - stake2.timestamp, 1); + + const r1 = await WAD.muln(10) + .mul(mw1.mul(WAD).div(mw1.add(mw2))) + .div(WAD); + + const r2 = await WAD.muln(10) + .mul(mw2.mul(WAD).div(mw1.add(mw2))) + .div(WAD); + + // Check that they have had their balance increase + const lockedFor1Updated = await tokenLocking.getUserLock(clnyToken.address, MINER1); + const lockedFor2Updated = await tokenLocking.getUserLock(clnyToken.address, MINER2); + // More than half of the reward + const m1Reward = new BN(lockedFor1Updated.balance).sub(new BN(lockedFor1.balance)); + expect(m1Reward).to.eq.BN(r1); + // Less than half of the reward + const m2Reward = new BN(lockedFor2Updated.balance).sub(new BN(lockedFor2.balance)); + expect(m2Reward).to.eq.BN(r2); + expect(m1Reward.add(m2Reward)).to.be.lte.BN(WAD.muln(10)); + // The first 18 significant figures should be correct, and they are 19 significant + // figures long. The biggest possible error in the sum is therefore 18 wei. + expect(WAD.muln(10).sub(m1Reward).sub(m2Reward).abs()).to.be.lte.BN(new BN(18)); + + const addr = await colonyNetwork.getReputationMiningCycle(false); + const inactiveRepCycle = await IReputationMiningCycle.at(addr); + + // Check that they will be getting the reputation owed to them. + let repLogEntryMiner = await inactiveRepCycle.getReputationUpdateLogEntry(0); + expect(repLogEntryMiner.user).to.equal(MINER1); + expect(repLogEntryMiner.amount).to.eq.BN(r1.divn(2)); + expect(repLogEntryMiner.skillId).to.eq.BN(miningSkillId); + expect(repLogEntryMiner.colony).to.equal(metaColony.address); + expect(repLogEntryMiner.nUpdates).to.eq.BN(4); + expect(repLogEntryMiner.nPreviousUpdates).to.be.zero; + + repLogEntryMiner = await inactiveRepCycle.getReputationUpdateLogEntry(1); + expect(repLogEntryMiner.user).to.equal(MINER2); + expect(repLogEntryMiner.amount).to.eq.BN(r2.divn(2)); + expect(repLogEntryMiner.skillId).to.eq.BN(miningSkillId); + expect(repLogEntryMiner.colony).to.equal(metaColony.address); + expect(repLogEntryMiner.nUpdates).to.eq.BN(4); + expect(repLogEntryMiner.nPreviousUpdates).to.eq.BN(4); + + const reputationUpdateLogLength = await inactiveRepCycle.getReputationUpdateLogLength(); + expect(reputationUpdateLogLength).to.eq.BN(2); + }); + it("should be able to complete a cycle and claim rewards even if CLNY has been locked", async () => { await metaColony.setReputationMiningCycleReward(WAD.muln(10)); await metaColony.mintTokens(WAD);