Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce ReputationBootstrapper #1086

Merged
merged 17 commits into from
Dec 12, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
219 changes: 219 additions & 0 deletions contracts/extensions/ReputationBootstrapper.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
/*
This file is part of The Colony Network.

The Colony Network is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

The Colony Network is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with The Colony Network. If not, see <http://www.gnu.org/licenses/>.
*/

pragma solidity 0.7.3;
pragma experimental ABIEncoderV2;

import "./../../lib/dappsys/erc20.sol";
import "./../reputationMiningCycle/IReputationMiningCycle.sol";
import "./../colonyNetwork/IColonyNetwork.sol";
import "./ColonyExtensionMeta.sol";

// ignore-file-swc-108


contract ReputationBootstrapper is ColonyExtensionMeta {

// Constants

uint256 constant INT128_MAX = 2**127 - 1;
uint256 constant SECURITY_DELAY = 1 hours;

// Events

event GrantSet(bool paid, bytes32 hashedSecret, uint256 reputationAmount);
event GrantClaimed(address recipient, uint256 reputationAmount, bool paid);

// Data structures

struct Grant {
uint256 amount;
uint256 timestamp;
}

// Storage

address token;

uint256 decayPeriod;
uint256 decayNumerator;
uint256 decayDenominator;

uint256 totalPayableGrants;
mapping (bool => mapping (bytes32 => Grant)) grants;
mapping (bytes32 => uint256) committedSecrets;

// Modifiers

modifier onlyRoot() {
require(colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root), "reputation-bootstrapper-caller-not-root");
_;
}

// Overrides

/// @notice Returns the identifier of the extension
function identifier() public override pure returns (bytes32) {
return keccak256("ReputationBootstrapper");
}

/// @notice Returns the version of the extension
function version() public override pure returns (uint256) {
return 1;
}

/// @notice Configures the extension
/// @param _colony The colony in which the extension holds permissions
function install(address _colony) public override auth {
require(address(colony) == address(0x0), "extension-already-installed");

colony = IColony(_colony);
token = colony.getToken();

address colonyNetwork = colony.getColonyNetwork();
address repCycle = IColonyNetwork(colonyNetwork).getReputationMiningCycle(false);
decayPeriod = IReputationMiningCycle(repCycle).getMiningWindowDuration();
(decayNumerator, decayDenominator) = IReputationMiningCycle(repCycle).getDecayConstant();
kronosapiens marked this conversation as resolved.
Show resolved Hide resolved
}

/// @notice Called when upgrading the extension
function finishUpgrade() public override auth {}

/// @notice Called when deprecating (or undeprecating) the extension
function deprecate(bool _deprecated) public override auth {
kronosapiens marked this conversation as resolved.
Show resolved Hide resolved
deprecated = _deprecated;
}

/// @notice Called when uninstalling the extension
function uninstall() public override auth {
uint256 balance = ERC20(token).balanceOf(address(this));
require(ERC20(token).transfer(address(colony), balance), "reputation-bootstrapper-transfer-failed");

selfdestruct(address(uint160(address(colony))));
kronosapiens marked this conversation as resolved.
Show resolved Hide resolved
}

// Public

/// @notice Set an arbitrary number of grants
/// @param _paid An array of booleans indicated pair or unpaid
/// @param _hashedSecrets An array of (hashed) secrets
/// @param _amounts An array of reputation amounts claimable by the secret
function setGrants(bool[] memory _paid, bytes32[] memory _hashedSecrets, uint256[] memory _amounts) public onlyRoot notDeprecated {
require(_paid.length == _hashedSecrets.length, "reputation-bootstrapper-invalid-arguments");
require(_hashedSecrets.length == _amounts.length, "reputation-bootstrapper-invalid-arguments");
uint256 balance = ERC20(token).balanceOf(address(this));

for (uint256 i; i < _hashedSecrets.length; i++) {
require(_amounts[i] <= INT128_MAX, "reputation-bootstrapper-invalid-amount");

if (_paid[i]) {
uint256 priorGrant = grants[_paid[i]][_hashedSecrets[i]].amount;
totalPayableGrants = totalPayableGrants - priorGrant + _amounts[i];
}

grants[_paid[i]][_hashedSecrets[i]] = Grant(_amounts[i], block.timestamp);

emit GrantSet(_paid[i], _hashedSecrets[i], _amounts[i]);
}

require(totalPayableGrants <= balance, "reputation-bootstrapper-insufficient-balance");
}

/// @notice Commit the secret, beginning the security delay window
/// @param _committedSecret A sha256 hash of (userAddress, secret)
function commitSecret(bytes32 _committedSecret) public notDeprecated {
bytes32 addressHash = keccak256(abi.encodePacked(msgSender(), _committedSecret));
committedSecrets[addressHash] = block.timestamp;
}

/// @notice Claim the grant, after committing the secret and having the security delay elapse
/// @param _secret The secret corresponding to a reputation grant
function claimGrant(bool _paid, uint256 _secret) public notDeprecated {
kronosapiens marked this conversation as resolved.
Show resolved Hide resolved
kronosapiens marked this conversation as resolved.
Show resolved Hide resolved
bytes32 committedSecret = keccak256(abi.encodePacked(msgSender(), _secret));
kronosapiens marked this conversation as resolved.
Show resolved Hide resolved
bytes32 addressHash = keccak256(abi.encodePacked(msgSender(), committedSecret));
uint256 commitTimestamp = committedSecrets[addressHash];

require(
commitTimestamp > 0 && commitTimestamp + SECURITY_DELAY <= block.timestamp,
"reputation-bootstrapper-commit-window-unelapsed"
);

bytes32 hashedSecret = keccak256(abi.encodePacked(_secret));
uint256 grantAmount = grants[_paid][hashedSecret].amount;
uint256 grantTimestamp = grants[_paid][hashedSecret].timestamp;

require(grantAmount > 0, "reputation-bootstrapper-nothing-to-claim");

delete grants[_paid][hashedSecret];

if (_paid) {
totalPayableGrants -= grantAmount;
require(ERC20(token).transfer(msgSender(), grantAmount), "reputation-bootstrapper-transfer-failed");
}

uint256 decayEpochs = (block.timestamp - grantTimestamp) / decayPeriod;
uint256 adjustedNumerator = decayNumerator;

// This algorithm successively doubles the decay factor while halving the number of epochs
// This allows us to perform the decay in O(log(n)) time
// For example, a decay of 50 epochs would be applied as (k**2)(k**16)(k**32)
while (decayEpochs > 0){
// slither-disable-next-line weak-prng
if (decayEpochs % 2 >= 1) {
// slither-disable-next-line divide-before-multiply
grantAmount = grantAmount * adjustedNumerator / decayDenominator;
}
// slither-disable-next-line divide-before-multiply
adjustedNumerator = adjustedNumerator * adjustedNumerator / decayDenominator;
decayEpochs >>= 1;
}

colony.emitDomainReputationReward(1, msgSender(), int256(grantAmount));

emit GrantClaimed(msgSender(), grantAmount, _paid);
kronosapiens marked this conversation as resolved.
Show resolved Hide resolved
}

// View

function getToken() external view returns (address) {
return token;
}

function getDecayPeriod() external view returns (uint256) {
return decayPeriod;
}

function getDecayNumerator() external view returns (uint256) {
return decayNumerator;
}

function getDecayDenominator() external view returns (uint256) {
return decayDenominator;
}

function getTotalPayableGrants() external view returns (uint256) {
return totalPayableGrants;
}

function getGrant(bool _paid, bytes32 _hashedSecret) external view returns (Grant memory grant) {
grant = grants[_paid][_hashedSecret];
}

function getCommittedSecret(bytes32 _addressHash) external view returns (uint256) {
return committedSecrets[_addressHash];
}
}
4 changes: 3 additions & 1 deletion helpers/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ const ACTIVE_TASK_STATE = 0;
const CANCELLED_TASK_STATE = 1;
const FINALIZED_TASK_STATE = 2;

const SECONDS_PER_DAY = 86400;
const SECONDS_PER_HOUR = 60 * 60;
const SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR;

const MINING_CYCLE_DURATION = 60 * 60 * 1; // 1 hour
const CHALLENGE_RESPONSE_WINDOW_DURATION = 60 * 20; // Twenty minutes
Expand Down Expand Up @@ -111,6 +112,7 @@ module.exports = {
ACTIVE_TASK_STATE,
CANCELLED_TASK_STATE,
FINALIZED_TASK_STATE,
SECONDS_PER_HOUR,
SECONDS_PER_DAY,
MINING_CYCLE_DURATION,
CHALLENGE_RESPONSE_WINDOW_DURATION,
Expand Down
4 changes: 3 additions & 1 deletion migrations/9_setup_extensions.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ const EvaluatedExpenditure = artifacts.require("./EvaluatedExpenditure");
const StakedExpenditure = artifacts.require("./StakedExpenditure");
const FundingQueue = artifacts.require("./FundingQueue");
const OneTxPayment = artifacts.require("./OneTxPayment");
const ReputationBootstrapper = artifacts.require("./ReputationBootstrapper");
const StreamingPayments = artifacts.require("./StreamingPayments");
const VotingReputation = artifacts.require("./VotingReputation");
const VotingReputationMisalignedRecovery = artifacts.require("./VotingReputationMisalignedRecovery");
Expand Down Expand Up @@ -44,9 +45,10 @@ module.exports = async function (deployer, network, accounts) {

await addExtension(colonyNetwork, "CoinMachine", "CoinMachine", [CoinMachine]);
await addExtension(colonyNetwork, "EvaluatedExpenditure", "EvaluatedExpenditure", [EvaluatedExpenditure]);
await addExtension(colonyNetwork, "StakedExpenditure", "StakedExpenditure", [StakedExpenditure]);
await addExtension(colonyNetwork, "FundingQueue", "FundingQueue", [FundingQueue]);
await addExtension(colonyNetwork, "OneTxPayment", "OneTxPayment", [OneTxPayment]);
await addExtension(colonyNetwork, "ReputationBootstrapper", "ReputationBootstrapper", [ReputationBootstrapper]);
await addExtension(colonyNetwork, "StakedExpenditure", "StakedExpenditure", [StakedExpenditure]);
await addExtension(colonyNetwork, "StreamingPayments", "StreamingPayments", [StreamingPayments]);
await addExtension(colonyNetwork, "TokenSupplier", "TokenSupplier", [TokenSupplier]);
await addExtension(colonyNetwork, "IVotingReputation", "VotingReputation", [VotingReputation, VotingReputationMisalignedRecovery]);
Expand Down
1 change: 1 addition & 0 deletions scripts/check-recovery.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ walkSync("./contracts/").forEach((contractName) => {
"contracts/extensions/StakedExpenditure.sol",
"contracts/extensions/FundingQueue.sol",
"contracts/extensions/OneTxPayment.sol",
"contracts/extensions/ReputationBootstrapper.sol",
"contracts/extensions/StreamingPayments.sol",
"contracts/extensions/TokenSupplier.sol",
"contracts/extensions/votingReputation/VotingReputation.sol",
Expand Down
1 change: 1 addition & 0 deletions scripts/check-storage.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ walkSync("./contracts/").forEach((contractName) => {
"contracts/extensions/ColonyExtension.sol",
"contracts/extensions/ColonyExtensionMeta.sol",
"contracts/extensions/OneTxPayment.sol",
"contracts/extensions/ReputationBootstrapper.sol",
"contracts/extensions/StreamingPayments.sol",
"contracts/extensions/TokenSupplier.sol",
"contracts/extensions/votingReputation/VotingReputationMisalignedRecovery.sol",
Expand Down
61 changes: 61 additions & 0 deletions test-gas-costs/gasCosts.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/* globals artifacts */

const path = require("path");
const { soliditySha3 } = require("web3-utils");
const { TruffleLoader } = require("../packages/package-utils");

const {
Expand All @@ -17,6 +18,7 @@ const {
RATING_2_SECRET,
SPECIFICATION_HASH,
DELIVERABLE_HASH,
SECONDS_PER_HOUR,
SECONDS_PER_DAY,
DEFAULT_STAKE,
INITIAL_FUNDING,
Expand Down Expand Up @@ -49,6 +51,7 @@ const IColonyNetwork = artifacts.require("IColonyNetwork");
const EtherRouter = artifacts.require("EtherRouter");
const ITokenLocking = artifacts.require("ITokenLocking");
const OneTxPayment = artifacts.require("OneTxPayment");
const ReputationBootstrapper = artifacts.require("ReputationBootstrapper");

const REAL_PROVIDER_PORT = process.env.SOLIDITY_COVERAGE ? 8555 : 8545;

Expand Down Expand Up @@ -368,5 +371,63 @@ contract("All", function (accounts) {
await forwardTime(5184001);
await newColony.finalizeRewardPayout(payoutId2);
});

it("when bootstrapping reputation", async function () {
const reputationBootstrapper = await ReputationBootstrapper.new();
await reputationBootstrapper.install(colony.address);
await colony.setRootRole(reputationBootstrapper.address, true);

await reputationBootstrapper.setGrants(
[false, false, false, false, false],
[soliditySha3(1), soliditySha3(2), soliditySha3(3), soliditySha3(4), soliditySha3(5)],
[WAD, WAD, WAD, WAD, WAD],
{ from: MANAGER }
);

await reputationBootstrapper.commitSecret(soliditySha3(WORKER, 1), { from: WORKER });
await forwardTime(SECONDS_PER_HOUR, this);

await reputationBootstrapper.claimGrant(false, 1, { from: WORKER });
});

it("when bootstrapping reputation with tokens", async function () {
const reputationBootstrapper = await ReputationBootstrapper.new();
await reputationBootstrapper.install(colony.address);
await colony.setRootRole(reputationBootstrapper.address, true);

await token.mint(reputationBootstrapper.address, WAD.muln(10));
await reputationBootstrapper.setGrants(
[true, true, true, true, true],
[soliditySha3(1), soliditySha3(2), soliditySha3(3), soliditySha3(4), soliditySha3(5)],
[WAD, WAD, WAD, WAD, WAD],
{ from: MANAGER }
);

await reputationBootstrapper.commitSecret(soliditySha3(WORKER, 1), { from: WORKER });
await forwardTime(SECONDS_PER_HOUR, this);

await reputationBootstrapper.claimGrant(true, 1, { from: WORKER });
});

it("when bootstrapping reputation with decay", async function () {
const reputationBootstrapper = await ReputationBootstrapper.new();
await reputationBootstrapper.install(colony.address);
await colony.setRootRole(reputationBootstrapper.address, true);

await reputationBootstrapper.setGrants(
[false, false, false, false, false],
[soliditySha3(1), soliditySha3(2), soliditySha3(3), soliditySha3(4), soliditySha3(5)],
[WAD, WAD, WAD, WAD, WAD],
{ from: MANAGER }
);

await reputationBootstrapper.commitSecret(soliditySha3(WORKER, 1), { from: WORKER });
await forwardTime(SECONDS_PER_HOUR, this);

// Reputation decays by half in 90 days
await forwardTime(SECONDS_PER_DAY * 90, this);

await reputationBootstrapper.claimGrant(false, 1, { from: WORKER });
});
});
});
4 changes: 2 additions & 2 deletions test-smoke/colony-storage-consistent.js
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,8 @@ contract("Contract Storage", (accounts) => {
console.log("miningCycleStateHash:", miningCycleStateHash);
console.log("tokenLockingStateHash:", tokenLockingStateHash);

expect(colonyNetworkStateHash).to.equal("0xa59c636e6dc71c79ac297dc2419e21f94e3bab0d47ada0723821b1c6fabebaf5");
expect(colonyStateHash).to.equal("0x7bb6c8a0c01bf0eda1b51e28739f3e7c43bc6bca8bb62331465b62dc6c5f7983");
expect(colonyNetworkStateHash).to.equal("0xa9289d3025a1f5e108b7b68f335327c1c5748015db91c78978679ba9832984e1");
expect(colonyStateHash).to.equal("0x54a0edcb2097270bd95d610dc827869cc827241d131461f58788f7c3257ca151");
expect(metaColonyStateHash).to.equal("0x15fab25907cfb6baedeaf1fdabd68678d37584a1817a08dfe77db60db378a508");
expect(miningCycleStateHash).to.equal("0x632d459a2197708bd2dbde87e8275c47dddcdf16d59e3efd21dcef9acb2a7366");
expect(tokenLockingStateHash).to.equal("0x30fbcbfbe589329fe20288101faabe1f60a4610ae0c0effb15526c6b390a8e07");
Expand Down
Loading