Skip to content

Commit

Permalink
Add Lock.timestamp and minerWeight calculation (#164 I)
Browse files Browse the repository at this point in the history
  • Loading branch information
kronosapiens committed Oct 27, 2018
1 parent 70de186 commit 40dcec4
Show file tree
Hide file tree
Showing 9 changed files with 125 additions and 20 deletions.
2 changes: 1 addition & 1 deletion contracts/ColonyFunding.sol
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs {

uint256 userTokens;
ITokenLocking tokenLocking = ITokenLocking(IColonyNetwork(colonyNetworkAddress).getTokenLocking());
(, userTokens) = tokenLocking.getUserLock(address(token), msg.sender);
(, userTokens,) = tokenLocking.getUserLock(address(token), msg.sender);

require(userTokens > 0, "colony-reward-payout-invalid-user-tokens");
require(userReputation > 0, "colony-reward-payout-invalid-user-reputation");
Expand Down
52 changes: 41 additions & 11 deletions contracts/ColonyNetworkMining.sol
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pragma experimental "v0.5.0";
import "./ColonyNetworkStorage.sol";
import "./ERC20Extended.sol";
import "./IReputationMiningCycle.sol";
import "./ITokenLocking.sol";
import "./EtherRouter.sol";


Expand Down Expand Up @@ -113,29 +114,58 @@ contract ColonyNetworkMining is ColonyNetworkStorage {
}
}

// Constants for miner weight calculations, in WADs
uint256 constant T = 7776000 * WAD; // Seconds in 90 days
uint256 constant N = 24 * WAD; // 2x maximum number of miners
uint256 constant UINT32_MAX = 4294967295;

function calculateMinerWeight(uint256 timeStaked, uint256 submissonIndex) public view returns (uint256) {
require((submissonIndex >= 1) && (submissonIndex <= 12), "colony-reputation-mining-invalid-submission-index");
uint256 timeStakedMax = min(timeStaked, UINT32_MAX); // Maximum of ~136 years (uint32)

// (1 - exp{-t_n/T}) * (1 - (n-1)/N), 3rd degree Taylor expansion for exponential term
uint256 tnDivT = wdiv(timeStakedMax * WAD, T);
uint256 expTnDivT = add(add(add(WAD, tnDivT), wmul(tnDivT, tnDivT) / 2), wmul(wmul(tnDivT, tnDivT), tnDivT) / 6);
uint256 stakeTerm = sub(WAD, wdiv(WAD, expTnDivT));
uint256 submissionTerm = sub(WAD, wdiv((submissonIndex - 1) * WAD, N));
return wmul(stakeTerm, submissionTerm);
}

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

// TODO: Actually think about this function
// Passing an array so that we don't incur the EtherRouter overhead for each staker if we looped over
// it in ReputationMiningCycle.confirmNewHash;

uint256 i;
address clnyToken = IColony(metaColony).getToken();

// I. Calculate (normalized) miner weights
// uint256 timeStaked;
// uint256 minerWeightsTotal = 1;
// 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);
// }

// 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.
if (reward >= uint256(int256(-1))/2) {
reward = uint256(int256(-1))/2;
}
// TODO: We need to be able to prove that the assert on the next line will never happen, otherwise we're locked out of reputation mining.
// Something like the above cap is an adequate short-term solution, but at the very least need to double check the limits
// (which I've fingered-in-the-air, but could easily have an OBOE hiding inside).
assert(reward < uint256(int256(-1))); // We do a cast later, so make sure we don't overflow.

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

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

for (uint256 i = 0; i < stakers.length; i++) {
// Also give them some newly minted tokens.
ERC20Extended(IColony(metaColony).getToken()).transfer(stakers[i], reward);
for (i = 0; i < stakers.length; i++) {
ERC20Extended(clnyToken).transfer(stakers[i], reward);
}
}
}
6 changes: 6 additions & 0 deletions contracts/IColonyNetwork.sol
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,12 @@ contract IColonyNetwork is IRecovery {
/// @return repMiningCycleAddress address of active or inactive ReputationMiningCycle
function getReputationMiningCycle(bool _active) public view returns (address repMiningCycleAddress);

/// @notice Calculate raw miner weight in WADs
/// @param _timeStaked Amount of time (in seconds) that the miner has staked their CLNY
/// @param _submissonIndex Index of reputation hash submission (between 1 and 12)
/// @return minerWeight The weight of miner reward
function calculateMinerWeight(uint256 _timeStaked, uint256 _submissonIndex) public view returns (uint256 minerWeight);

/// @notice Get the `Resolver` address for Colony contract version `_version`
/// @param _version The Colony contract version
/// @return resolverAddress Address of the `Resolver` contract
Expand Down
3 changes: 2 additions & 1 deletion contracts/ITokenLocking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,6 @@ contract ITokenLocking {
/// @param _user Address of the user
/// @return lockCount User's token lock count
/// @return amount User's deposited amount
function getUserLock(address _token, address _user) public view returns (uint256 lockCount, uint256 amount);
/// @return timestamp Timestamp of deposit
function getUserLock(address _token, address _user) public view returns (uint256 lockCount, uint256 amount, uint256 timestamp);
}
6 changes: 4 additions & 2 deletions contracts/ReputationMiningCycle.sol
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,18 @@ contract ReputationMiningCycle is ReputationMiningCycleStorage, PatriciaTreeProo
/// @param entryIndex The number of the entry the submitter hash asked us to consider.
modifier entryQualifies(bytes32 newHash, uint256 nNodes, uint256 entryIndex) {
uint256 balance;
(, balance) = ITokenLocking(tokenLockingAddress).getUserLock(clnyTokenAddress, msg.sender);
(, balance,) = ITokenLocking(tokenLockingAddress).getUserLock(clnyTokenAddress, msg.sender);
require(entryIndex <= balance / MIN_STAKE, "colony-reputation-mining-stake-minimum-not-met-for-index");
require(entryIndex > 0, "colony-reputation-mining-zero-entry-index-passed");

// If this user has submitted before during this round...
if (reputationHashSubmissions[msg.sender].proposedNewRootHash != 0x0) {
// ...require that they are submitting the same hash ...
require(newHash == reputationHashSubmissions[msg.sender].proposedNewRootHash, "colony-reputation-mining-submitting-different-hash");
// ...require that they are submitting the same number of nodes for that hash ...
require(nNodes == reputationHashSubmissions[msg.sender].nNodes, "colony-reputation-mining-submitting-different-nnodes");
require(submittedEntries[newHash][msg.sender][entryIndex] == false, "colony-reputation-mining-submitting-same-entry-index"); // ... but not this exact entry
// ... but not this exact entry
require(submittedEntries[newHash][msg.sender][entryIndex] == false, "colony-reputation-mining-submitting-same-entry-index");
}
_;
}
Expand Down
23 changes: 19 additions & 4 deletions contracts/TokenLocking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -67,14 +67,28 @@ contract TokenLocking is TokenLockingStorage, DSMath {
userLocks[_token][msg.sender].lockCount = _lockId;
}

uint256 constant UINT192_MAX = 2**192 - 1; // Used for updating the deposit timestamp

function deposit(address _token, uint256 _amount) public
tokenNotLocked(_token)
{
require(_amount > 0, "colony-token-locking-invalid-amount");

require(ERC20Extended(_token).transferFrom(msg.sender, address(this), _amount), "colony-token-locking-transfer-failed");

userLocks[_token][msg.sender] = Lock(totalLockCount[_token], add(userLocks[_token][msg.sender].balance, _amount));
Lock storage lock = userLocks[_token][msg.sender];

uint256 prevWeight = lock.balance;
uint256 currWeight = _amount;

// Needed to prevent overflows in the timestamp calculation
while ((prevWeight >= UINT192_MAX) || (currWeight >= UINT192_MAX)) {
prevWeight /= 2;
currWeight /= 2;
}

uint256 timestamp = add(mul(prevWeight, lock.timestamp), mul(currWeight, now)) / add(prevWeight, currWeight);

userLocks[_token][msg.sender] = Lock(totalLockCount[_token], add(lock.balance, _amount), timestamp);
}

function withdraw(address _token, uint256 _amount) public
Expand Down Expand Up @@ -108,7 +122,8 @@ contract TokenLocking is TokenLockingStorage, DSMath {
return totalLockCount[_token];
}

function getUserLock(address _token, address _user) public view returns (uint256, uint256) {
return (userLocks[_token][_user].lockCount, userLocks[_token][_user].balance);
function getUserLock(address _token, address _user) public view returns (uint256, uint256, uint256) {
Lock storage lock = userLocks[_token][_user];
return (lock.lockCount, lock.balance, lock.timestamp);
}
}
2 changes: 2 additions & 0 deletions contracts/TokenLockingStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ contract TokenLockingStorage is DSAuth {
uint256 lockCount;
// Deposited balance
uint256 balance;
// Timestamp of last deposit
uint256 timestamp;
}

// Maps token to user to Lock struct
Expand Down
30 changes: 30 additions & 0 deletions test/colony-network-mining.js
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,36 @@ contract("ColonyNetworkMining", accounts => {
const windowOpenTimestampAfter = await repCycle.getReputationMiningWindowOpenTimestamp();
assert.equal(windowOpenTimestampBefore.toString(), windowOpenTimestampAfter.toString());
});

it("should correctly calculate the miner weight", async () => {
const UINT256_MAX = new BN(0).notn(256);
const UINT32_MAX = new BN(0).notn(32);
let weight;

// Large weight (staked for UINT256_MAX, first submission)
weight = await colonyNetwork.calculateMinerWeight(UINT256_MAX, 1);
assert.equal("999999964585636861", weight.toString());

// Large weight (staked for UINT32_MAX, first submission)
weight = await colonyNetwork.calculateMinerWeight(UINT32_MAX, 1);
assert.equal("999999964585636861", weight.toString());

// Middle weight (staked for UINT32_MAX, last submission)
weight = await colonyNetwork.calculateMinerWeight(UINT32_MAX, 12);
assert.equal("541666647483886633", weight.toString());

// Middle weight I (staked for T, first submission)
weight = await colonyNetwork.calculateMinerWeight(7776000, 1);
assert.equal("625000000000000000", weight.toString());

// Middle weight II (staked for T, last submission)
weight = await colonyNetwork.calculateMinerWeight(7776000, 12);
assert.equal("338541666666666667", weight.toString());

// Smallest weight (staked for 0, last submission)
weight = await colonyNetwork.calculateMinerWeight(0, 12);
assert.equal("0", weight.toString());
});
});

describe("Elimination of submissions", () => {
Expand Down
21 changes: 20 additions & 1 deletion test/token-locking.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/* globals artifacts */
import path from "path";
import { TruffleLoader } from "@colony/colony-js-contract-loader-fs";
import { getTokenArgs, checkErrorRevert, forwardTime, makeReputationKey } from "../helpers/test-helper";
import { getTokenArgs, checkErrorRevert, forwardTime, makeReputationKey, currentBlockTime } from "../helpers/test-helper";
import { giveUserCLNYTokensAndStake } from "../helpers/test-data-generator";
import { DEFAULT_STAKE } from "../helpers/constants";

Expand Down Expand Up @@ -107,6 +107,25 @@ contract("TokenLocking", addresses => {
assert.equal(tokenLockingContractBalance.toNumber(), usersTokens);
});

it("should correctly set deposit timestamp", async () => {
await token.approve(tokenLocking.address, usersTokens, { from: userAddress });
const deposit = usersTokens / 2;

const time1 = await currentBlockTime();
await tokenLocking.deposit(token.address, deposit, { from: userAddress });
const info1 = await tokenLocking.getUserLock(token.address, userAddress);
assert.equal(info1[2].toNumber(), time1);

await forwardTime(3600);

const time2 = await currentBlockTime();
await tokenLocking.deposit(token.address, deposit, { from: userAddress });
const info2 = await tokenLocking.getUserLock(token.address, userAddress);

const avgTime = (time1 + time2) / 2;
assert.closeTo(info2[2].toNumber(), avgTime, 1); // Tolerance of 1 second
});

it("should not be able to deposit tokens if they are not approved", async () => {
await checkErrorRevert(
tokenLocking.deposit(token.address, usersTokens, {
Expand Down

0 comments on commit 40dcec4

Please sign in to comment.