Skip to content

Commit

Permalink
Implement variable miner rewards (#164 II)
Browse files Browse the repository at this point in the history
  • Loading branch information
kronosapiens committed Nov 12, 2018
1 parent 39876cf commit 0b82977
Show file tree
Hide file tree
Showing 5 changed files with 67 additions and 48 deletions.
47 changes: 25 additions & 22 deletions contracts/ColonyNetworkMining.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ contract ColonyNetworkMining is ColonyNetworkStorage {
return replacementReputationUpdateLogsExist[_reputationMiningCycle];
}

function setReputationRootHash(bytes32 newHash, uint256 newNNodes, address[] stakers) public
function setReputationRootHash(bytes32 newHash, uint256 newNNodes, address[] stakers, uint256 reward) public
stoppable
onlyReputationMiningCycle
{
Expand All @@ -76,7 +76,7 @@ contract ColonyNetworkMining is ColonyNetworkStorage {
// Reward stakers
activeReputationMiningCycle = 0x0;
startNextCycle();
rewardStakers(stakers);
rewardStakers(stakers, reward);
}

function initialiseReputationMining() public stoppable {
Expand Down Expand Up @@ -114,7 +114,7 @@ contract ColonyNetworkMining is ColonyNetworkStorage {
}
}

// Constants for miner weight calculations, in WADs
// Constants for miner weight calculations
uint256 constant T = 7776000 * WAD; // Seconds in 90 days
uint256 constant N = 24 * WAD; // 2x maximum number of miners
uint256 constant UINT32_MAX = 4294967295;
Expand All @@ -131,7 +131,7 @@ contract ColonyNetworkMining is ColonyNetworkStorage {
return wmul(stakeTerm, submissionTerm);
}

function rewardStakers(address[] stakers) internal {
function rewardStakers(address[] stakers, uint256 reward) internal {
// Internal unlike punish, because it's only ever called from setReputationRootHash

// Passing an array so that we don't incur the EtherRouter overhead for each staker if we looped over
Expand All @@ -141,31 +141,34 @@ contract ColonyNetworkMining is ColonyNetworkStorage {
address clnyToken = IColony(metaColony).getToken();

// I. Calculate (normalized) miner weights
// uint256 timeStaked;
// uint256 minerWeightsTotal = 1;
// uint256[] memory minerWeights = new uint256[](stakers.length);
uint256 timeStaked;
uint256 minerWeightsTotal;
uint256[] memory minerWeights = new uint256[](stakers.length);

// for (i = 0; i < stakers.length; i++) {
// (,,timeStaked) = ITokenLocking(tokenLocking).getUserLock(clnyToken, stakers[i]);
// minerWeights[i] = calculateMinerWeight(now - timeStaked, i);
// minerWeightsTotal = add(minerWeightsTotal, minerWeights[i]);
// }

// for (i = 0; i < stakers.length; i++) {
// minerWeights[i] = wdiv(minerWeights[i], minerWeightsTotal);
// }
for (i = 0; i < stakers.length; i++) {
(,,timeStaked) = ITokenLocking(tokenLocking).getUserLock(clnyToken, stakers[i]);
minerWeights[i] = calculateMinerWeight(now - timeStaked, i + 1);
minerWeightsTotal = add(minerWeightsTotal, minerWeights[i]);
}

// II. Figure out total miner reward M
uint256 reward = 10**18; //TODO: Actually work out how much reputation they earn, based on activity elsewhere in the colony.
for (i = 0; i < stakers.length; i++) {
minerWeights[i] = wdiv(minerWeights[i], minerWeightsTotal);
}

// III. Disburse reputation and tokens
IMetaColony(metaColony).mintTokensForColonyNetwork(stakers.length * reward); // This should be the total amount of new tokens we're awarding.
// II. Disburse reputation and tokens
IMetaColony(metaColony).mintTokensForColonyNetwork(reward);

// This gives them reputation in the next update cycle.
IReputationMiningCycle(inactiveReputationMiningCycle).rewardStakersWithReputation(stakers, metaColony, reward, rootGlobalSkillId + 2);
IReputationMiningCycle(inactiveReputationMiningCycle).rewardStakersWithReputation(
stakers,
minerWeights,
metaColony,
reward,
rootGlobalSkillId + 2
);

for (i = 0; i < stakers.length; i++) {
ERC20Extended(clnyToken).transfer(stakers[i], reward);
ERC20Extended(clnyToken).transfer(stakers[i], wmul(reward, minerWeights[i]));
}
}
}
3 changes: 2 additions & 1 deletion contracts/IColonyNetwork.sol
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ contract IColonyNetwork is IRecovery {
/// @param newHash The reputation root hash
/// @param newNNodes The updated nodes count value
/// @param stakers Array of users who submitted or backed the hash, being accepted here as the new reputation root hash
function setReputationRootHash(bytes32 newHash, uint256 newNNodes, address[] stakers) public;
/// @param reward Amount of CLNY to be distributed as reward to miners
function setReputationRootHash(bytes32 newHash, uint256 newNNodes, address[] stakers, uint256 reward) public;

/// @notice Starts a new Reputation Mining cycle. Explicitly called only the first time,
/// subsequently called from within `setReputationRootHash`
Expand Down
5 changes: 3 additions & 2 deletions contracts/IReputationMiningCycle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -199,12 +199,13 @@ contract IReputationMiningCycle {

/// @notice Start the reputation log with the rewards for the stakers who backed the accepted new reputation root hash.
/// @param stakers The array of stakers addresses to receive the reward.
/// @param commonColonyAddress The address of the common colony, which the special mining skill is earned in
/// @param weights The array of weights determining the proportion of reward to go to each staker
/// @param metaColonyAddress The address of the meta colony, which the special mining skill is earned in
/// @param reward The amount of reputation to be rewarded to each staker
/// @dev Only callable by colonyNetwork
/// @dev Note that the same address might be present multiple times in `stakers` - this is acceptable, and indicates the
/// same address backed the same hash multiple times with different entries.
function rewardStakersWithReputation(address[] stakers, address commonColonyAddress, uint reward, uint miningSkillId) public;
function rewardStakersWithReputation(address[] stakers, uint256[] weights, address metaColonyAddress, uint reward, uint miningSkillId) public;

/// @notice Get the timestamp that the current reputation mining window opened
function getReputationMiningWindowOpenTimestamp() public view returns (uint256 timestamp);
Expand Down
17 changes: 13 additions & 4 deletions contracts/ReputationMiningCycle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,8 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo
IColonyNetwork(colonyNetworkAddress).setReputationRootHash(
submission.proposedNewRootHash,
submission.nNodes,
submittedHashes[submission.proposedNewRootHash][submission.nNodes]
submittedHashes[submission.proposedNewRootHash][submission.nNodes],
1200 * WAD // TODO: Make this a function of reputation state
);
selfdestruct(colonyNetworkAddress);
}
Expand Down Expand Up @@ -485,17 +486,25 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo
);
}

function rewardStakersWithReputation(address[] stakers, address commonColonyAddress, uint256 reward, uint256 miningSkillId) public {
function rewardStakersWithReputation(
address[] stakers,
uint256[] weights,
address metaColonyAddress,
uint256 reward,
uint256 miningSkillId
) public
{
require(msg.sender == colonyNetworkAddress, "colony-reputation-mining-sender-not-network");
require(reputationUpdateLog.length == 0, "colony-reputation-mining-log-length-non-zero");
require(stakers.length == weights.length, "colony-reputation-mining-staker-weight-mismatch");
for (uint256 i = 0; i < stakers.length; i++) {
// We *know* we're the first entries in this reputation update log, so we don't need all the bookkeeping in
// the AppendReputationUpdateLog function
reputationUpdateLog.push(ReputationLogEntry(
stakers[i],
int256(reward),
int256(wmul(reward, weights[i])),
miningSkillId, //This should be the special 'mining' skill.
commonColonyAddress, // They earn this reputation in the common colony.
metaColonyAddress, // They earn this reputation in the meta colony.
4, // Updates the user's skill, and the colony's skill, both globally and for the special 'mining' skill
i*4 //We're zero indexed, so this is the number of updates that came before in the reputation log.
));
Expand Down
43 changes: 24 additions & 19 deletions test/colony-network-mining.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ const contractLoader = new TruffleLoader({

const useJsTree = true;

const REWARD = WAD.muln(1200); // 1200 CLNY

contract("ColonyNetworkMining", accounts => {
const MAIN_ACCOUNT = accounts[0];
const OTHER_ACCOUNT = accounts[1];
Expand Down Expand Up @@ -668,23 +670,23 @@ contract("ColonyNetworkMining", accounts => {

// Check that they received the reward
const balance1Updated = await clny.balanceOf(MAIN_ACCOUNT);
assert.equal(balance1Updated.toString(), new BN("2").mul(WAD).toString(), "Account was not rewarded properly");
assert.equal(balance1Updated.toString(), REWARD.toString(), "Account was not rewarded properly");

addr = await colonyNetwork.getReputationMiningCycle(false);
repCycle = await IReputationMiningCycle.at(addr);

// Check that they will be getting the reputation owed to them.
let repLogEntryMiner = await repCycle.getReputationUpdateLogEntry(0);
assert.equal(repLogEntryMiner[0], MAIN_ACCOUNT);
assert.equal(repLogEntryMiner[1].toString(), new BN("1").mul(WAD).toString());
assert.isTrue(repLogEntryMiner[1].sub(REWARD.divn(2)).gtn(0));
assert.equal(repLogEntryMiner[2].toString(), "3");
assert.equal(repLogEntryMiner[3], metaColony.address);
assert.equal(repLogEntryMiner[4].toString(), "4");
assert.equal(repLogEntryMiner[5].toString(), "0");

repLogEntryMiner = await repCycle.getReputationUpdateLogEntry(1);
assert.equal(repLogEntryMiner[0], MAIN_ACCOUNT);
assert.equal(repLogEntryMiner[1].toString(), new BN("1").mul(WAD).toString());
assert.isTrue(repLogEntryMiner[1].sub(REWARD.divn(2)).ltn(0));
assert.equal(repLogEntryMiner[2].toString(), "3");
assert.equal(repLogEntryMiner[3], metaColony.address);
assert.equal(repLogEntryMiner[4].toString(), "4");
Expand Down Expand Up @@ -768,7 +770,6 @@ contract("ColonyNetworkMining", accounts => {
it("should reward all stakers if they submitted the agreed new hash", async () => {
await giveUserCLNYTokensAndStake(colonyNetwork, MAIN_ACCOUNT, DEFAULT_STAKE);
await giveUserCLNYTokensAndStake(colonyNetwork, OTHER_ACCOUNT, DEFAULT_STAKE);
await giveUserCLNYTokensAndStake(colonyNetwork, accounts[2], DEFAULT_STAKE);

let addr = await colonyNetwork.getReputationMiningCycle(true);
let repCycle = await IReputationMiningCycle.at(addr);
Expand All @@ -784,25 +785,30 @@ contract("ColonyNetworkMining", accounts => {

// Check that they have had their balance increase
const balance1Updated = await clny.balanceOf(MAIN_ACCOUNT);
assert.equal(balance1Updated.toString(), new BN("1").mul(WAD).toString(), "Account was not rewarded properly");
const balance2Updated = await clny.balanceOf(OTHER_ACCOUNT);
assert.equal(balance2Updated.toString(), new BN("1").mul(WAD).toString(), "Account was not rewarded properly");

// More than half of the reward
assert.isTrue(balance1Updated.sub(REWARD.divn(2)).gtn(0), "Account was not rewarded properly");
// Less than half of the reward
assert.isTrue(balance2Updated.sub(REWARD.divn(2)).ltn(0), "Account was not rewarded properly");
// Sum is total reward within 100 wei of precision error
assert.closeTo(balance1Updated.add(balance2Updated).sub(REWARD).toNumber(), 0, 100); // eslint-disable-line prettier/prettier

addr = await colonyNetwork.getReputationMiningCycle(false);
repCycle = await IReputationMiningCycle.at(addr);

// Check that they will be getting the reputation owed to them.
let repLogEntryMiner = await repCycle.getReputationUpdateLogEntry(0);
assert.equal(repLogEntryMiner[0], MAIN_ACCOUNT);
assert.equal(repLogEntryMiner[1].toString(), new BN("1").mul(WAD).toString());
assert.equal(repLogEntryMiner[1].toString(), balance1Updated.toString());
assert.equal(repLogEntryMiner[2].toString(), "3");
assert.equal(repLogEntryMiner[3], metaColony.address);
assert.equal(repLogEntryMiner[4].toString(), "4");
assert.equal(repLogEntryMiner[5].toString(), "0");

repLogEntryMiner = await repCycle.getReputationUpdateLogEntry(1);
assert.equal(repLogEntryMiner[0], OTHER_ACCOUNT);
assert.equal(repLogEntryMiner[1].toString(), new BN("1").mul(WAD).toString());
assert.equal(repLogEntryMiner[1].toString(), balance2Updated.toString());
assert.equal(repLogEntryMiner[2].toString(), "3");
assert.equal(repLogEntryMiner[3], metaColony.address);
assert.equal(repLogEntryMiner[4].toString(), "4");
Expand All @@ -816,7 +822,7 @@ contract("ColonyNetworkMining", accounts => {
describe("Function permissions", () => {
it('should not allow "setReputationRootHash" to be called from an account that is not not reputationMiningCycle', async () => {
await checkErrorRevert(
colonyNetwork.setReputationRootHash("0x000001", 10, [accounts[0], accounts[1]]),
colonyNetwork.setReputationRootHash("0x000001", 10, [accounts[0], accounts[1]], 0),
"colony-reputation-mining-sender-not-active-reputation-cycle"
);
});
Expand All @@ -831,7 +837,7 @@ contract("ColonyNetworkMining", accounts => {
it('should not allow "rewardStakersWithReputation" to be called by someone not the colonyNetwork', async () => {
const addr = await colonyNetwork.getReputationMiningCycle(true);
const repCycle = await IReputationMiningCycle.at(addr);
await checkErrorRevert(repCycle.rewardStakersWithReputation([MAIN_ACCOUNT], 0x0, 10000, 3), "colony-reputation-mining-sender-not-network");
await checkErrorRevert(repCycle.rewardStakersWithReputation([MAIN_ACCOUNT], [1], 0x0, 10000, 3), "colony-reputation-mining-sender-not-network");
});

it('should not allow "initialise" to be called on either the active or inactive ReputationMiningCycle', async () => {
Expand Down Expand Up @@ -2827,7 +2833,6 @@ contract("ColonyNetworkMining", accounts => {
// 5. Worker reputation for metacolony's root skill
// 6. Colony-wide total reputation for global skill task was in
// 7. Worker reputation for global skill task was in
//

const GLOBAL_SKILL = 1;
const META_ROOT_SKILL = 2;
Expand All @@ -2839,22 +2844,22 @@ contract("ColonyNetworkMining", accounts => {
// These should be:
// 1. Colony-wide total reputation for metacolony's root skill
key = makeReputationKey(metaColony.address, META_ROOT_SKILL);
value = makeReputationValue(DEFAULT_STAKE.muln(6).add(WAD), 1);
value = makeReputationValue(DEFAULT_STAKE.muln(6).add(REWARD), 1);
assert.equal(client.reputations[key], value);

// 2. Colony-wide total reputation for mining skill
key = makeReputationKey(metaColony.address, MINING_SKILL);
value = makeReputationValue(1000000000000000000, 2);
value = makeReputationValue(REWARD, 2);
assert.equal(client.reputations[key], value);

// 3. Reputation reward for MAIN_ACCOUNT for being the manager for the tasks created by giveUserCLNYTokens
key = makeReputationKey(metaColony.address, META_ROOT_SKILL, MAIN_ACCOUNT);
value = makeReputationValue(DEFAULT_STAKE.muln(6).add(WAD), 3);
value = makeReputationValue(DEFAULT_STAKE.muln(6).add(REWARD), 3);
assert.equal(client.reputations[key], value);

// 4. Reputation reward for MAIN_ACCOUNT for submitting the previous reputation hash
key = makeReputationKey(metaColony.address, MINING_SKILL, MAIN_ACCOUNT);
value = makeReputationValue(1000000000000000000, 4);
value = makeReputationValue(REWARD, 4);
assert.equal(client.reputations[key], value);

// 5. Reputation reward for accounts[2] for being the worker for the tasks created by giveUserCLNYTokens
Expand Down Expand Up @@ -2923,10 +2928,10 @@ contract("ColonyNetworkMining", accounts => {
const MINING_SKILL = 3;

const reputationProps = [
{ id: 1, skill: META_ROOT_SKILL, account: undefined, value: DEFAULT_STAKE.muln(2).add(new BN("1000003000000000000")) },
{ id: 2, skill: MINING_SKILL, account: undefined, value: 1000000000000000000 },
{ id: 3, skill: META_ROOT_SKILL, account: MAIN_ACCOUNT, value: DEFAULT_STAKE.muln(2).add(new BN("1000001000000000000")) },
{ id: 4, skill: MINING_SKILL, account: MAIN_ACCOUNT, value: 1000000000000000000 },
{ id: 1, skill: META_ROOT_SKILL, account: undefined, value: DEFAULT_STAKE.muln(2).add(REWARD).add(new BN("3000000000000")) }, // eslint-disable-line prettier/prettier
{ id: 2, skill: MINING_SKILL, account: undefined, value: REWARD },
{ id: 3, skill: META_ROOT_SKILL, account: MAIN_ACCOUNT, value: DEFAULT_STAKE.muln(2).add(REWARD).add(new BN("1000000000000")) }, // eslint-disable-line prettier/prettier
{ id: 4, skill: MINING_SKILL, account: MAIN_ACCOUNT, value: REWARD },
{ id: 5, skill: META_ROOT_SKILL, account: accounts[2], value: 1000000000000 },
{ id: 6, skill: 1, account: undefined, value: 1000000000000 },
{ id: 7, skill: 1, account: accounts[2], value: 0 },
Expand Down

0 comments on commit 0b82977

Please sign in to comment.