From 203a26d49dcf84346b37d999820ad2d277c895da Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Mon, 13 Feb 2023 13:02:27 -0800 Subject: [PATCH 1/3] Introduce Korporatio extension --- contracts/extensions/Korporatio.sol | 253 +++++++++++++++ migrations/9_setup_extensions.js | 2 + scripts/check-recovery.js | 1 + scripts/check-storage.js | 1 + test-smoke/colony-storage-consistent.js | 4 +- test/extensions/korporatio.js | 388 ++++++++++++++++++++++++ 6 files changed, 647 insertions(+), 2 deletions(-) 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..75f71ecf15 --- /dev/null +++ b/contracts/extensions/Korporatio.sol @@ -0,0 +1,253 @@ +/* + 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.8.19; +pragma experimental ABIEncoderV2; + +import "./../../lib/dappsys/erc20.sol"; +import "./../colonyNetwork/IColonyNetwork.sol"; +import "./ColonyExtensionMeta.sol"; + +// ignore-file-swc-108 + + +contract Korporatio is ColonyExtensionMeta { + + // Constants + + uint256 constant APPLICATION_FEE = 6500 * WAD; + + // Events + + event ApplicationCreated(uint256 indexed stakeId, address indexed applicant); + event ApplicationCancelled(uint256 indexed stakeId); + event StakeReclaimed(uint256 indexed stakeId); + event StakeSlashed(uint256 indexed stakeId); + event ApplicationUpdated(uint256 indexed stakeId, bytes32 ipfsHash); + event ApplicationSubmitted(uint256 indexed stakeId, bytes32 ipfsHash); + + // Data structures + + struct Application { + address applicant; + uint256 stakeAmount; + uint256 cancelledAt; + } + + // Storage + + address colonyNetworkAddress; + + address paymentToken; + uint256 applicationFee; + uint256 stakeFraction; + uint256 claimDelay; + + uint256 numApplications; + mapping (uint256 => Application) applications; + + // Modifiers + + // 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(payable(address(colony))); + } + + // Public + + function initialise( + address _paymentToken, + uint256 _applicationFee, + uint256 _stakeFraction, + uint256 _claimDelay + ) + public + { + require( + colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Architecture), + "korporatio-not-root-architect" + ); + + paymentToken = _paymentToken; + applicationFee = _applicationFee; + stakeFraction = _stakeFraction; + claimDelay = _claimDelay; + } + + function createApplication( + 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, "korporatio-insufficient-rep"); + + applications[++numApplications] = Application({ + applicant: msgSender(), + stakeAmount: requiredStake, + cancelledAt: UINT256_MAX + }); + + colony.obligateStake(msgSender(), 1, requiredStake); + + emit ApplicationCreated(numApplications, msgSender()); + } + + function createFreeApplication() public notDeprecated { + require ( + colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root) || + colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Administration), + "korporatio-must-submit-stake" + ); + + applications[++numApplications] = Application({ + applicant: msgSender(), + stakeAmount: 0, + cancelledAt: UINT256_MAX + }); + + emit ApplicationCreated(numApplications, msgSender()); + } + + function cancelApplication(uint256 _applicationId) public { + require(applications[_applicationId].applicant == msgSender(), "korporatio-cannot-cancel"); + + applications[_applicationId].cancelledAt = block.timestamp; + + emit ApplicationCancelled(_applicationId); + } + + function reclaimStake(uint256 _applicationId) public { + require( + applications[_applicationId].cancelledAt + claimDelay <= block.timestamp, + "korporatio-cannot-reclaim" + ); + + uint256 stakeAmount = applications[_applicationId].stakeAmount; + delete applications[_applicationId]; + + colony.deobligateStake(msgSender(), 1, stakeAmount); + + emit StakeReclaimed(_applicationId); + } + + function slashStake(uint256 _applicationId, bool _punish) public { + require(applications[_applicationId].stakeAmount > 0, "korporatio-cannot-slash"); + + require( + colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Arbitration), + "korporatio-caller-not-arbitration" + ); + + address applicant = applications[_applicationId].applicant; + uint256 stakeAmount = applications[_applicationId].stakeAmount; + delete applications[_applicationId]; + + colony.transferStake(1, UINT256_MAX, address(this), applicant, 1, stakeAmount, address(0x0)); + if (_punish) { colony.emitDomainReputationPenalty(1, UINT256_MAX, 1, applicant, -int256(stakeAmount)); } + + emit StakeSlashed(_applicationId); + } + + function updateApplication(uint256 _applicationId, bytes32 _ipfsHash) public { + require(applications[_applicationId].applicant == msgSender(), "korporatio-not-applicant"); + require(applications[_applicationId].cancelledAt == UINT256_MAX, "korporatio-stake-cancelled"); + + emit ApplicationUpdated(_applicationId, _ipfsHash); + } + + function submitApplication(uint256 _applicationId, bytes32 _ipfsHash) public { + require(colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root), "korporatio-caller-not-root"); + require(applications[_applicationId].cancelledAt == UINT256_MAX, "korporatio-stake-cancelled"); + + applications[_applicationId].cancelledAt = block.timestamp; + + address metaColony = IColonyNetwork(colonyNetworkAddress).getMetaColony(); + require(ERC20(paymentToken).transferFrom(msgSender(), metaColony, applicationFee), "korporatio-transfer-failed"); + + emit ApplicationSubmitted(_applicationId, _ipfsHash); + } + + // View + + function getPaymentToken() external view returns (address) { + return paymentToken; + } + + function getApplicationFee() external view returns (uint256) { + return applicationFee; + } + + function getStakeFraction() external view returns (uint256) { + return stakeFraction; + } + + function getClaimDelay() external view returns (uint256) { + return claimDelay; + } + + function getNumApplications() external view returns (uint256) { + return numApplications; + } + + function getApplication(uint256 _id) external view returns (Application memory application) { + application = applications[_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/scripts/check-recovery.js b/scripts/check-recovery.js index bd241d873c..509bbb09b7 100755 --- a/scripts/check-recovery.js +++ b/scripts/check-recovery.js @@ -53,6 +53,7 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/extensions/EvaluatedExpenditure.sol", "contracts/extensions/StakedExpenditure.sol", "contracts/extensions/FundingQueue.sol", + "contracts/extensions/Korporatio.sol", "contracts/extensions/OneTxPayment.sol", "contracts/extensions/ReputationBootstrapper.sol", "contracts/extensions/StreamingPayments.sol", diff --git a/scripts/check-storage.js b/scripts/check-storage.js index ab218dbdb6..8de36fd672 100755 --- a/scripts/check-storage.js +++ b/scripts/check-storage.js @@ -33,6 +33,7 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/extensions/FundingQueue.sol", "contracts/extensions/ColonyExtension.sol", "contracts/extensions/ColonyExtensionMeta.sol", + "contracts/extensions/Korporatio.sol", "contracts/extensions/OneTxPayment.sol", "contracts/extensions/ReputationBootstrapper.sol", "contracts/extensions/StreamingPayments.sol", diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index a95afa2325..3b095e0ff8 100644 --- a/test-smoke/colony-storage-consistent.js +++ b/test-smoke/colony-storage-consistent.js @@ -149,8 +149,8 @@ contract("Contract Storage", (accounts) => { console.log("miningCycleStateHash:", miningCycleStateHash); console.log("tokenLockingStateHash:", tokenLockingStateHash); - expect(colonyNetworkStateHash).to.equal("0x711cd2107e1466eee7e531b4abee44853d8ac1a0708f31004a615f57f3de819c"); - expect(colonyStateHash).to.equal("0x54a0edcb2097270bd95d610dc827869cc827241d131461f58788f7c3257ca151"); + expect(colonyNetworkStateHash).to.equal("0xc4320825b94c76e32b6e2f05dceb178ad070568dd470a469282f620f3152c7ce"); + expect(colonyStateHash).to.equal("0xefa81fadaf3890213272d8621d411d8d476997660d320aa9bcf5d6d3b898f3b3"); expect(metaColonyStateHash).to.equal("0x15fab25907cfb6baedeaf1fdabd68678d37584a1817a08dfe77db60db378a508"); expect(miningCycleStateHash).to.equal("0x632d459a2197708bd2dbde87e8275c47dddcdf16d59e3efd21dcef9acb2a7366"); expect(tokenLockingStateHash).to.equal("0x30fbcbfbe589329fe20288101faabe1f60a4610ae0c0effb15526c6b390a8e07"); diff --git a/test/extensions/korporatio.js b/test/extensions/korporatio.js new file mode 100644 index 0000000000..2bdde5b93c --- /dev/null +++ b/test/extensions/korporatio.js @@ -0,0 +1,388 @@ +/* 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, + expectEvent, +} = 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 EtherRouter = artifacts.require("EtherRouter"); +const IColonyNetwork = artifacts.require("IColonyNetwork"); +const IReputationMiningCycle = artifacts.require("IReputationMiningCycle"); +const Korporatio = artifacts.require("Korporatio"); +const TokenLocking = artifacts.require("TokenLocking"); + +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 USER2 = accounts[2]; + const MINER = accounts[5]; + + const APPLICATION_FEE = WAD.muln(6500); + + 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.setAdministrationRole(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("creating applications", async () => { + beforeEach(async () => { + await korporatio.initialise(token.address, APPLICATION_FEE, WAD.divn(100), SECONDS_PER_DAY, { from: USER0 }); + + await colony.approveStake(korporatio.address, 1, WAD, { from: USER0 }); + }); + + it("can query for configuration params", async () => { + const paymentToken = await korporatio.getPaymentToken(); + const applicationFee = await korporatio.getApplicationFee(); + const stakeFraction = await korporatio.getStakeFraction(); + const claimDelay = await korporatio.getClaimDelay(); + + expect(paymentToken).to.equal(token.address); + expect(applicationFee).to.eq.BN(APPLICATION_FEE); + expect(stakeFraction).to.eq.BN(WAD.divn(100)); + expect(claimDelay).to.eq.BN(SECONDS_PER_DAY); + }); + + it("cannot set configuration params if not root architect", async () => { + await checkErrorRevert( + korporatio.initialise(token.address, APPLICATION_FEE, WAD.divn(100), SECONDS_PER_DAY, { from: USER1 }), + "korporatio-not-root-architect" + ); + }); + + it("can create an application", async () => { + await korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }); + + const applicationId = await korporatio.getNumApplications(); + const application = await korporatio.getApplication(applicationId); + expect(application.applicant).to.equal(USER0); + expect(application.stakeAmount).to.eq.BN(WAD.divn(100).muln(3)); + expect(application.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 create a free application if root or admin", async () => { + await korporatio.createFreeApplication({ from: USER1 }); + + const applicationId = await korporatio.getNumApplications(); + const application = await korporatio.getApplication(applicationId); + expect(application.applicant).to.equal(USER1); + expect(application.stakeAmount).to.be.zero; + expect(application.cancelledAt).to.eq.BN(UINT256_MAX); + + // Must have root or admin role + await checkErrorRevert(korporatio.createFreeApplication({ from: USER2 }), "korporatio-must-submit-stake"); + }); + + it("cannot create an application with insufficient rep", async () => { + await korporatio.initialise(token.address, APPLICATION_FEE, WAD, SECONDS_PER_DAY, { from: USER0 }); + + await checkErrorRevert( + korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }), + "korporatio-insufficient-rep" + ); + }); + + it("cannot create an application if deprecated", async () => { + await colony.deprecateExtension(KORPORATIO, true); + + await checkErrorRevert( + korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }), + "colony-extension-deprecated" + ); + + await checkErrorRevert(korporatio.createFreeApplication({ from: USER1 }), "colony-extension-deprecated"); + }); + + it("can cancel an application", async () => { + await korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }); + + const applicationId = await korporatio.getNumApplications(); + + // Only applicant can cancel + await checkErrorRevert(korporatio.cancelApplication(applicationId, { from: USER1 }), "korporatio-cannot-cancel"); + + const tx = await korporatio.cancelApplication(applicationId, { from: USER0 }); + const blockTime = await getBlockTime(tx.receipt.blockNumber); + + const application = await korporatio.getApplication(applicationId); + expect(application.cancelledAt).to.eq.BN(blockTime); + }); + + it("can reclaim a stake", async () => { + await korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }); + + const applicationId = await korporatio.getNumApplications(); + await korporatio.cancelApplication(applicationId, { from: USER0 }); + + // Cannot reclaim before claim delay elapses + await checkErrorRevert(korporatio.reclaimStake(applicationId), "korporatio-cannot-reclaim"); + + await forwardTime(SECONDS_PER_DAY, this); + + await korporatio.reclaimStake(applicationId); + + const obligation = await colony.getObligation(USER0, korporatio.address, 1); + expect(obligation).to.be.zero; + }); + + it("can slash a stake", async () => { + await korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }); + + const applicationId = await korporatio.getNumApplications(); + await korporatio.slashStake(applicationId, 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.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }); + + const applicationId = await korporatio.getNumApplications(); + await korporatio.slashStake(applicationId, 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("cannot slash a nonexistent stake", async () => { + await checkErrorRevert(korporatio.slashStake(10, false, { from: USER1 }), "korporatio-cannot-slash"); + }); + + it("cannot slash if not an arbitration user", async () => { + await korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }); + + const applicationId = await korporatio.getNumApplications(); + await checkErrorRevert(korporatio.slashStake(applicationId, false, { from: USER2 }), "korporatio-caller-not-arbitration"); + }); + + it("can update an application", async () => { + await korporatio.createFreeApplication({ from: USER0 }); + + const applicationId = await korporatio.getNumApplications(); + const ipfsHash = soliditySha3("IPFS Hash"); + + const tx = await korporatio.updateApplication(applicationId, ipfsHash, { from: USER0 }); + await expectEvent(tx, "ApplicationUpdated", [applicationId, ipfsHash]); + + // Cannot update if not applicant + await checkErrorRevert(korporatio.updateApplication(applicationId, ipfsHash, { from: USER1 }), "korporatio-not-applicant"); + + // Cannot update once cancelled + await korporatio.cancelApplication(applicationId, { from: USER0 }); + await checkErrorRevert(korporatio.updateApplication(applicationId, ipfsHash, { from: USER0 }), "korporatio-stake-cancelled"); + }); + + it("can submit an application and pay the fee", async () => { + await token.mint(USER0, APPLICATION_FEE); + await token.approve(korporatio.address, APPLICATION_FEE); + + await korporatio.createFreeApplication({ from: USER0 }); + + const applicationId = await korporatio.getNumApplications(); + const ipfsHash = soliditySha3("IPFS Hash"); + + // Cannot submit if not root + await checkErrorRevert(korporatio.submitApplication(applicationId, ipfsHash, { from: USER1 }), "korporatio-caller-not-root"); + + const tx = await korporatio.submitApplication(applicationId, ipfsHash, { from: USER0 }); + await expectEvent(tx, "ApplicationSubmitted", [applicationId, ipfsHash]); + + const metaColonyAddress = await colonyNetwork.getMetaColony(); + const metaColonyBalance = await token.balanceOf(metaColonyAddress); + expect(metaColonyBalance).to.eq.BN(APPLICATION_FEE); + + // Cannot submit once cancelled + await checkErrorRevert(korporatio.submitApplication(applicationId, ipfsHash, { from: USER0 }), "korporatio-stake-cancelled"); + }); + + it("can submit a stake via metatransactions", async () => { + await colony.approveStake(korporatio.address, 1, WAD, { from: USER0 }); + + const txData = await korporatio.contract.methods + .createApplication(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 applicationId = await korporatio.getNumApplications(); + const application = await korporatio.getApplication(applicationId); + expect(application.applicant).to.equal(USER0); + }); + }); +}); From 72df91ec7c052c0f295dcee158a7567dcf1aa743 Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Mon, 3 Apr 2023 19:47:50 -0400 Subject: [PATCH 2/3] Update in response to review comments --- contracts/extensions/Korporatio.sol | 57 ++++-------- test-smoke/colony-storage-consistent.js | 2 +- test/extensions/korporatio.js | 111 ++++++++++++++++++------ 3 files changed, 102 insertions(+), 68 deletions(-) diff --git a/contracts/extensions/Korporatio.sol b/contracts/extensions/Korporatio.sol index 75f71ecf15..bf3a259fac 100644 --- a/contracts/extensions/Korporatio.sol +++ b/contracts/extensions/Korporatio.sol @@ -29,8 +29,6 @@ contract Korporatio is ColonyExtensionMeta { // Constants - uint256 constant APPLICATION_FEE = 6500 * WAD; - // Events event ApplicationCreated(uint256 indexed stakeId, address indexed applicant); @@ -38,7 +36,7 @@ contract Korporatio is ColonyExtensionMeta { event StakeReclaimed(uint256 indexed stakeId); event StakeSlashed(uint256 indexed stakeId); event ApplicationUpdated(uint256 indexed stakeId, bytes32 ipfsHash); - event ApplicationSubmitted(uint256 indexed stakeId, bytes32 ipfsHash); + event ApplicationSubmitted(uint256 indexed stakeId); // Data structures @@ -52,8 +50,6 @@ contract Korporatio is ColonyExtensionMeta { address colonyNetworkAddress; - address paymentToken; - uint256 applicationFee; uint256 stakeFraction; uint256 claimDelay; @@ -62,6 +58,11 @@ contract Korporatio is ColonyExtensionMeta { // Modifiers + modifier onlyApplicant(uint256 _applicationId) { + require(msgSender() == applications[_applicationId].applicant, "korporatio-not-applicant"); + _; + } + // Overrides /// @notice Returns the identifier of the extension @@ -98,21 +99,12 @@ contract Korporatio is ColonyExtensionMeta { // Public - function initialise( - address _paymentToken, - uint256 _applicationFee, - uint256 _stakeFraction, - uint256 _claimDelay - ) - public - { + function initialise(uint256 _stakeFraction, uint256 _claimDelay) public { require( colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Architecture), "korporatio-not-root-architect" ); - paymentToken = _paymentToken; - applicationFee = _applicationFee; stakeFraction = _stakeFraction; claimDelay = _claimDelay; } @@ -130,6 +122,8 @@ contract Korporatio is ColonyExtensionMeta { public notDeprecated { + require(stakeFraction > 0, "korporatio-not-initialised"); + bytes32 rootHash = IColonyNetwork(colonyNetworkAddress).getReputationRootHash(); uint256 rootSkillId = colony.getDomain(1).skillId; @@ -166,21 +160,18 @@ contract Korporatio is ColonyExtensionMeta { emit ApplicationCreated(numApplications, msgSender()); } - function cancelApplication(uint256 _applicationId) public { - require(applications[_applicationId].applicant == msgSender(), "korporatio-cannot-cancel"); - + function cancelApplication(uint256 _applicationId) public onlyApplicant(_applicationId) { applications[_applicationId].cancelledAt = block.timestamp; emit ApplicationCancelled(_applicationId); } - function reclaimStake(uint256 _applicationId) public { - require( - applications[_applicationId].cancelledAt + claimDelay <= block.timestamp, - "korporatio-cannot-reclaim" - ); + function reclaimStake(uint256 _applicationId) public onlyApplicant(_applicationId) { + Application storage application = applications[_applicationId]; + require(application.applicant == msgSender(), "korporatio-not-applicant"); + require(application.cancelledAt + claimDelay <= block.timestamp, "korporatio-cannot-reclaim"); - uint256 stakeAmount = applications[_applicationId].stakeAmount; + uint256 stakeAmount = application.stakeAmount; delete applications[_applicationId]; colony.deobligateStake(msgSender(), 1, stakeAmount); @@ -206,35 +197,23 @@ contract Korporatio is ColonyExtensionMeta { emit StakeSlashed(_applicationId); } - function updateApplication(uint256 _applicationId, bytes32 _ipfsHash) public { - require(applications[_applicationId].applicant == msgSender(), "korporatio-not-applicant"); + function updateApplication(uint256 _applicationId, bytes32 _ipfsHash) public onlyApplicant(_applicationId) { require(applications[_applicationId].cancelledAt == UINT256_MAX, "korporatio-stake-cancelled"); emit ApplicationUpdated(_applicationId, _ipfsHash); } - function submitApplication(uint256 _applicationId, bytes32 _ipfsHash) public { + function submitApplication(uint256 _applicationId) public { require(colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root), "korporatio-caller-not-root"); require(applications[_applicationId].cancelledAt == UINT256_MAX, "korporatio-stake-cancelled"); applications[_applicationId].cancelledAt = block.timestamp; - address metaColony = IColonyNetwork(colonyNetworkAddress).getMetaColony(); - require(ERC20(paymentToken).transferFrom(msgSender(), metaColony, applicationFee), "korporatio-transfer-failed"); - - emit ApplicationSubmitted(_applicationId, _ipfsHash); + emit ApplicationSubmitted(_applicationId); } // View - function getPaymentToken() external view returns (address) { - return paymentToken; - } - - function getApplicationFee() external view returns (uint256) { - return applicationFee; - } - function getStakeFraction() external view returns (uint256) { return stakeFraction; } diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index 3b095e0ff8..666a404478 100644 --- a/test-smoke/colony-storage-consistent.js +++ b/test-smoke/colony-storage-consistent.js @@ -150,7 +150,7 @@ contract("Contract Storage", (accounts) => { console.log("tokenLockingStateHash:", tokenLockingStateHash); expect(colonyNetworkStateHash).to.equal("0xc4320825b94c76e32b6e2f05dceb178ad070568dd470a469282f620f3152c7ce"); - expect(colonyStateHash).to.equal("0xefa81fadaf3890213272d8621d411d8d476997660d320aa9bcf5d6d3b898f3b3"); + expect(colonyStateHash).to.equal("0xdb586290061130dacc48f11eb373e4a01baf177c968ab68679d25e1794074f3a"); expect(metaColonyStateHash).to.equal("0x15fab25907cfb6baedeaf1fdabd68678d37584a1817a08dfe77db60db378a508"); expect(miningCycleStateHash).to.equal("0x632d459a2197708bd2dbde87e8275c47dddcdf16d59e3efd21dcef9acb2a7366"); expect(tokenLockingStateHash).to.equal("0x30fbcbfbe589329fe20288101faabe1f60a4610ae0c0effb15526c6b390a8e07"); diff --git a/test/extensions/korporatio.js b/test/extensions/korporatio.js index 2bdde5b93c..1b78aae4a9 100644 --- a/test/extensions/korporatio.js +++ b/test/extensions/korporatio.js @@ -1,5 +1,6 @@ /* globals artifacts */ +const { BN } = require("bn.js"); const chai = require("chai"); const bnChai = require("bn-chai"); const { ethers } = require("ethers"); @@ -16,6 +17,7 @@ const { forwardTime, getBlockTime, expectEvent, + encodeTxData, } = require("../../helpers/test-helper"); const { setupRandomColony, getMetaTransactionParameters } = require("../../helpers/test-data-generator"); @@ -28,10 +30,12 @@ chai.use(bnChai(web3.utils.BN)); const EtherRouter = artifacts.require("EtherRouter"); const IColonyNetwork = artifacts.require("IColonyNetwork"); const IReputationMiningCycle = artifacts.require("IReputationMiningCycle"); +const IVotingReputation = artifacts.require("IVotingReputation"); const Korporatio = artifacts.require("Korporatio"); const TokenLocking = artifacts.require("TokenLocking"); const KORPORATIO = soliditySha3("Korporatio"); +const VOTING_REPUTATION = soliditySha3("VotingReputation"); contract("Korporatio", (accounts) => { let colony; @@ -60,8 +64,6 @@ contract("Korporatio", (accounts) => { const USER2 = accounts[2]; const MINER = accounts[5]; - const APPLICATION_FEE = WAD.muln(6500); - before(async () => { const etherRouter = await EtherRouter.deployed(); colonyNetwork = await IColonyNetwork.at(etherRouter.address); @@ -168,32 +170,34 @@ contract("Korporatio", (accounts) => { await checkErrorRevert(korporatio.deprecate(true, { from: USER1 }), "ds-auth-unauthorized"); await checkErrorRevert(korporatio.uninstall({ from: USER1 }), "ds-auth-unauthorized"); }); + + it("cannot create applications unless initialised", async () => { + await checkErrorRevert( + korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }), + "korporatio-not-initialised" + ); + }); }); describe("creating applications", async () => { beforeEach(async () => { - await korporatio.initialise(token.address, APPLICATION_FEE, WAD.divn(100), SECONDS_PER_DAY, { from: USER0 }); - await colony.approveStake(korporatio.address, 1, WAD, { from: USER0 }); + + await korporatio.initialise(WAD.divn(100), SECONDS_PER_DAY, { from: USER0 }); }); it("can query for configuration params", async () => { - const paymentToken = await korporatio.getPaymentToken(); - const applicationFee = await korporatio.getApplicationFee(); const stakeFraction = await korporatio.getStakeFraction(); const claimDelay = await korporatio.getClaimDelay(); - expect(paymentToken).to.equal(token.address); - expect(applicationFee).to.eq.BN(APPLICATION_FEE); expect(stakeFraction).to.eq.BN(WAD.divn(100)); expect(claimDelay).to.eq.BN(SECONDS_PER_DAY); }); it("cannot set configuration params if not root architect", async () => { - await checkErrorRevert( - korporatio.initialise(token.address, APPLICATION_FEE, WAD.divn(100), SECONDS_PER_DAY, { from: USER1 }), - "korporatio-not-root-architect" - ); + await checkErrorRevert(korporatio.initialise(WAD.divn(100), SECONDS_PER_DAY, { from: USER1 }), "korporatio-not-root-architect"); }); it("can create an application", async () => { @@ -225,7 +229,7 @@ contract("Korporatio", (accounts) => { }); it("cannot create an application with insufficient rep", async () => { - await korporatio.initialise(token.address, APPLICATION_FEE, WAD, SECONDS_PER_DAY, { from: USER0 }); + await korporatio.initialise(WAD, SECONDS_PER_DAY, { from: USER0 }); await checkErrorRevert( korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { @@ -256,7 +260,7 @@ contract("Korporatio", (accounts) => { const applicationId = await korporatio.getNumApplications(); // Only applicant can cancel - await checkErrorRevert(korporatio.cancelApplication(applicationId, { from: USER1 }), "korporatio-cannot-cancel"); + await checkErrorRevert(korporatio.cancelApplication(applicationId, { from: USER1 }), "korporatio-not-applicant"); const tx = await korporatio.cancelApplication(applicationId, { from: USER0 }); const blockTime = await getBlockTime(tx.receipt.blockNumber); @@ -278,7 +282,7 @@ contract("Korporatio", (accounts) => { await forwardTime(SECONDS_PER_DAY, this); - await korporatio.reclaimStake(applicationId); + await korporatio.reclaimStake(applicationId, { from: USER0 }); const obligation = await colony.getObligation(USER0, korporatio.address, 1); expect(obligation).to.be.zero; @@ -331,6 +335,28 @@ contract("Korporatio", (accounts) => { await checkErrorRevert(korporatio.slashStake(applicationId, false, { from: USER2 }), "korporatio-caller-not-arbitration"); }); + it("can reclaim a stake via arbitration if the extension is deleted", async () => { + const korporatioAddress = korporatio.address; + await korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + from: USER0, + }); + + const lockPre = await tokenLocking.getUserLock(token.address, USER0); + const obligationPre = await colony.getObligation(USER0, korporatioAddress, 1); + expect(obligationPre).to.eq.BN(WAD.divn(100).muln(3)); + + await colony.uninstallExtension(KORPORATIO, { from: USER0 }); + + await colony.transferStake(1, UINT256_MAX, korporatioAddress, USER0, 1, obligationPre, USER0, { from: USER1 }); + + const lockPost = await tokenLocking.getUserLock(token.address, USER0); + const obligationPost = await colony.getObligation(USER0, korporatioAddress, 1); + + // Obligation is zeroed out, but token balance is unchanged + expect(obligationPost).to.be.zero; + expect(new BN(lockPre.balance)).to.eq.BN(lockPost.balance); + }); + it("can update an application", async () => { await korporatio.createFreeApplication({ from: USER0 }); @@ -348,27 +374,56 @@ contract("Korporatio", (accounts) => { await checkErrorRevert(korporatio.updateApplication(applicationId, ipfsHash, { from: USER0 }), "korporatio-stake-cancelled"); }); - it("can submit an application and pay the fee", async () => { - await token.mint(USER0, APPLICATION_FEE); - await token.approve(korporatio.address, APPLICATION_FEE); - + it("can submit an application", async () => { await korporatio.createFreeApplication({ from: USER0 }); const applicationId = await korporatio.getNumApplications(); - const ipfsHash = soliditySha3("IPFS Hash"); // Cannot submit if not root - await checkErrorRevert(korporatio.submitApplication(applicationId, ipfsHash, { from: USER1 }), "korporatio-caller-not-root"); + await checkErrorRevert(korporatio.submitApplication(applicationId, { from: USER1 }), "korporatio-caller-not-root"); + + const tx = await korporatio.submitApplication(applicationId, { from: USER0 }); + await expectEvent(tx, "ApplicationSubmitted", [applicationId]); + + // Cannot submit twice + await checkErrorRevert(korporatio.submitApplication(applicationId, { from: USER0 }), "korporatio-stake-cancelled"); + }); + + it("can submit an application via a motion", async () => { + await colony.installExtension(VOTING_REPUTATION, 9); + const votingAddress = await colonyNetwork.getExtensionInstallation(VOTING_REPUTATION, colony.address); + await colony.setArbitrationRole(1, UINT256_MAX, votingAddress, 1, true); + await colony.setRootRole(votingAddress, true); + const voting = await IVotingReputation.at(votingAddress); + + await voting.initialise(WAD.divn(1000), 0, 0, WAD, SECONDS_PER_DAY, SECONDS_PER_DAY, SECONDS_PER_DAY, SECONDS_PER_DAY); + + await korporatio.createFreeApplication({ from: USER0 }); + const applicationId = await korporatio.getNumApplications(); + + const action = await encodeTxData(korporatio, "submitApplication", [applicationId]); + + // Can't create a motion in a subdomain + await colony.addDomain(1, UINT256_MAX, 1); + await checkErrorRevert( + voting.createMotion(2, UINT256_MAX, korporatio.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings), + "voting-rep-invalid-domain-id" + ); + + // Only in the root domain + await voting.createMotion(1, UINT256_MAX, korporatio.address, action, domain1Key, domain1Value, domain1Mask, domain1Siblings); + const motionId = await voting.getMotionCount(); - const tx = await korporatio.submitApplication(applicationId, ipfsHash, { from: USER0 }); - await expectEvent(tx, "ApplicationSubmitted", [applicationId, ipfsHash]); + await colony.approveStake(voting.address, 1, WAD, { from: USER0 }); + await voting.stakeMotion(motionId, 1, UINT256_MAX, 1, WAD.muln(3).divn(1000), user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }); - const metaColonyAddress = await colonyNetwork.getMetaColony(); - const metaColonyBalance = await token.balanceOf(metaColonyAddress); - expect(metaColonyBalance).to.eq.BN(APPLICATION_FEE); + await forwardTime(SECONDS_PER_DAY, this); - // Cannot submit once cancelled - await checkErrorRevert(korporatio.submitApplication(applicationId, ipfsHash, { from: USER0 }), "korporatio-stake-cancelled"); + const tx = await voting.finalizeMotion(motionId); + const finalizedAt = await getBlockTime(tx.blockNumber); + + const application = await korporatio.getApplication(applicationId); + expect(application.cancelledAt).to.eq.BN(finalizedAt); }); it("can submit a stake via metatransactions", async () => { From 24a488e3fdd5559b1328b61848d3afdaba3c202f Mon Sep 17 00:00:00 2001 From: Daniel Kronovet Date: Mon, 24 Apr 2023 15:25:59 -0700 Subject: [PATCH 3/3] Update in response to review comments II --- contracts/extensions/Korporatio.sol | 34 ++++++++---- test/extensions/korporatio.js | 84 +++++++++++++++++------------ 2 files changed, 74 insertions(+), 44 deletions(-) diff --git a/contracts/extensions/Korporatio.sol b/contracts/extensions/Korporatio.sol index bf3a259fac..30e609c21a 100644 --- a/contracts/extensions/Korporatio.sol +++ b/contracts/extensions/Korporatio.sol @@ -34,7 +34,7 @@ contract Korporatio is ColonyExtensionMeta { event ApplicationCreated(uint256 indexed stakeId, address indexed applicant); event ApplicationCancelled(uint256 indexed stakeId); event StakeReclaimed(uint256 indexed stakeId); - event StakeSlashed(uint256 indexed stakeId); + event ApplicationDeleted(uint256 indexed stakeId, bool punish); event ApplicationUpdated(uint256 indexed stakeId, bytes32 ipfsHash); event ApplicationSubmitted(uint256 indexed stakeId); @@ -51,6 +51,7 @@ contract Korporatio is ColonyExtensionMeta { address colonyNetworkAddress; uint256 stakeFraction; + uint256 repFraction; uint256 claimDelay; uint256 numApplications; @@ -99,13 +100,14 @@ contract Korporatio is ColonyExtensionMeta { // Public - function initialise(uint256 _stakeFraction, uint256 _claimDelay) public { + function initialise(uint256 _stakeFraction, uint256 _repFraction, uint256 _claimDelay) public { require( colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Architecture), "korporatio-not-root-architect" ); stakeFraction = _stakeFraction; + repFraction = _repFraction; claimDelay = _claimDelay; } @@ -130,7 +132,7 @@ contract Korporatio is ColonyExtensionMeta { 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); + uint256 requiredStake = wmul(colonyReputation, repFraction); require(userReputation >= requiredStake, "korporatio-insufficient-rep"); applications[++numApplications] = Application({ @@ -144,7 +146,7 @@ contract Korporatio is ColonyExtensionMeta { emit ApplicationCreated(numApplications, msgSender()); } - function createFreeApplication() public notDeprecated { + function createApplication() public notDeprecated { require ( colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root) || colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Administration), @@ -160,13 +162,14 @@ contract Korporatio is ColonyExtensionMeta { emit ApplicationCreated(numApplications, msgSender()); } - function cancelApplication(uint256 _applicationId) public onlyApplicant(_applicationId) { + function cancelApplication(uint256 _applicationId) public { + require(msgSender() == applications[_applicationId].applicant, "korporatio-not-applicant"); applications[_applicationId].cancelledAt = block.timestamp; emit ApplicationCancelled(_applicationId); } - function reclaimStake(uint256 _applicationId) public onlyApplicant(_applicationId) { + function reclaimStake(uint256 _applicationId) public { Application storage application = applications[_applicationId]; require(application.applicant == msgSender(), "korporatio-not-applicant"); require(application.cancelledAt + claimDelay <= block.timestamp, "korporatio-cannot-reclaim"); @@ -179,7 +182,7 @@ contract Korporatio is ColonyExtensionMeta { emit StakeReclaimed(_applicationId); } - function slashStake(uint256 _applicationId, bool _punish) public { + function deleteApplication(uint256 _applicationId, bool _punish) public { require(applications[_applicationId].stakeAmount > 0, "korporatio-cannot-slash"); require( @@ -191,14 +194,23 @@ contract Korporatio is ColonyExtensionMeta { uint256 stakeAmount = applications[_applicationId].stakeAmount; delete applications[_applicationId]; - colony.transferStake(1, UINT256_MAX, address(this), applicant, 1, stakeAmount, address(0x0)); - if (_punish) { colony.emitDomainReputationPenalty(1, UINT256_MAX, 1, applicant, -int256(stakeAmount)); } + if (_punish) { + colony.emitDomainReputationPenalty(1, UINT256_MAX, 1, applicant, -int256(stakeAmount)); + colony.transferStake(1, UINT256_MAX, address(this), applicant, 1, stakeAmount, address(0x0)); + } else { + colony.deobligateStake(applicant, 1, stakeAmount); + } - emit StakeSlashed(_applicationId); + emit ApplicationDeleted(_applicationId, _punish); } - function updateApplication(uint256 _applicationId, bytes32 _ipfsHash) public onlyApplicant(_applicationId) { + function updateApplication(uint256 _applicationId, bytes32 _ipfsHash) public { require(applications[_applicationId].cancelledAt == UINT256_MAX, "korporatio-stake-cancelled"); + require( + msgSender() == applications[_applicationId].applicant || + colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root), + "korporatio-not-applicant-or-root" + ); emit ApplicationUpdated(_applicationId, _ipfsHash); } diff --git a/test/extensions/korporatio.js b/test/extensions/korporatio.js index 1b78aae4a9..731858d76c 100644 --- a/test/extensions/korporatio.js +++ b/test/extensions/korporatio.js @@ -45,6 +45,8 @@ contract("Korporatio", (accounts) => { let tokenLocking; let korporatio; + let createApplication; + let createFreeApplication; let version; let reputationTree; @@ -62,6 +64,7 @@ contract("Korporatio", (accounts) => { const USER0 = accounts[0]; const USER1 = accounts[1]; const USER2 = accounts[2]; + const USER3 = accounts[3]; const MINER = accounts[5]; before(async () => { @@ -82,10 +85,13 @@ contract("Korporatio", (accounts) => { await colony.installExtension(KORPORATIO, version); const korporatioAddress = await colonyNetwork.getExtensionInstallation(KORPORATIO, colony.address); korporatio = await Korporatio.at(korporatioAddress); + createApplication = korporatio.methods["createApplication(bytes,bytes,uint256,bytes32[],bytes,bytes,uint256,bytes32[])"]; + createFreeApplication = korporatio.methods["createApplication()"]; await colony.setArchitectureRole(1, UINT256_MAX, USER0, 1, true); await colony.setArbitrationRole(1, UINT256_MAX, USER1, 1, true); await colony.setAdministrationRole(1, UINT256_MAX, USER1, 1, true); + await colony.setRootRole(USER2, true); await colony.setArbitrationRole(1, UINT256_MAX, korporatio.address, 1, true); await token.mint(USER0, WAD); @@ -173,9 +179,7 @@ contract("Korporatio", (accounts) => { it("cannot create applications unless initialised", async () => { await checkErrorRevert( - korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { - from: USER0, - }), + createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0 }), "korporatio-not-initialised" ); }); @@ -185,7 +189,7 @@ contract("Korporatio", (accounts) => { beforeEach(async () => { await colony.approveStake(korporatio.address, 1, WAD, { from: USER0 }); - await korporatio.initialise(WAD.divn(100), SECONDS_PER_DAY, { from: USER0 }); + await korporatio.initialise(WAD.divn(100), WAD.divn(100), SECONDS_PER_DAY, { from: USER0 }); }); it("can query for configuration params", async () => { @@ -197,11 +201,11 @@ contract("Korporatio", (accounts) => { }); it("cannot set configuration params if not root architect", async () => { - await checkErrorRevert(korporatio.initialise(WAD.divn(100), SECONDS_PER_DAY, { from: USER1 }), "korporatio-not-root-architect"); + await checkErrorRevert(korporatio.initialise(WAD.divn(100), WAD.divn(100), SECONDS_PER_DAY, { from: USER1 }), "korporatio-not-root-architect"); }); it("can create an application", async () => { - await korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + await createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0, }); @@ -216,7 +220,7 @@ contract("Korporatio", (accounts) => { }); it("can create a free application if root or admin", async () => { - await korporatio.createFreeApplication({ from: USER1 }); + await createFreeApplication({ from: USER1 }); const applicationId = await korporatio.getNumApplications(); const application = await korporatio.getApplication(applicationId); @@ -225,14 +229,14 @@ contract("Korporatio", (accounts) => { expect(application.cancelledAt).to.eq.BN(UINT256_MAX); // Must have root or admin role - await checkErrorRevert(korporatio.createFreeApplication({ from: USER2 }), "korporatio-must-submit-stake"); + await checkErrorRevert(createFreeApplication({ from: USER3 }), "korporatio-must-submit-stake"); }); it("cannot create an application with insufficient rep", async () => { - await korporatio.initialise(WAD, SECONDS_PER_DAY, { from: USER0 }); + await korporatio.initialise(new BN(1), WAD, SECONDS_PER_DAY, { from: USER0 }); await checkErrorRevert( - korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0, }), "korporatio-insufficient-rep" @@ -243,17 +247,17 @@ contract("Korporatio", (accounts) => { await colony.deprecateExtension(KORPORATIO, true); await checkErrorRevert( - korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0, }), "colony-extension-deprecated" ); - await checkErrorRevert(korporatio.createFreeApplication({ from: USER1 }), "colony-extension-deprecated"); + await checkErrorRevert(createFreeApplication({ from: USER1 }), "colony-extension-deprecated"); }); it("can cancel an application", async () => { - await korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + await createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0, }); @@ -270,7 +274,7 @@ contract("Korporatio", (accounts) => { }); it("can reclaim a stake", async () => { - await korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + await createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0, }); @@ -288,29 +292,35 @@ contract("Korporatio", (accounts) => { expect(obligation).to.be.zero; }); - it("can slash a stake", async () => { - await korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + it("can delete an application without punishing", async () => { + await createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0, }); const applicationId = await korporatio.getNumApplications(); - await korporatio.slashStake(applicationId, false, { from: USER1 }); + await korporatio.deleteApplication(applicationId, false, { from: USER1 }); const obligation = await colony.getObligation(USER0, korporatio.address, 1); expect(obligation).to.be.zero; + + const lock = await tokenLocking.getUserLock(token.address, USER0); + expect(lock.balance).to.eq.BN(WAD); }); - it("can slash a stake and punish", async () => { - await korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + it("can delete an application and punish", async () => { + await createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0, }); const applicationId = await korporatio.getNumApplications(); - await korporatio.slashStake(applicationId, true, { from: USER1 }); + await korporatio.deleteApplication(applicationId, true, { from: USER1 }); const obligation = await colony.getObligation(USER0, korporatio.address, 1); expect(obligation).to.be.zero; + const lock = await tokenLocking.getUserLock(token.address, USER0); + expect(lock.balance).to.eq.BN(WAD.sub(WAD.divn(100).muln(3))); + // Staker gets a reputation penalty const addr = await colonyNetwork.getReputationMiningCycle(false); const repCycle = await IReputationMiningCycle.at(addr); @@ -323,21 +333,21 @@ contract("Korporatio", (accounts) => { }); it("cannot slash a nonexistent stake", async () => { - await checkErrorRevert(korporatio.slashStake(10, false, { from: USER1 }), "korporatio-cannot-slash"); + await checkErrorRevert(korporatio.deleteApplication(10, false, { from: USER1 }), "korporatio-cannot-slash"); }); it("cannot slash if not an arbitration user", async () => { - await korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + await createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0, }); const applicationId = await korporatio.getNumApplications(); - await checkErrorRevert(korporatio.slashStake(applicationId, false, { from: USER2 }), "korporatio-caller-not-arbitration"); + await checkErrorRevert(korporatio.deleteApplication(applicationId, false, { from: USER2 }), "korporatio-caller-not-arbitration"); }); it("can reclaim a stake via arbitration if the extension is deleted", async () => { const korporatioAddress = korporatio.address; - await korporatio.createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { + await createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings, { from: USER0, }); @@ -357,8 +367,8 @@ contract("Korporatio", (accounts) => { expect(new BN(lockPre.balance)).to.eq.BN(lockPost.balance); }); - it("can update an application", async () => { - await korporatio.createFreeApplication({ from: USER0 }); + it("can update an application as the applicant", async () => { + await createFreeApplication({ from: USER0 }); const applicationId = await korporatio.getNumApplications(); const ipfsHash = soliditySha3("IPFS Hash"); @@ -366,16 +376,26 @@ contract("Korporatio", (accounts) => { const tx = await korporatio.updateApplication(applicationId, ipfsHash, { from: USER0 }); await expectEvent(tx, "ApplicationUpdated", [applicationId, ipfsHash]); - // Cannot update if not applicant - await checkErrorRevert(korporatio.updateApplication(applicationId, ipfsHash, { from: USER1 }), "korporatio-not-applicant"); + // Cannot update if not applicant or root + await checkErrorRevert(korporatio.updateApplication(applicationId, ipfsHash, { from: USER1 }), "korporatio-not-applicant-or-root"); // Cannot update once cancelled await korporatio.cancelApplication(applicationId, { from: USER0 }); await checkErrorRevert(korporatio.updateApplication(applicationId, ipfsHash, { from: USER0 }), "korporatio-stake-cancelled"); }); + it("can update an application as a root user", async () => { + await createFreeApplication({ from: USER0 }); + + const applicationId = await korporatio.getNumApplications(); + const ipfsHash = soliditySha3("IPFS Hash"); + + const tx = await korporatio.updateApplication(applicationId, ipfsHash, { from: USER2 }); + await expectEvent(tx, "ApplicationUpdated", [applicationId, ipfsHash]); + }); + it("can submit an application", async () => { - await korporatio.createFreeApplication({ from: USER0 }); + await createFreeApplication({ from: USER0 }); const applicationId = await korporatio.getNumApplications(); @@ -398,7 +418,7 @@ contract("Korporatio", (accounts) => { await voting.initialise(WAD.divn(1000), 0, 0, WAD, SECONDS_PER_DAY, SECONDS_PER_DAY, SECONDS_PER_DAY, SECONDS_PER_DAY); - await korporatio.createFreeApplication({ from: USER0 }); + await createFreeApplication({ from: USER0 }); const applicationId = await korporatio.getNumApplications(); const action = await encodeTxData(korporatio, "submitApplication", [applicationId]); @@ -429,9 +449,7 @@ contract("Korporatio", (accounts) => { it("can submit a stake via metatransactions", async () => { await colony.approveStake(korporatio.address, 1, WAD, { from: USER0 }); - const txData = await korporatio.contract.methods - .createApplication(domain1Key, domain1Value, domain1Mask, domain1Siblings, user0Key, user0Value, user0Mask, user0Siblings) - .encodeABI(); + const txData = await korporatio.contract.methods["createApplication()"]().encodeABI(); const { r, s, v } = await getMetaTransactionParameters(txData, USER0, korporatio.address); await korporatio.executeMetaTransaction(USER0, txData, r, s, v, { from: USER0 });