Skip to content

Commit

Permalink
Merge pull request #812 from JoinColony/feat/reward-users-for-dispute
Browse files Browse the repository at this point in the history
Reward users for responding during dispute
  • Loading branch information
kronosapiens authored May 29, 2020
2 parents d357dcc + fe10cee commit 092c617
Show file tree
Hide file tree
Showing 24 changed files with 595 additions and 142 deletions.
2 changes: 1 addition & 1 deletion contracts/colonyNetwork/ColonyNetworkDataTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ contract ColonyNetworkDataTypes {
/// @param label The label registered
event ColonyLabelRegistered(address indexed colony, bytes32 label);

event ReputationMinerPenalised(address miner, address beneficiary, uint256 tokensLost);
event ReputationMinerPenalised(address miner, uint256 tokensLost);

struct Skill {
// total number of parent skills
Expand Down
32 changes: 22 additions & 10 deletions contracts/colonyNetwork/ColonyNetworkMining.sol
Original file line number Diff line number Diff line change
Expand Up @@ -183,21 +183,31 @@ contract ColonyNetworkMining is ColonyNetworkStorage {
);
}

function punishStakers(address[] memory _stakers, address _beneficiary, uint256 _amount) public stoppable onlyReputationMiningCycle {
function punishStakers(address[] memory _stakers, uint256 _amount) public stoppable onlyReputationMiningCycle {
address clnyToken = IMetaColony(metaColony).getToken();
uint256 lostStake;
// Passing an array so that we don't incur the EtherRouter overhead for each staker if we looped over
// it in ReputationMiningCycle.invalidateHash;
for (uint256 i = 0; i < _stakers.length; i++) {
lostStake = min(ITokenLocking(tokenLocking).getObligation(_stakers[i], clnyToken, address(this)), _amount);
ITokenLocking(tokenLocking).transferStake(_stakers[i], lostStake, clnyToken, _beneficiary);

ITokenLocking(tokenLocking).transferStake(_stakers[i], lostStake, clnyToken, address(this));
// TODO: Lose rep?

emit ReputationMinerPenalised(_stakers[i], _beneficiary, lostStake);
emit ReputationMinerPenalised(_stakers[i], lostStake);
}
}

function reward(address _recipient, uint256 _amount) public stoppable onlyReputationMiningCycle {
// TODO: Gain rep?
pendingMiningRewards[_recipient] = add(pendingMiningRewards[_recipient], _amount);
}

function claimMiningReward(address _recipient) public stoppable {
address clnyToken = IMetaColony(metaColony).getToken();
uint256 amount = pendingMiningRewards[_recipient];
pendingMiningRewards[_recipient] = 0;
ITokenLocking(tokenLocking).transfer(clnyToken, amount, _recipient, true);
}

function stakeForMining(uint256 _amount) public stoppable {
address clnyToken = IMetaColony(metaColony).getToken();
uint256 existingObligation = ITokenLocking(tokenLocking).getObligation(msg.sender, clnyToken, address(this));
Expand All @@ -211,9 +221,8 @@ contract ColonyNetworkMining is ColonyNetworkStorage {

function unstakeForMining(uint256 _amount) public stoppable {
address clnyToken = IMetaColony(metaColony).getToken();
// Prevent reputation miners from withdrawing stake during the mining process.
bytes32 submissionHash = IReputationMiningCycle(activeReputationMiningCycle).getReputationHashSubmission(msg.sender).proposedNewRootHash;
require(submissionHash == 0x0, "colony-network-hash-submitted");
// Prevent those involved in a mining cycle withdrawing stake during the mining process.
require(!IReputationMiningCycle(activeReputationMiningCycle).userInvolvedInMiningCycle(msg.sender), "colony-network-hash-submitted");
ITokenLocking(tokenLocking).deobligateStake(msg.sender, _amount, clnyToken);
miningStakes[msg.sender].amount = sub(miningStakes[msg.sender].amount, _amount);
}
Expand All @@ -222,6 +231,11 @@ contract ColonyNetworkMining is ColonyNetworkStorage {
return miningStakes[_user];
}

function burnUnneededRewards(uint256 _amount) public stoppable onlyReputationMiningCycle() {
ITokenLocking(tokenLocking).claim(IMetaColony(metaColony).getToken(), true);
ITokenLocking(tokenLocking).burn(_amount);
}

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) {
Expand All @@ -236,6 +250,4 @@ contract ColonyNetworkMining is ColonyNetworkStorage {

return add(mul(prevWeight, _prevTime), mul(currWeight, _currTime)) / add(prevWeight, currWeight);
}


}
1 change: 1 addition & 0 deletions contracts/colonyNetwork/ColonyNetworkStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ contract ColonyNetworkStorage is CommonStorage, ColonyNetworkDataTypes, DSMath {
mapping (address => mapping(uint256 => ReputationLogEntry)) replacementReputationUpdateLog; // Storage slot 31
mapping (address => bool) replacementReputationUpdateLogsExist; // Storage slot 32
mapping (address => MiningStake) miningStakes; // Storage slot 33
mapping (address => uint256) pendingMiningRewards; // Storage slot 34

modifier calledByColony() {
require(_isColony[msg.sender], "colony-caller-must-be-colony");
Expand Down
21 changes: 19 additions & 2 deletions contracts/colonyNetwork/IColonyNetwork.sol
Original file line number Diff line number Diff line change
Expand Up @@ -283,9 +283,8 @@ contract IColonyNetwork is ColonyNetworkDataTypes, IRecovery {
/// @notice Function called to punish people who staked against a new reputation root hash that turned out to be incorrect.
/// @dev While public, it can only be called successfully by the current ReputationMiningCycle.
/// @param _stakers Array of the addresses of stakers to punish
/// @param _beneficiary Address of beneficiary to receive forfeited stake
/// @param _amount Amount of stake to slash
function punishStakers(address[] memory _stakers, address _beneficiary, uint256 _amount) public;
function punishStakers(address[] memory _stakers, uint256 _amount) public;

/// @notice Stake CLNY to allow the staker to participate in reputation mining.
/// @param _amount Amount of CLNY to stake for the purposes of mining
Expand All @@ -295,6 +294,24 @@ contract IColonyNetwork is ColonyNetworkDataTypes, IRecovery {
/// @param _amount Amount of CLNY staked for mining to unstake
function unstakeForMining(uint256 _amount) public;

/// @notice returns how much CLNY _user has staked for the purposes of reputation mining
/// @param _user The user to query
/// @return _info The amount staked and the timestamp the stake was made at.
function getMiningStake(address _user) public view returns (MiningStake memory _info);

/// @notice Used to track that a user is eligible to claim a reward
/// @dev Only callable by the active reputation mining cycle
/// @param _recipient The address receiving the award
/// @param _amount The amount of CLNY to be awarded
function reward(address _recipient, uint256 _amount) public;

/// @notice Used to burn tokens that are not needed to pay out rewards (because not every possible defence was made for all submissions)
/// @dev Only callable by the active reputation mining cycle
/// @param _amount The amount of CLNY to burn
function burnUnneededRewards(uint256 _amount) public;

/// @notice Used by a user to claim any mining rewards due to them. This will place them in their balance or pending balance, as appropriate.
/// @dev Can be called by anyone, not just _recipient
/// @param _recipient The user whose rewards to claim
function claimMiningReward(address _recipient) public;
}
10 changes: 10 additions & 0 deletions contracts/reputationMiningCycle/IReputationMiningCycle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -255,4 +255,14 @@ contract IReputationMiningCycle is ReputationMiningCycleDataTypes {
/// @param jrh The JRH of that was submitted
/// @return count The number of submissions - should be 0-12, as up to twelve submissions can be made
function getNSubmissionsForHash(bytes32 hash, uint256 nNodes, bytes32 jrh) public view returns (uint256 count);

/// @notice Returns whether a particular address has been involved in the current mining cycle. This might be
/// from submitting a hash, or from defending one during a dispute.
/// @param _user The address whose involvement is being queried
/// @return bool Whether the address has been involved in the current mining cycle
function userInvolvedInMiningCycle(address _user) public view returns (bool involved);

/// @notice Returns the amount of CLNY given for defending a hash during the current dispute cycle
/// @return uint256 The amount of CLNY given.
function getDisputeRewardSize() public view returns (uint256 reward);
}
81 changes: 50 additions & 31 deletions contracts/reputationMiningCycle/ReputationMiningCycle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,11 @@ pragma experimental "ABIEncoderV2";
import "./../../lib/dappsys/math.sol";
import "./../colonyNetwork/IColonyNetwork.sol";
import "./../patriciaTree/PatriciaTreeProofs.sol";
import "./../reputationMiningCycle/ReputationMiningCycleStorage.sol";
import "./../tokenLocking/ITokenLocking.sol";
import "./ReputationMiningCycleCommon.sol";


contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProofs, DSMath {
/// @notice Minimum reputation mining stake in CLNY
uint256 constant MIN_STAKE = 2000 * WAD;

/// @notice Size of mining window in seconds
uint256 constant MINING_WINDOW_SIZE = 60 * 60 * 24; // 24 hours

contract ReputationMiningCycle is ReputationMiningCycleCommon {
/// @notice A modifier that checks that the supplied `roundNumber` is the final round
/// @param roundNumber The `roundNumber` to check if it is the final round
modifier finalDisputeRoundCompleted(uint256 roundNumber) {
Expand Down Expand Up @@ -182,10 +176,10 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo
hash1: 0x00,
hash2: 0x00
}));
// If we've got a pair of submissions to face off, may as well start now.
// If we've got a pair of submissions to face off, set them ready to start when the window closes.
if (nUniqueSubmittedHashes % 2 == 0) {
disputeRounds[0][nUniqueSubmittedHashes-1].lastResponseTimestamp = now;
disputeRounds[0][nUniqueSubmittedHashes-2].lastResponseTimestamp = now;
disputeRounds[0][nUniqueSubmittedHashes-1].lastResponseTimestamp = reputationMiningWindowOpenTimestamp + MINING_WINDOW_SIZE;
disputeRounds[0][nUniqueSubmittedHashes-2].lastResponseTimestamp = reputationMiningWindowOpenTimestamp + MINING_WINDOW_SIZE;
/* disputeRounds[0][nUniqueSubmittedHashes-1].upperBound = disputeRounds[0][nUniqueSubmittedHashes-1].jrhNNodes; */
/* disputeRounds[0][nUniqueSubmittedHashes-2].upperBound = disputeRounds[0][nUniqueSubmittedHashes-2].jrhNNodes; */
}
Expand All @@ -211,7 +205,13 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo
function confirmNewHash(uint256 roundNumber) public
finalDisputeRoundCompleted(roundNumber)
{
// No rewardResponders here - the submitters of the hash are incentivised to make this call, as it
// is the one that gives them the reward for staking in the first place. This means we don't have to
// take it in to account when calculating the reward for responders, which in turn means that the
// calculation can be done from a purely pairwise dispute perspective.
require(submissionWindowClosed(), "colony-reputation-mining-submission-window-still-open");
// Burn tokens that have been slashed, but will not be awarded to others as rewards.
IColonyNetwork(colonyNetworkAddress).burnUnneededRewards(sub(stakeLost, rewardsPaidOut));

DisputedEntry storage winningDisputeEntry = disputeRounds[roundNumber][0];
Submission storage submission = reputationHashSubmissions[winningDisputeEntry.firstSubmitter];
Expand All @@ -226,7 +226,7 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo

function invalidateHash(uint256 round, uint256 idx) public {
// What we do depends on our opponent, so work out which index it was at in disputeRounds[round]
uint256 opponentIdx = (idx % 2 == 1 ? idx-1 : idx + 1);
uint256 opponentIdx = getOpponentIdx(idx);
uint256 nInNextRound;

// We require either
Expand Down Expand Up @@ -274,7 +274,7 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo
require(disputeRounds[round][opponentIdx].challengeStepCompleted >= disputeRounds[round][idx].challengeStepCompleted, "colony-reputation-mining-less-challenge-rounds-completed");

// Require that it has failed a challenge (i.e. failed to respond in time)
require(now - disputeRounds[round][idx].lastResponseTimestamp >= 600, "colony-reputation-mining-not-timed-out"); // Timeout is ten minutes here.
require(add(disputeRounds[round][idx].lastResponseTimestamp, 600) <= now, "colony-reputation-mining-not-timed-out"); // Timeout is ten minutes here.

// Work out whether we are invalidating just the supplied idx or its opponent too.
bool eliminateOpponent = false;
Expand All @@ -297,14 +297,15 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo
} else {
// Our opponent completed the same number of challenge rounds, and both have now timed out.
nInvalidatedHashes += 2;

// Punish the people who proposed our opponent
IColonyNetwork(colonyNetworkAddress).punishStakers(
submittedHashes[opponentSubmission.proposedNewRootHash][opponentSubmission.nNodes][opponentSubmission.jrh],
msg.sender,
MIN_STAKE
);
emit HashInvalidated(opponentSubmission.proposedNewRootHash, opponentSubmission.nNodes, opponentSubmission.jrh);
stakeLost += submittedHashes[opponentSubmission.proposedNewRootHash][opponentSubmission.nNodes][opponentSubmission.jrh].length * MIN_STAKE;

emit HashInvalidated(opponentSubmission.proposedNewRootHash, opponentSubmission.nNodes, opponentSubmission.jrh);
}

// Note that two hashes have completed this challenge round (either one accepted for now and one rejected, or two rejected)
Expand All @@ -313,11 +314,13 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo
// Punish the people who proposed the hash that was rejected
IColonyNetwork(colonyNetworkAddress).punishStakers(
submittedHashes[submission.proposedNewRootHash][submission.nNodes][submission.jrh],
msg.sender,
MIN_STAKE
);
stakeLost += submittedHashes[submission.proposedNewRootHash][submission.nNodes][submission.jrh].length * MIN_STAKE;

emit HashInvalidated(submission.proposedNewRootHash, submission.nNodes, submission.jrh);
}
rewardResponder(msg.sender);
//TODO: Can we do some deleting to make calling this as cheap as possible for people?
}

Expand Down Expand Up @@ -355,6 +358,8 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo
// If require hasn't thrown, proof is correct.
// Process the consequences
processBinaryChallengeSearchResponse(round, idx, jhIntermediateValue, lastSiblings);
// Reward the user
rewardResponder(msg.sender);
}

function confirmBinarySearchResult(
Expand Down Expand Up @@ -388,6 +393,7 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo
while (2**(disputeRounds[round][idx].challengeStepCompleted - 2) <= submission.jrhNNodes) {
disputeRounds[round][idx].challengeStepCompleted += 1;
}
rewardResponder(msg.sender);

emit BinarySearchConfirmed(submission.proposedNewRootHash, submission.nNodes, submission.jrh, disputeRounds[round][idx].lowerBound);
}
Expand All @@ -399,9 +405,11 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo
bytes32[] memory siblings2
) public
{
require(submissionWindowClosed(), "colony-reputation-mining-cycle-submissions-not-closed");
require(index < disputeRounds[round].length, "colony-reputation-mining-index-beyond-round-length");

Submission storage submission = reputationHashSubmissions[disputeRounds[round][index].firstSubmitter];
// Require we've not submitted already.
// Require we've not confirmed the JRH already.
require(submission.jrhNNodes == 0, "colony-reputation-jrh-hash-already-verified");

// Calculate how many updates we're expecting in the justification tree
Expand Down Expand Up @@ -436,6 +444,8 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo
// Set bounds for first binary search if it's going to be needed
disputeRounds[round][index].upperBound = submission.jrhNNodes - 1;

rewardResponder(msg.sender);

emit JustificationRootHashConfirmed(submission.proposedNewRootHash, submission.nNodes, submission.jrh);
}

Expand Down Expand Up @@ -534,14 +544,28 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo
return reputationMiningWindowOpenTimestamp;
}

function getDisputeRewardSize() public returns (uint256) {
return disputeRewardSize();
}

function expectedBranchMask(uint256 nNodes, uint256 node) public pure returns (uint256) {
// Gets the expected branchmask for a patricia tree which has nNodes, with keys from 0 to nNodes -1
// i.e. the tree is 'full' - there are no missing nodes
uint256 mask = sub(nNodes, 1); // Every branchmask in a full tree has at least these 1s set
uint256 xored = mask ^ node; // Where do mask and node differ?
// Set every bit in the mask from the first bit where they differ to 1
uint256 remainderMask = sub(nextPowerOfTwoInclusive(add(xored, 1)), 1);
return mask | remainderMask;
}

function userInvolvedInMiningCycle(address _user) public view returns (bool) {
return reputationHashSubmissions[_user].proposedNewRootHash != 0x00 || respondedToChallenge[_user];
}

/////////////////////////
// Internal functions
/////////////////////////

function submissionWindowClosed() internal view returns (bool) {
return now - reputationMiningWindowOpenTimestamp >= MINING_WINDOW_SIZE;
}

function processBinaryChallengeSearchResponse(
uint256 round,
uint256 idx,
Expand All @@ -564,7 +588,7 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo
disputeRounds[round][idx].hash1 = lastSiblings[0];
disputeRounds[round][idx].hash2 = lastSiblings[1];

uint256 opponentIdx = (idx % 2 == 1 ? idx-1 : idx + 1);
uint256 opponentIdx = getOpponentIdx(idx);
if (disputeRounds[round][opponentIdx].challengeStepCompleted == disputeRounds[round][idx].challengeStepCompleted ) {
// Our opponent answered this challenge already.
// Compare our intermediateReputationHash to theirs to establish how to move the bounds.
Expand All @@ -573,7 +597,7 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo
}

function processBinaryChallengeSearchStep(uint256 round, uint256 idx) internal {
uint256 opponentIdx = (idx % 2 == 1 ? idx-1 : idx + 1);
uint256 opponentIdx = getOpponentIdx(idx);
uint256 searchWidth = (disputeRounds[round][idx].upperBound - disputeRounds[round][idx].lowerBound) + 1;
uint256 searchWidthNextPowerOfTwo = nextPowerOfTwoInclusive(searchWidth);
if (
Expand Down Expand Up @@ -705,13 +729,8 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo
return layers;
}

function expectedBranchMask(uint256 nNodes, uint256 node) public pure returns (uint256) {
// Gets the expected branchmask for a patricia tree which has nNodes, with keys from 0 to nNodes -1
// i.e. the tree is 'full' - there are no missing nodes
uint256 mask = sub(nNodes, 1); // Every branchmask in a full tree has at least these 1s set
uint256 xored = mask ^ node; // Where do mask and node differ?
// Set every bit in the mask from the first bit where they differ to 1
uint256 remainderMask = sub(nextPowerOfTwoInclusive(add(xored, 1)), 1);
return mask | remainderMask;
function getOpponentIdx(uint256 _idx) private pure returns (uint256) {
return _idx % 2 == 1 ? _idx - 1 : _idx + 1;
}

}
Loading

0 comments on commit 092c617

Please sign in to comment.