diff --git a/.solcover.js b/.solcover.js index 0a4a6a5f0e..4feebd4785 100644 --- a/.solcover.js +++ b/.solcover.js @@ -19,7 +19,8 @@ module.exports = { 'testHelpers/ContractEditing.sol', // only used in setting up colony-network-recovery.js tests, never in production 'testHelpers/NoLimitSubdomains.sol', 'testHelpers/TaskSkillEditing.sol', - 'testHelpers/PreviousVersion.sol' + 'testHelpers/PreviousVersion.sol', + 'testHelpers/RequireExecuteCall.sol', ], providerOptions: { port: 8555, diff --git a/.solcover.reputation.js b/.solcover.reputation.js index 7cd9f5d9f3..5e2490382f 100644 --- a/.solcover.reputation.js +++ b/.solcover.reputation.js @@ -19,7 +19,8 @@ module.exports = { 'testHelpers/ContractEditing.sol', // only used in setting up colony-network-recovery.js tests, never in production 'testHelpers/NoLimitSubdomains.sol', 'testHelpers/TaskSkillEditing.sol', - 'testHelpers/PreviousVersion.sol' + 'testHelpers/PreviousVersion.sol', + 'testHelpers/RequireExecuteCall.sol', ], providerOptions: { port: 8555, diff --git a/contracts/colony/Colony.sol b/contracts/colony/Colony.sol index 35f527396f..9a3e8ac587 100755 --- a/contracts/colony/Colony.sol +++ b/contracts/colony/Colony.sol @@ -20,7 +20,6 @@ pragma experimental ABIEncoderV2; import "./../common/ERC20Extended.sol"; import "./../common/IEtherRouter.sol"; -import "./../extensions/ColonyExtension.sol"; import "./../tokenLocking/ITokenLocking.sol"; import "./ColonyStorage.sol"; @@ -48,12 +47,8 @@ contract Colony is ColonyStorage, PatriciaTreeProofs { public stoppable auth returns (bool) { - // Ensure _to is a contract - uint256 size; - assembly { size := extcodesize(_to) } - require(size > 0, "colony-to-must-be-contract"); - // Prevent transactions to network contracts + require(_to != address(this), "colony-cannot-target-self"); require(_to != colonyNetworkAddress, "colony-cannot-target-network"); require(_to != tokenLockingAddress, "colony-cannot-target-token-locking"); @@ -68,6 +63,7 @@ contract Colony is ColonyStorage, PatriciaTreeProofs { require(sig != BURN_GUY_SIG, "colony-cannot-call-burn-guy"); // Prevent transactions to network-managed extensions installed in this colony + require(isContract(_to), "colony-to-must-be-contract"); try ColonyExtension(_to).identifier() returns (bytes32 extensionId) { require( IColonyNetwork(colonyNetworkAddress).getExtensionInstallation(extensionId, address(this)) != _to, diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 97d3b5481a..520dd800e5 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -23,6 +23,17 @@ import "./ColonyStorage.sol"; contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 + function lockToken() public stoppable onlyExtension returns (uint256) { + uint256 lockId = ITokenLocking(tokenLockingAddress).lockToken(token); + tokenLocks[msg.sender][lockId] = true; + return lockId; + } + + function unlockTokenForUser(address _user, uint256 _lockId) public stoppable onlyExtension { + require(tokenLocks[msg.sender][_lockId], "colony-bad-lock-id"); + ITokenLocking(tokenLockingAddress).unlockTokenForUser(token, _user, _lockId); + } + function setTaskManagerPayout(uint256 _id, address _token, uint256 _amount) public stoppable self { setTaskPayout(_id, TaskRole.Manager, _token, _amount); emit TaskPayoutSet(_id, TaskRole.Manager, _token, _amount); diff --git a/contracts/colony/ColonyStorage.sol b/contracts/colony/ColonyStorage.sol index 587ab084b6..750ea6b032 100755 --- a/contracts/colony/ColonyStorage.sol +++ b/contracts/colony/ColonyStorage.sol @@ -22,6 +22,7 @@ import "./../../lib/dappsys/math.sol"; import "./../common/CommonStorage.sol"; import "./../common/ERC20Extended.sol"; import "./../colonyNetwork/IColonyNetwork.sol"; +import "./../extensions/ColonyExtension.sol"; import "./../patriciaTree/PatriciaTreeProofs.sol"; import "./ColonyAuthority.sol"; import "./ColonyDataTypes.sol"; @@ -93,6 +94,8 @@ contract ColonyStorage is CommonStorage, ColonyDataTypes, ColonyNetworkDataTypes address tokenLockingAddress; // Storage slot 30 + mapping (address => mapping (uint256 => bool)) tokenLocks; // Storage slot 31 + // Constants uint256 constant MAX_PAYOUT = 2**128 - 1; // 340,282,366,920,938,463,463 WADs bytes32 constant ROOT_ROLES = bytes32(uint256(1)) << uint8(ColonyRole.Recovery) | bytes32(uint256(1)) << uint8(ColonyRole.Root); @@ -211,6 +214,22 @@ contract ColonyStorage is CommonStorage, ColonyDataTypes, ColonyNetworkDataTypes _; } + modifier onlyExtension() { + // Ensure msg.sender is a contract + require(isContract(msg.sender), "colony-sender-must-be-contract"); + + // Ensure msg.sender is an extension + try ColonyExtension(msg.sender).identifier() returns (bytes32 extensionId) { + require( + IColonyNetwork(colonyNetworkAddress).getExtensionInstallation(extensionId, address(this)) == msg.sender, + "colony-must-be-extension" + ); + } catch { + require(false, "colony-must-be-extension"); + } + _; + } + modifier auth override { require(isAuthorized(msg.sender, 1, msg.sig), "ds-auth-unauthorized"); _; @@ -250,6 +269,12 @@ contract ColonyStorage is CommonStorage, ColonyDataTypes, ColonyNetworkDataTypes return (src == owner) || DomainRoles(address(authority)).canCall(src, domainId, address(this), sig); } + function isContract(address addr) internal returns (bool) { + uint256 size; + assembly { size := extcodesize(addr) } + return size > 0; + } + function domainExists(uint256 domainId) internal view returns (bool) { return domainId > 0 && domainId <= domainCount; } diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index 819c373c0b..2959876de6 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -227,6 +227,14 @@ interface IColony is ColonyDataTypes, IRecovery { /// @param _wad Amount to mint function mintTokensFor(address _guy, uint256 _wad) external; + /// @notice Lock the colony's token. Can only be called by a network-managed extension. + function lockToken() external returns (uint256); + + /// @notice Unlock the colony's token for a user. Can only be called by a network-managed extension. + /// @param user The user to unlock + /// @param lockId The specific lock to unlock + function unlockTokenForUser(address user, uint256 lockId) external; + /// @notice Register colony's ENS label. /// @param colonyName The label to register. /// @param orbitdb The path of the orbitDB database associated with the colony name diff --git a/contracts/testHelpers/RequireExecuteCall.sol b/contracts/testHelpers/RequireExecuteCall.sol index 11eb40815e..acfcce1e73 100644 --- a/contracts/testHelpers/RequireExecuteCall.sol +++ b/contracts/testHelpers/RequireExecuteCall.sol @@ -22,7 +22,20 @@ pragma experimental ABIEncoderV2; contract RequireExecuteCall { function executeCall(address target, bytes memory action) public { bool success; - assembly { success := call(gas(), target, 0, add(action, 0x20), mload(action), 0, 0) } - require(success, "transaction-failed"); + bytes memory returndata; + (success, returndata) = target.call(action); + if (!success){ + // Stolen shamelessly from + // https://ethereum.stackexchange.com/questions/83528/how-can-i-get-the-revert-reason-of-a-call-in-solidity-so-that-i-can-use-it-in-th + // If the _res length is less than 68, then the transaction failed silently (without a revert message) + if (returndata.length >= 68) { + assembly { + // Slice the sighash. + returndata := add(returndata, 0x04) + } + require(false, abi.decode(returndata, (string))); // All that remains is the revert string + } + require(false, "require-execute-call-reverted-with-no-error"); + } } -} +} \ No newline at end of file diff --git a/contracts/testHelpers/TestExtensions.sol b/contracts/testHelpers/TestExtensions.sol index 4fe9bf7ae0..2f137e548b 100644 --- a/contracts/testHelpers/TestExtensions.sol +++ b/contracts/testHelpers/TestExtensions.sol @@ -19,6 +19,7 @@ pragma solidity 0.7.3; pragma experimental ABIEncoderV2; import "../extensions/ColonyExtension.sol"; +import "./RequireExecuteCall.sol"; abstract contract TestExtension is ColonyExtension { @@ -65,22 +66,25 @@ contract TestExtension3 is TestExtension { function version() public pure override returns (uint256) { return 3; } } -contract TestVotingReputation is TestExtension { +contract TestVotingReputation is TestExtension, RequireExecuteCall { function identifier() public pure override returns (bytes32) { return keccak256("VotingReputation"); } function version() public pure override returns (uint256) { return 1; } - function executeCall(address target, bytes memory action) public { - bool success; - assembly { success := call(gas(), target, 0, add(action, 0x20), mload(action), 0, 0) } - require(success, "transaction-failed"); +} + +contract TestVotingToken is TestExtension { + function identifier() public pure override returns (bytes32) { return keccak256("VotingToken"); } + function version() public pure override returns (uint256) { return 1; } + function lockToken() public returns (uint256) { + return colony.lockToken(); + } + function unlockTokenForUser(address _user, uint256 _lockId) public { + colony.unlockTokenForUser(_user, _lockId); } } -contract TestVotingHybrid is TestExtension { +contract TestVotingHybrid is TestExtension, RequireExecuteCall { function identifier() public pure override returns (bytes32) { return keccak256("VotingHybrid"); } function version() public pure override returns (uint256) { return 1; } - function executeCall(address target, bytes memory action) public { - bool success; - assembly { success := call(gas(), target, 0, add(action, 0x20), mload(action), 0, 0) } - require(success, "transaction-failed"); - } } + + diff --git a/contracts/tokenLocking/TokenLocking.sol b/contracts/tokenLocking/TokenLocking.sol index 16b1cfbf42..db60d90c5e 100644 --- a/contracts/tokenLocking/TokenLocking.sol +++ b/contracts/tokenLocking/TokenLocking.sol @@ -65,8 +65,9 @@ contract TokenLocking is TokenLockingStorage, DSMath { // ignore-swc-123 function lockToken(address _token) public calledByColonyOrNetwork returns (uint256) { totalLockCount[_token] += 1; + lockers[_token][totalLockCount[_token]] = msg.sender; - emit TokenLocked(_token, totalLockCount[_token]); + emit TokenLocked(_token, msg.sender, totalLockCount[_token]); return totalLockCount[_token]; } @@ -74,12 +75,14 @@ contract TokenLocking is TokenLockingStorage, DSMath { // ignore-swc-123 function unlockTokenForUser(address _token, address _user, uint256 _lockId) public calledByColonyOrNetwork { + require(lockers[_token][_lockId] == msg.sender, "colony-token-locking-not-locker"); + // If we want to unlock tokens at id greater than total lock count, we are doing something wrong require(_lockId <= totalLockCount[_token], "colony-token-invalid-lockid"); // These checks should happen in this order, as the second is stricter than the first uint256 lockCountDelta = sub(_lockId, userLocks[_token][_user].lockCount); - require(lockCountDelta != 0, "colony-token-already-unlocked"); + require(lockCountDelta != 0, "colony-token-locking-already-unlocked"); require(lockCountDelta == 1, "colony-token-locking-has-previous-active-locks"); userLocks[_token][_user].lockCount = _lockId; // Basically just a ++ diff --git a/contracts/tokenLocking/TokenLockingDataTypes.sol b/contracts/tokenLocking/TokenLockingDataTypes.sol index d9f73abf65..c951abedc6 100644 --- a/contracts/tokenLocking/TokenLockingDataTypes.sol +++ b/contracts/tokenLocking/TokenLockingDataTypes.sol @@ -21,7 +21,7 @@ pragma solidity 0.7.3; interface TokenLockingDataTypes { event ColonyNetworkSet(address colonyNetwork); - event TokenLocked(address token, uint256 lockCount); + event TokenLocked(address indexed token, address indexed lockedBy, uint256 lockCount); event UserTokenUnlocked(address token, address user, uint256 lockId); event UserTokenDeposited(address token, address user, uint256 amount); event UserTokenClaimed(address token, address user, uint256 amount); diff --git a/contracts/tokenLocking/TokenLockingStorage.sol b/contracts/tokenLocking/TokenLockingStorage.sol index 203ea0f1b6..ad9780db07 100644 --- a/contracts/tokenLocking/TokenLockingStorage.sol +++ b/contracts/tokenLocking/TokenLockingStorage.sol @@ -43,4 +43,7 @@ contract TokenLockingStorage is TokenLockingDataTypes, DSAuth { mapping (address => mapping (address => mapping (address => uint256))) approvals; mapping (address => mapping (address => mapping (address => uint256))) obligations; mapping (address => mapping (address => uint256)) totalObligations; + + // Keep track of which colony is placing which lock ([token][lockId] => colony) + mapping (address => mapping (uint256 => address)) lockers; } diff --git a/docs/_Interface_IColony.md b/docs/_Interface_IColony.md index e0385f24b4..92884a6c72 100644 --- a/docs/_Interface_IColony.md +++ b/docs/_Interface_IColony.md @@ -1004,6 +1004,18 @@ Install an extension to the colony. Secured function to authorised members. |version|uint256|The new extension version to install +### `lockToken` + +Lock the colony's token. Can only be called by a network-managed extension. + + + +**Return Parameters** + +|Name|Type|Description| +|---|---|---| +|uint256|uint256| + ### `makeArbitraryTransaction` Execute arbitrary transaction on behalf of the Colony @@ -1689,6 +1701,26 @@ Uninstall an extension from a colony. Secured function to authorised members. |extensionId|bytes32|keccak256 hash of the extension name, used as an indentifier +### `unlockToken` + +unlock the native colony token, if possible + + + + +### `unlockTokenForUser` + +Unlock the colony's token for a user. Can only be called by a network-managed extension. + + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|user|address|The user to unlock +|lockId|uint256|The specific lock to unlock + + ### `updateColonyOrbitDB` Update a colony's orbitdb address. Can only be called by a colony with a registered subdomain diff --git a/helpers/test-helper.js b/helpers/test-helper.js index 8498d9dfd0..5c841d09a9 100644 --- a/helpers/test-helper.js +++ b/helpers/test-helper.js @@ -266,11 +266,16 @@ export async function expectEvent(tx, nameOrSig, args) { if (nameOrSig.match(re)) { // i.e. if the passed nameOrSig has () in it, we assume it's a signature const { rawLogs } = await tx.receipt; - const topic = web3.utils.soliditySha3(nameOrSig); - const types = nameOrSig.match(re)[1].split(","); + const canonicalSig = nameOrSig.replace(/ indexed/g, ""); + const topic = web3.utils.soliditySha3(canonicalSig); event = rawLogs.find((e) => e.topics[0] === topic); expect(event).to.exist; - event.args = web3.eth.abi.decodeParameters(types, event.data); + + // Set up an abi so we decode correctly, including indexed topics + const abi = [`event ${nameOrSig}`]; + const iface = new ethers.utils.Interface(abi); + + event.args = iface.parseLog(event).args; } else { const { logs } = await tx; event = logs.find((e) => e.event === nameOrSig); diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index ddd725699b..7b4a39d1af 100644 --- a/test-smoke/colony-storage-consistent.js +++ b/test-smoke/colony-storage-consistent.js @@ -153,11 +153,11 @@ contract("Contract Storage", (accounts) => { console.log("miningCycleStateHash:", miningCycleAccount.stateRoot.toString("hex")); console.log("tokenLockingStateHash:", tokenLockingAccount.stateRoot.toString("hex")); - expect(colonyNetworkAccount.stateRoot.toString("hex")).to.equal("97e1c6d4d66e2d25f9383c8b1b70bb7680f09746871a589c70453b8348bd12f8"); - expect(colonyAccount.stateRoot.toString("hex")).to.equal("13eb14d2eecef97fc149f34d4395a2740ff73e648ba628c96bca47cc7a1fe5fc"); - expect(metaColonyAccount.stateRoot.toString("hex")).to.equal("732c7f5e08625dd988817c08cab50f790e936be6631d6977ae4759c8eace4501"); - expect(miningCycleAccount.stateRoot.toString("hex")).to.equal("0d53ccdb3a8572d8dbce13d4c44c764d936dd33c1f16602ccb4cf5b749255935"); - expect(tokenLockingAccount.stateRoot.toString("hex")).to.equal("2ad6ae85a6fca70dd65e94acca366d699b5fb558b4017ea790f1c8371c9e0aca"); + expect(colonyNetworkAccount.stateRoot.toString("hex")).to.equal("0389ad89b0b6e33f0c61344a3e0a9046c9e77e0783279852f4f5dd796835e2cc"); + expect(colonyAccount.stateRoot.toString("hex")).to.equal("a855bdceb16d16c2b84557d8836b2af9ed80f2ee61b026d33444c9b73b45343f"); + expect(metaColonyAccount.stateRoot.toString("hex")).to.equal("87cb26976b719dc15b579dbbde4d517dd6109e57453e6aff855b2293842091cf"); + expect(miningCycleAccount.stateRoot.toString("hex")).to.equal("b5ed349fbd30f4c326e9b781cfa4f74341615f3ec7d5d0e326b2d30ef64dbcbc"); + expect(tokenLockingAccount.stateRoot.toString("hex")).to.equal("8f63b2041527c2c8bacea1ee895300c92d3f918f992ef1e2756745300b4c873f"); }); }); }); diff --git a/test/contracts-network/colony-network-extensions.js b/test/contracts-network/colony-network-extensions.js index 5add18055f..7e158ba669 100644 --- a/test/contracts-network/colony-network-extensions.js +++ b/test/contracts-network/colony-network-extensions.js @@ -6,7 +6,7 @@ import { BN } from "bn.js"; import { ethers } from "ethers"; import { soliditySha3 } from "web3-utils"; -import { checkErrorRevert, web3GetBalance } from "../../helpers/test-helper"; +import { checkErrorRevert, web3GetBalance, encodeTxData } from "../../helpers/test-helper"; import { setupEtherRouter } from "../../helpers/upgradable-contracts"; import { setupColonyNetwork, setupMetaColonyWithLockedCLNYToken, setupRandomColony } from "../../helpers/test-data-generator"; import { UINT256_MAX } from "../../helpers/constants"; @@ -15,59 +15,63 @@ const { expect } = chai; chai.use(bnChai(web3.utils.BN)); const ColonyExtension = artifacts.require("ColonyExtension"); +const EtherRouter = artifacts.require("EtherRouter"); +const IMetaColony = artifacts.require("IMetaColony"); +const ITokenLocking = artifacts.require("ITokenLocking"); const TestExtension0 = artifacts.require("TestExtension0"); const TestExtension1 = artifacts.require("TestExtension1"); const TestExtension2 = artifacts.require("TestExtension2"); const TestExtension3 = artifacts.require("TestExtension3"); +const TestVotingToken = artifacts.require("TestVotingToken"); const Resolver = artifacts.require("Resolver"); -const IMetaColony = artifacts.require("IMetaColony"); +const RequireExecuteCall = artifacts.require("RequireExecuteCall"); contract("Colony Network Extensions", (accounts) => { let colonyNetwork; let metaColony; let colony; + let token; - let resolver0; - let resolver1; - let resolver2; - let resolver3; + let testExtension0Resolver; + let testExtension1Resolver; + let testExtension2Resolver; + let testExtension3Resolver; + let testVotingTokenResolver; const ROOT = accounts[0]; const ARCHITECT = accounts[1]; const USER = accounts[2]; const TEST_EXTENSION = soliditySha3("TestExtension"); - - async function setupResolver(versionId) { - const resolver = await Resolver.new(); - if (versionId === 0) { - const testExtension0 = await TestExtension0.new(); - await setupEtherRouter("TestExtension0", { TestExtension0: testExtension0.address }, resolver); - } else if (versionId === 1) { - const testExtension1 = await TestExtension1.new(); - await setupEtherRouter("TestExtension1", { TestExtension1: testExtension1.address }, resolver); - } else if (versionId === 2) { - const testExtension2 = await TestExtension2.new(); - await setupEtherRouter("TestExtension2", { TestExtension2: testExtension2.address }, resolver); - } else if (versionId === 3) { - const testExtension3 = await TestExtension3.new(); - await setupEtherRouter("TestExtension3", { TestExtension3: testExtension3.address }, resolver); - } - return resolver; - } + const TEST_VOTING_TOKEN = soliditySha3("VotingToken"); before(async () => { - resolver0 = await setupResolver(0); - resolver1 = await setupResolver(1); - resolver2 = await setupResolver(2); - resolver3 = await setupResolver(3); + testExtension0Resolver = await Resolver.new(); + const testExtension0 = await TestExtension0.new(); + await setupEtherRouter("TestExtension0", { TestExtension0: testExtension0.address }, testExtension0Resolver); + + testExtension1Resolver = await Resolver.new(); + const testExtension1 = await TestExtension1.new(); + await setupEtherRouter("TestExtension1", { TestExtension1: testExtension1.address }, testExtension1Resolver); + + testExtension2Resolver = await Resolver.new(); + const testExtension2 = await TestExtension2.new(); + await setupEtherRouter("TestExtension2", { TestExtension2: testExtension2.address }, testExtension2Resolver); + + testExtension3Resolver = await Resolver.new(); + const testExtension3 = await TestExtension3.new(); + await setupEtherRouter("TestExtension3", { TestExtension3: testExtension3.address }, testExtension3Resolver); + + testVotingTokenResolver = await Resolver.new(); + const testVotingToken = await TestVotingToken.new(); + await setupEtherRouter("TestVotingToken", { TestVotingToken: testVotingToken.address }, testVotingTokenResolver); }); beforeEach(async () => { colonyNetwork = await setupColonyNetwork(); ({ metaColony } = await setupMetaColonyWithLockedCLNYToken(colonyNetwork)); - ({ colony } = await setupRandomColony(colonyNetwork)); + ({ colony, token } = await setupRandomColony(colonyNetwork)); await colony.addDomain(1, UINT256_MAX, 1); // Domain 2 await colony.setArchitectureRole(1, UINT256_MAX, ARCHITECT, 1, true); @@ -80,6 +84,10 @@ contract("Colony Network Extensions", (accounts) => { // Can install directly await extension.install(colony.address); + // Can get the colony + const colonyAddress = await extension.getColony(); + expect(colonyAddress).to.equal(colony.address); + // Can only install once await checkErrorRevert(extension.install(colony.address), "extension-already-installed"); @@ -98,30 +106,39 @@ contract("Colony Network Extensions", (accounts) => { describe("adding extensions", () => { it("allows the meta colony to add new extensions", async () => { // Versions start at 1 - await checkErrorRevert(metaColony.addExtensionToNetwork(TEST_EXTENSION, resolver0.address), "colony-network-extension-bad-version"); + await checkErrorRevert( + metaColony.addExtensionToNetwork(TEST_EXTENSION, testExtension0Resolver.address), + "colony-network-extension-bad-version" + ); - await metaColony.addExtensionToNetwork(TEST_EXTENSION, resolver1.address); - await metaColony.addExtensionToNetwork(TEST_EXTENSION, resolver2.address); + await metaColony.addExtensionToNetwork(TEST_EXTENSION, testExtension1Resolver.address); + await metaColony.addExtensionToNetwork(TEST_EXTENSION, testExtension2Resolver.address); const resolverAddress = await colonyNetwork.getExtensionResolver(TEST_EXTENSION, 1); - expect(resolverAddress).to.equal(resolver1.address); + expect(resolverAddress).to.equal(testExtension1Resolver.address); }); it("does not allow the meta colony to set a non-matching identifier", async () => { - await checkErrorRevert(metaColony.addExtensionToNetwork("0x0", resolver1.address), "colony-network-extension-bad-identifier"); + await checkErrorRevert(metaColony.addExtensionToNetwork("0x0", testExtension1Resolver.address), "colony-network-extension-bad-identifier"); }); it("does not allow the meta colony to overwrite existing extensions", async () => { - await metaColony.addExtensionToNetwork(TEST_EXTENSION, resolver1.address); + await metaColony.addExtensionToNetwork(TEST_EXTENSION, testExtension1Resolver.address); - await checkErrorRevert(metaColony.addExtensionToNetwork(TEST_EXTENSION, resolver1.address), "colony-network-extension-already-set"); + await checkErrorRevert( + metaColony.addExtensionToNetwork(TEST_EXTENSION, testExtension1Resolver.address), + "colony-network-extension-already-set" + ); }); it("does not allow the meta colony to add versions out of order", async () => { - await checkErrorRevert(metaColony.addExtensionToNetwork(TEST_EXTENSION, resolver2.address), "colony-network-extension-bad-version"); + await checkErrorRevert( + metaColony.addExtensionToNetwork(TEST_EXTENSION, testExtension2Resolver.address), + "colony-network-extension-bad-version" + ); - await metaColony.addExtensionToNetwork(TEST_EXTENSION, resolver1.address); - await metaColony.addExtensionToNetwork(TEST_EXTENSION, resolver2.address); + await metaColony.addExtensionToNetwork(TEST_EXTENSION, testExtension1Resolver.address); + await metaColony.addExtensionToNetwork(TEST_EXTENSION, testExtension2Resolver.address); }); it("does not allow the meta colony to add a null resolver", async () => { @@ -131,14 +148,17 @@ contract("Colony Network Extensions", (accounts) => { it("does not allow other colonies to add new extensions", async () => { const fakeMetaColony = await IMetaColony.at(colony.address); - await checkErrorRevert(fakeMetaColony.addExtensionToNetwork(TEST_EXTENSION, resolver1.address), "colony-caller-must-be-meta-colony"); + await checkErrorRevert( + fakeMetaColony.addExtensionToNetwork(TEST_EXTENSION, testExtension1Resolver.address), + "colony-caller-must-be-meta-colony" + ); }); }); describe("installing extensions", () => { beforeEach(async () => { - await metaColony.addExtensionToNetwork(TEST_EXTENSION, resolver1.address); - await metaColony.addExtensionToNetwork(TEST_EXTENSION, resolver2.address); + await metaColony.addExtensionToNetwork(TEST_EXTENSION, testExtension1Resolver.address); + await metaColony.addExtensionToNetwork(TEST_EXTENSION, testExtension2Resolver.address); }); it("allows a root user to install an extension with any version", async () => { @@ -171,9 +191,9 @@ contract("Colony Network Extensions", (accounts) => { describe("upgrading extensions", () => { beforeEach(async () => { - await metaColony.addExtensionToNetwork(TEST_EXTENSION, resolver1.address); - await metaColony.addExtensionToNetwork(TEST_EXTENSION, resolver2.address); - await metaColony.addExtensionToNetwork(TEST_EXTENSION, resolver3.address); + await metaColony.addExtensionToNetwork(TEST_EXTENSION, testExtension1Resolver.address); + await metaColony.addExtensionToNetwork(TEST_EXTENSION, testExtension2Resolver.address); + await metaColony.addExtensionToNetwork(TEST_EXTENSION, testExtension3Resolver.address); }); it("allows root users to upgrade an extension", async () => { @@ -220,7 +240,7 @@ contract("Colony Network Extensions", (accounts) => { describe("deprecating extensions", () => { beforeEach(async () => { - await metaColony.addExtensionToNetwork(TEST_EXTENSION, resolver1.address); + await metaColony.addExtensionToNetwork(TEST_EXTENSION, testExtension1Resolver.address); }); it("allows root users to deprecate and undeprecate an extension", async () => { @@ -249,7 +269,7 @@ contract("Colony Network Extensions", (accounts) => { describe("uninstalling extensions", () => { beforeEach(async () => { - await metaColony.addExtensionToNetwork(TEST_EXTENSION, resolver1.address); + await metaColony.addExtensionToNetwork(TEST_EXTENSION, testExtension1Resolver.address); }); it("allows root users to uninstall an extension and send ether to the beneficiary", async () => { @@ -278,4 +298,76 @@ contract("Colony Network Extensions", (accounts) => { await checkErrorRevert(colony.uninstallExtension(TEST_EXTENSION, { from: ROOT }), "colony-network-extension-not-installed"); }); }); + + describe("using extensions", () => { + beforeEach(async () => { + await metaColony.addExtensionToNetwork(TEST_VOTING_TOKEN, testVotingTokenResolver.address); + }); + + it("allows network-managed extensions to lock and unlock tokens", async () => { + const tokenLockingAddress = await colonyNetwork.getTokenLocking(); + const tokenLocking = await ITokenLocking.at(tokenLockingAddress); + + await colony.installExtension(TEST_VOTING_TOKEN, 1, { from: ROOT }); + const testVotingTokenAddress = await colonyNetwork.getExtensionInstallation(TEST_VOTING_TOKEN, colony.address); + const testVotingToken = await TestVotingToken.at(testVotingTokenAddress); + + const lockCountPre = await tokenLocking.getTotalLockCount(token.address); + + await testVotingToken.lockToken(); + + const lockCountPost = await tokenLocking.getTotalLockCount(token.address); + expect(lockCountPost.sub(lockCountPre)).to.eq.BN(1); + + // Check that you can't unlock a lock you haven't set + await checkErrorRevert(testVotingToken.unlockTokenForUser(ROOT, lockCountPost.addn(1)), "colony-bad-lock-id"); + + // Check that you can't unlock too far ahead + await testVotingToken.lockToken(); + await checkErrorRevert(testVotingToken.unlockTokenForUser(ROOT, lockCountPost.addn(1)), "colony-token-locking-has-previous-active-locks"); + + await testVotingToken.unlockTokenForUser(ROOT, lockCountPost); + + const userLock = await tokenLocking.getUserLock(token.address, ROOT); + expect(userLock.lockCount).to.eq.BN(lockCountPost); + + // Check that you can't unlock twice + await checkErrorRevert(testVotingToken.unlockTokenForUser(ROOT, lockCountPost), "colony-token-locking-already-unlocked"); + }); + + it("does not allow non network-managed extensions to lock and unlock tokens", async () => { + const testVotingToken = await TestVotingToken.new(); + await testVotingToken.install(colony.address); + await checkErrorRevert(testVotingToken.lockToken(), "colony-must-be-extension"); + await checkErrorRevert(testVotingToken.unlockTokenForUser(ROOT, 0), "colony-must-be-extension"); + }); + + it("does not allow users to lock and unlock tokens", async () => { + await checkErrorRevert(colony.lockToken(), "colony-sender-must-be-contract"); + await checkErrorRevert(colony.unlockTokenForUser(ROOT, 0), "colony-sender-must-be-contract"); + }); + + it("does not allow a colony to unlock a lock placed by another colony", async () => { + const tokenLockingAddress = await colonyNetwork.getTokenLocking(); + const tokenLocking = await ITokenLocking.at(tokenLockingAddress); + + await colony.installExtension(TEST_VOTING_TOKEN, 1, { from: ROOT }); + const testVotingTokenAddress = await colonyNetwork.getExtensionInstallation(TEST_VOTING_TOKEN, colony.address); + const testVotingToken = await TestVotingToken.at(testVotingTokenAddress); + + await testVotingToken.lockToken(); + const lockId = await tokenLocking.getTotalLockCount(token.address); + + const { colony: otherColony } = await setupRandomColony(colonyNetwork); + const otherColonyAsER = await EtherRouter.at(otherColony.address); + const resolverAddress = await otherColonyAsER.resolver(); + const resolver = await Resolver.at(resolverAddress); + const requireExecuteCall = await RequireExecuteCall.new(); + await resolver.register("executeCall(address,bytes)", requireExecuteCall.address); + const otherColonyExecuteCall = await RequireExecuteCall.at(otherColony.address); + + const action = await encodeTxData(tokenLocking, "unlockTokenForUser", [token.address, USER, lockId]); + await checkErrorRevert(otherColonyExecuteCall.executeCall(tokenLocking.address, action), "colony-token-locking-not-locker"); + }); + }); }); diff --git a/test/contracts-network/colony-reward-payouts.js b/test/contracts-network/colony-reward-payouts.js index 55aaf2ad93..79f0cbbbd7 100644 --- a/test/contracts-network/colony-reward-payouts.js +++ b/test/contracts-network/colony-reward-payouts.js @@ -514,7 +514,7 @@ contract("Colony Reward Payouts", (accounts) => { await checkErrorRevert( colony.claimRewardPayout(payoutId, initialSquareRoots, ...userReputationProof1, { from: userAddress1 }), - "colony-token-already-unlocked" + "colony-token-locking-already-unlocked" ); }); @@ -610,7 +610,7 @@ contract("Colony Reward Payouts", (accounts) => { await checkErrorRevert( colony.claimRewardPayout(payoutId, initialSquareRoots, ...userReputationProof1, { from: userAddress1 }), - "colony-token-already-unlocked" + "colony-token-locking-already-unlocked" ); }); @@ -626,7 +626,7 @@ contract("Colony Reward Payouts", (accounts) => { await checkErrorRevert( colony.claimRewardPayout(payoutId, initialSquareRoots, ...userReputationProof1, { from: userAddress1 }), - "colony-token-already-unlocked" + "colony-token-locking-already-unlocked" ); }); diff --git a/test/contracts-network/colony.js b/test/contracts-network/colony.js index 7b9d68c92a..4f3b325018 100755 --- a/test/contracts-network/colony.js +++ b/test/contracts-network/colony.js @@ -150,6 +150,10 @@ contract("Colony", (accounts) => { await checkErrorRevert(colony.makeArbitraryTransaction(token.address, action, { from: USER1 }), "ds-auth-unauthorized"); }); + it("should not be able to make arbitrary transactions to a colony itself", async () => { + await checkErrorRevert(colony.makeArbitraryTransaction(colony.address, "0x0"), "colony-cannot-target-self"); + }); + it("should not be able to make arbitrary transactions to a user address", async () => { await checkErrorRevert(colony.makeArbitraryTransaction(accounts[0], "0x0"), "colony-to-must-be-contract"); }); diff --git a/test/contracts-network/token-locking.js b/test/contracts-network/token-locking.js index bdb5677b32..441cf00015 100644 --- a/test/contracts-network/token-locking.js +++ b/test/contracts-network/token-locking.js @@ -5,7 +5,7 @@ import bnChai from "bn-chai"; import { ethers } from "ethers"; import TruffleLoader from "../../packages/reputation-miner/TruffleLoader"; -import { getTokenArgs, checkErrorRevert, makeReputationKey, advanceMiningCycleNoContest } from "../../helpers/test-helper"; +import { getTokenArgs, checkErrorRevert, makeReputationKey, advanceMiningCycleNoContest, expectEvent } from "../../helpers/test-helper"; import { giveUserCLNYTokensAndStake, setupRandomColony, fundColonyWithTokens } from "../../helpers/test-data-generator"; import { UINT256_MAX, DEFAULT_STAKE } from "../../helpers/constants"; @@ -324,6 +324,15 @@ contract("Token Locking", (addresses) => { }); describe("locking behavior", async () => { + it("should correctly emit the TokenLocked event", async () => { + await token.approve(tokenLocking.address, usersTokens, { from: userAddress }); + await tokenLocking.deposit(token.address, usersTokens, { from: userAddress }); + await fundColonyWithTokens(colony, otherToken); + await colony.moveFundsBetweenPots(1, UINT256_MAX, UINT256_MAX, 1, 0, 100, otherToken.address); + const tx = await colony.startNextRewardPayout(otherToken.address, ...colonyWideReputationProof); + await expectEvent(tx, "TokenLocked(address indexed,address indexed,uint256)", [token.address, colony.address, 1]); + }); + it("should correctly increment total lock count", async () => { await token.approve(tokenLocking.address, usersTokens, { from: userAddress }); await tokenLocking.deposit(token.address, usersTokens, { from: userAddress }); diff --git a/test/extensions/token-supplier.js b/test/extensions/token-supplier.js index 3d5a1814ea..7c2f9468b4 100644 --- a/test/extensions/token-supplier.js +++ b/test/extensions/token-supplier.js @@ -125,19 +125,19 @@ contract("Token Supplier", (accounts) => { // Cannot set if not a network-managed extension const unofficialVotingHybrid = await VotingHybrid.new(colony.address); await colony.setRootRole(unofficialVotingHybrid.address, true); - await checkErrorRevert(unofficialVotingHybrid.executeCall(tokenSupplier.address, action), "transaction-failed"); + await checkErrorRevert(unofficialVotingHybrid.executeCall(tokenSupplier.address, action), "token-supplier-not-managed-extension"); // Cannot set if the caller does not implement `identifier()` const requireExecuteCall = await RequireExecuteCall.new(); await colony.setRootRole(requireExecuteCall.address, true); - await checkErrorRevert(requireExecuteCall.executeCall(tokenSupplier.address, action), "transaction-failed"); + await checkErrorRevert(requireExecuteCall.executeCall(tokenSupplier.address, action), "token-supplier-no-identifier"); // Cannot set if not VotingHybrid await colony.installExtension(VOTING_REPUTATION, 1); const votingReputationAddress = await colonyNetwork.getExtensionInstallation(VOTING_REPUTATION, colony.address); const votingReputation = await VotingHybrid.at(votingReputationAddress); await colony.setRootRole(votingReputation.address, true); - await checkErrorRevert(votingReputation.executeCall(tokenSupplier.address, action), "transaction-failed"); + await checkErrorRevert(votingReputation.executeCall(tokenSupplier.address, action), "token-supplier-cannot-set-value"); }); it("can update the tokenIssuanceRate via a hybrid vote", async () => { @@ -160,12 +160,12 @@ contract("Token Supplier", (accounts) => { // Cannot set if not a network-managed extension const unofficialVotingHybrid = await VotingHybrid.new(colony.address); await colony.setRootRole(unofficialVotingHybrid.address, true); - await checkErrorRevert(unofficialVotingHybrid.executeCall(tokenSupplier.address, action), "transaction-failed"); + await checkErrorRevert(unofficialVotingHybrid.executeCall(tokenSupplier.address, action), "token-supplier-not-managed-extension"); // Cannot set if the caller does not implement `identifier()` const requireExecuteCall = await RequireExecuteCall.new(); await colony.setRootRole(requireExecuteCall.address, true); - await checkErrorRevert(requireExecuteCall.executeCall(tokenSupplier.address, action), "transaction-failed"); + await checkErrorRevert(requireExecuteCall.executeCall(tokenSupplier.address, action), "token-supplier-no-identifier"); }); it("can update the tokenIssuanceRate via a reputation vote, by <=10% once every 4 weeks", async () => { @@ -182,12 +182,12 @@ contract("Token Supplier", (accounts) => { const action2 = await encodeTxData(tokenSupplier, "setTokenIssuanceRate", [WAD.add(WAD.divn(9))]); // Cannot change more than once in 4 weeks - await checkErrorRevert(votingReputation.executeCall(tokenSupplier.address, action1), "transaction-failed"); + await checkErrorRevert(votingReputation.executeCall(tokenSupplier.address, action1), "token-supplier-cannot-set-value"); await forwardTime(SECONDS_PER_DAY * 28, this); // Cannot change more than 10% - await checkErrorRevert(votingReputation.executeCall(tokenSupplier.address, action2), "transaction-failed"); + await checkErrorRevert(votingReputation.executeCall(tokenSupplier.address, action2), "token-supplier-cannot-set-value"); await votingReputation.executeCall(tokenSupplier.address, action1); @@ -195,7 +195,7 @@ contract("Token Supplier", (accounts) => { expect(tokenIssuanceRate).to.eq.BN(WAD.add(WAD.divn(10))); // Cannot change more than once in 4 weeks - await checkErrorRevert(votingReputation.executeCall(tokenSupplier.address, action2), "transaction-failed"); + await checkErrorRevert(votingReputation.executeCall(tokenSupplier.address, action2), "token-supplier-cannot-set-value"); await forwardTime(SECONDS_PER_DAY * 28, this);