From f5e93175925d0309eb6d3da4480061efc9c8abbf Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Mon, 13 Feb 2023 13:02:27 -0800 Subject: [PATCH] Introduce Korporatio extension WIP --- contracts/extensions/Korporatio.sol | 198 ++++++++++++++++++++ migrations/9_setup_extensions.js | 2 + test/extensions/korporatio.js | 275 ++++++++++++++++++++++++++++ 3 files changed, 475 insertions(+) create mode 100644 contracts/extensions/Korporatio.sol create mode 100644 test/extensions/korporatio.js diff --git a/contracts/extensions/Korporatio.sol b/contracts/extensions/Korporatio.sol new file mode 100644 index 0000000000..29396e6c62 --- /dev/null +++ b/contracts/extensions/Korporatio.sol @@ -0,0 +1,198 @@ +/* + 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 . +*/ + +pragma solidity 0.7.3; +pragma experimental ABIEncoderV2; + +import "./../../lib/dappsys/erc20.sol"; +import "./../colonyNetwork/IColonyNetwork.sol"; +import "./ColonyExtensionMeta.sol"; + +// ignore-file-swc-108 + + +contract Korporatio is ColonyExtensionMeta { + + // Constants + + // Events + + event StakeSubmitted(uint256 indexed stakeId, address indexed staker); + event StakeCancelled(uint256 indexed stakeId); + event StakeReclaimed(uint256 indexed stakeId); + event StakeSlashed(uint256 indexed stakeId); + + // Data structures + + struct Stake { + address staker; + uint256 amount; + uint256 cancelledAt; + } + + // Storage + + address colonyNetworkAddress; + uint256 stakeFraction; + uint256 claimDelay; + + uint256 numStakes; + mapping (uint256 => Stake) stakes; + + // Modifiers + + modifier onlyRootArchitect() { + require( + colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Architecture), + "ux-gate-caller-not-root-architect" + ); + _; + } + + // Overrides + + /// @notice Returns the identifier of the extension + function identifier() public override pure returns (bytes32) { + return keccak256("Korporatio"); + } + + /// @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); + colonyNetworkAddress = colony.getColonyNetwork(); + } + + /// @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 { + deprecated = _deprecated; + } + + /// @notice Called when uninstalling the extension + function uninstall() public override auth { + selfdestruct(address(uint160(address(colony)))); + } + + // Public + + function setStakeFraction(uint256 _stakeFraction) public onlyRootArchitect { + stakeFraction = _stakeFraction; + } + + function setClaimDelay(uint256 _claimDelay) public onlyRootArchitect { + claimDelay = _claimDelay; + } + + function submitStake( + bytes memory _colonyKey, + bytes memory _colonyValue, + uint256 _colonyBranchMask, + bytes32[] memory _colonySiblings, + bytes memory _userKey, + bytes memory _userValue, + uint256 _userBranchMask, + bytes32[] memory _userSiblings + ) + public + notDeprecated + { + bytes32 rootHash = IColonyNetwork(colonyNetworkAddress).getReputationRootHash(); + uint256 rootSkillId = colony.getDomain(1).skillId; + + uint256 colonyReputation = checkReputation(rootHash, rootSkillId, address(0x0), _colonyKey, _colonyValue, _colonyBranchMask, _colonySiblings); + uint256 userReputation = checkReputation(rootHash, rootSkillId, msgSender(), _userKey, _userValue, _userBranchMask, _userSiblings); + + uint256 requiredStake = wmul(colonyReputation, stakeFraction); + require(userReputation >= requiredStake, "ux-gate-insufficient-rep"); + + stakes[++numStakes] = Stake({ + staker: msgSender(), + amount: requiredStake, + cancelledAt: UINT256_MAX + }); + + colony.obligateStake(msgSender(), 1, requiredStake); + + emit StakeSubmitted(numStakes, msgSender()); + } + + function cancelStake(uint256 _stakeId) public { + require(stakes[_stakeId].staker == msgSender(), "ux-gate-cannot-cancel"); + + stakes[_stakeId].cancelledAt = block.timestamp; + + emit StakeCancelled(_stakeId); + } + + function reclaimStake(uint256 _stakeId) public { + Stake storage stake = stakes[_stakeId]; + require(stake.cancelledAt + claimDelay <= block.timestamp, "ux-gate-cannot-reclaim"); + + uint256 stakeAmount = stake.amount; + colony.deobligateStake(msgSender(), 1, stakeAmount); + + delete stakes[_stakeId]; + + emit StakeReclaimed(_stakeId); + } + + function slashStake(uint256 _stakeId, bool _punish) public { + require(stakes[_stakeId].amount > 0, "ux-gate-cannot-slash"); + + require( + colony.hasInheritedUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Arbitration, UINT256_MAX, 1), + "staked-expenditure-caller-not-arbitration" + ); + + address staker = stakes[_stakeId].staker; + uint256 amount = stakes[_stakeId].amount; + delete stakes[_stakeId]; + + colony.transferStake(1, UINT256_MAX, address(this), staker, 1, amount, address(0x0)); + if (_punish) { colony.emitDomainReputationPenalty(1, UINT256_MAX, 1, staker, -int256(amount)); } + + emit StakeSlashed(_stakeId); + } + + // View + + function getNumStakes() external view returns (uint256) { + return numStakes; + } + + function getStakeFraction() external view returns (uint256) { + return stakeFraction; + } + + function getClaimDelay() external view returns (uint256) { + return claimDelay; + } + + function getStake(uint256 _id) external view returns (Stake memory stake) { + return stakes[_id]; + } +} diff --git a/migrations/9_setup_extensions.js b/migrations/9_setup_extensions.js index cef35d8955..8eff039ded 100644 --- a/migrations/9_setup_extensions.js +++ b/migrations/9_setup_extensions.js @@ -15,6 +15,7 @@ const VotingReputation = artifacts.require("./VotingReputation"); const VotingReputationMisalignedRecovery = artifacts.require("./VotingReputationMisalignedRecovery"); const TokenSupplier = artifacts.require("./TokenSupplier"); const Whitelist = artifacts.require("./Whitelist"); +const Korporatio = artifacts.require("./Korporatio"); const Resolver = artifacts.require("./Resolver"); const EtherRouter = artifacts.require("./EtherRouter"); @@ -53,4 +54,5 @@ module.exports = async function (deployer, network, accounts) { await addExtension(colonyNetwork, "TokenSupplier", "TokenSupplier", [TokenSupplier]); await addExtension(colonyNetwork, "IVotingReputation", "VotingReputation", [VotingReputation, VotingReputationMisalignedRecovery]); await addExtension(colonyNetwork, "Whitelist", "Whitelist", [Whitelist]); + await addExtension(colonyNetwork, "Korporatio", "Korporatio", [Korporatio]); }; diff --git a/test/extensions/korporatio.js b/test/extensions/korporatio.js new file mode 100644 index 0000000000..c7e2f26453 --- /dev/null +++ b/test/extensions/korporatio.js @@ -0,0 +1,275 @@ +/* globals artifacts */ + +const chai = require("chai"); +const bnChai = require("bn-chai"); +const { ethers } = require("ethers"); +const { soliditySha3 } = require("web3-utils"); + +const { UINT256_MAX, WAD, MINING_CYCLE_DURATION, SECONDS_PER_DAY, CHALLENGE_RESPONSE_WINDOW_DURATION } = require("../../helpers/constants"); + +const { + checkErrorRevert, + web3GetCode, + makeReputationKey, + makeReputationValue, + getActiveRepCycle, + forwardTime, + getBlockTime, +} = require("../../helpers/test-helper"); + +const { setupRandomColony, getMetaTransactionParameters } = require("../../helpers/test-data-generator"); + +const PatriciaTree = require("../../packages/reputation-miner/patricia"); + +const { expect } = chai; +chai.use(bnChai(web3.utils.BN)); + +const IColonyNetwork = artifacts.require("IColonyNetwork"); +const EtherRouter = artifacts.require("EtherRouter"); +const IReputationMiningCycle = artifacts.require("IReputationMiningCycle"); +const TokenLocking = artifacts.require("TokenLocking"); +const Korporatio = artifacts.require("Korporatio"); + +const KORPORATIO = soliditySha3("Korporatio"); + +contract("Korporatio", (accounts) => { + let colony; + let token; + let domain1; + let colonyNetwork; + let tokenLocking; + + let korporatio; + let version; + + let reputationTree; + + let domain1Key; + let domain1Value; + let domain1Mask; + let domain1Siblings; + + let user0Key; + let user0Value; + let user0Mask; + let user0Siblings; + + const USER0 = accounts[0]; + const USER1 = accounts[1]; + const MINER = accounts[5]; + + before(async () => { + const etherRouter = await EtherRouter.deployed(); + colonyNetwork = await IColonyNetwork.at(etherRouter.address); + + const tokenLockingAddress = await colonyNetwork.getTokenLocking(); + tokenLocking = await TokenLocking.at(tokenLockingAddress); + + const extension = await Korporatio.new(); + version = await extension.version(); + }); + + beforeEach(async () => { + ({ colony, token } = await setupRandomColony(colonyNetwork)); + domain1 = await colony.getDomain(1); + + await colony.installExtension(KORPORATIO, version); + const korporatioAddress = await colonyNetwork.getExtensionInstallation(KORPORATIO, colony.address); + korporatio = await Korporatio.at(korporatioAddress); + + await colony.setArchitectureRole(1, UINT256_MAX, USER0, 1, true); + await colony.setArbitrationRole(1, UINT256_MAX, USER1, 1, true); + await colony.setArbitrationRole(1, UINT256_MAX, korporatio.address, 1, true); + + await token.mint(USER0, WAD); + await token.approve(tokenLocking.address, WAD, { from: USER0 }); + await tokenLocking.methods["deposit(address,uint256,bool)"](token.address, WAD, true, { from: USER0 }); + + reputationTree = new PatriciaTree(); + reputationTree.insert( + makeReputationKey(colony.address, domain1.skillId), // Colony total + makeReputationValue(WAD.muln(3), 1) + ); + reputationTree.insert( + makeReputationKey(colony.address, domain1.skillId, USER0), // User0 + makeReputationValue(WAD.muln(2), 2) + ); + reputationTree.insert( + makeReputationKey(colony.address, domain1.skillId, USER1), // User1 + makeReputationValue(WAD, 3) + ); + + domain1Key = makeReputationKey(colony.address, domain1.skillId); + domain1Value = makeReputationValue(WAD.muln(3), 1); + [domain1Mask, domain1Siblings] = reputationTree.getProof(domain1Key); + + user0Key = makeReputationKey(colony.address, domain1.skillId, USER0); + user0Value = makeReputationValue(WAD.muln(2), 2); + [user0Mask, user0Siblings] = reputationTree.getProof(user0Key); + + const rootHash = reputationTree.getRootHash(); + const repCycle = await getActiveRepCycle(colonyNetwork); + await forwardTime(MINING_CYCLE_DURATION, this); + await repCycle.submitRootHash(rootHash, 0, "0x00", 10, { from: MINER }); + await forwardTime(CHALLENGE_RESPONSE_WINDOW_DURATION + 1, this); + await repCycle.confirmNewHash(0, { from: MINER }); + }); + + describe("managing the extension", async () => { + it("can install the extension manually", async () => { + korporatio = await Korporatio.new(); + await korporatio.install(colony.address); + + await checkErrorRevert(korporatio.install(colony.address), "extension-already-installed"); + + const identifier = await korporatio.identifier(); + expect(identifier).to.equal(KORPORATIO); + + const capabilityRoles = await korporatio.getCapabilityRoles("0x0"); + expect(capabilityRoles).to.equal(ethers.constants.HashZero); + + await korporatio.finishUpgrade(); + await korporatio.deprecate(true); + await korporatio.uninstall(); + + const code = await web3GetCode(korporatio.address); + expect(code).to.equal("0x"); + }); + + it("can install the extension with the extension manager", async () => { + ({ colony } = await setupRandomColony(colonyNetwork)); + await colony.installExtension(KORPORATIO, version, { from: USER0 }); + + await checkErrorRevert(colony.installExtension(KORPORATIO, version, { from: USER0 }), "colony-network-extension-already-installed"); + await checkErrorRevert(colony.uninstallExtension(KORPORATIO, { from: USER1 }), "ds-auth-unauthorized"); + + await colony.uninstallExtension(KORPORATIO, { from: USER0 }); + }); + + it("can deprecate the extension if root", async () => { + let deprecated = await korporatio.getDeprecated(); + expect(deprecated).to.equal(false); + + await checkErrorRevert(colony.deprecateExtension(KORPORATIO, true, { from: USER1 }), "ds-auth-unauthorized"); + await colony.deprecateExtension(KORPORATIO, true); + + deprecated = await korporatio.getDeprecated(); + expect(deprecated).to.equal(true); + }); + + it("can't use the network-level functions if installed via ColonyNetwork", async () => { + // await checkErrorRevert(korporatio.install(ADDRESS_ZERO, { from: USER1 }), "ds-auth-unauthorized"); + await checkErrorRevert(korporatio.finishUpgrade({ from: USER1 }), "ds-auth-unauthorized"); + await checkErrorRevert(korporatio.deprecate(true, { from: USER1 }), "ds-auth-unauthorized"); + await checkErrorRevert(korporatio.uninstall({ from: USER1 }), "ds-auth-unauthorized"); + }); + }); + + describe("submitting stakes", async () => { + beforeEach(async () => { + await korporatio.setStakeFraction(WAD.divn(100), { from: USER0 }); + await korporatio.setClaimDelay(SECONDS_PER_DAY, { from: USER0 }); + + await colony.approveStake(korporatio.address, 1, WAD, { from: USER0 }); + }); + + it("can submit a stake", async () => { + await korporatio.submitStake(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }); + + const stakeId = await korporatio.getNumStakes(); + const stake = await korporatio.getStake(stakeId); + expect(stake.staker).to.equal(USER0); + expect(stake.amount).to.eq.BN(WAD.divn(100).muln(3)); + expect(stake.cancelledAt).to.eq.BN(UINT256_MAX); + + const obligation = await colony.getObligation(USER0, korporatio.address, 1); + expect(obligation).to.eq.BN(WAD.divn(100).muln(3)); + }); + + it("can cancel a stake", async () => { + await korporatio.submitStake(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }); + + const stakeId = await korporatio.getNumStakes(); + + // Only staker can cancel + await checkErrorRevert(korporatio.cancelStake(stakeId, { from: USER1 }), "ux-gate-cannot-cancel"); + + const tx = await korporatio.cancelStake(stakeId, { from: USER0 }); + const blockTime = await getBlockTime(tx.receipt.blockNumber); + + const stake = await korporatio.getStake(stakeId); + expect(stake.cancelledAt).to.eq.BN(blockTime); + }); + + it("can reclaim a stake", async () => { + await korporatio.submitStake(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }); + + const stakeId = await korporatio.getNumStakes(); + await korporatio.cancelStake(stakeId, { from: USER0 }); + + // Cannot reclaim before claim delay elapses + await checkErrorRevert(korporatio.reclaimStake(stakeId), "ux-gate-cannot-reclaim"); + + await forwardTime(SECONDS_PER_DAY, this); + + await korporatio.reclaimStake(stakeId); + + const obligation = await colony.getObligation(USER0, korporatio.address, 1); + expect(obligation).to.be.zero; + }); + + it("can slash a stake", async () => { + await korporatio.submitStake(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }); + + const stakeId = await korporatio.getNumStakes(); + await korporatio.slashStake(stakeId, false, { from: USER1 }); + + const obligation = await colony.getObligation(USER0, korporatio.address, 1); + expect(obligation).to.be.zero; + }); + + it("can slash a stake and punish", async () => { + await korporatio.submitStake(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }); + + const stakeId = await korporatio.getNumStakes(); + await korporatio.slashStake(stakeId, true, { from: USER1 }); + + const obligation = await colony.getObligation(USER0, korporatio.address, 1); + expect(obligation).to.be.zero; + + // Staker gets a reputation penalty + const addr = await colonyNetwork.getReputationMiningCycle(false); + const repCycle = await IReputationMiningCycle.at(addr); + const numUpdates = await repCycle.getReputationUpdateLogLength(); + const repUpdate = await repCycle.getReputationUpdateLogEntry(numUpdates.subn(1)); + + expect(repUpdate.user).to.equal(USER0); + expect(repUpdate.amount).to.eq.BN(WAD.divn(100).muln(3).neg()); + expect(repUpdate.skillId).to.eq.BN(domain1.skillId); + }); + + it("can submit a stake via metatransactions", async () => { + await colony.approveStake(korporatio.address, 1, WAD, { from: USER0 }); + + const txData = await korporatio.contract.methods + .submitStake(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings) + .encodeABI(); + const { r, s, v } = await getMetaTransactionParameters(txData, USER0, korporatio.address); + await korporatio.executeMetaTransaction(USER0, txData, r, s, v, { from: USER0 }); + + const stakeId = await korporatio.getNumStakes(); + const stake = await korporatio.getStake(stakeId); + expect(stake.staker).to.equal(USER0); + }); + }); +});