Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .solcover.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 2 additions & 1 deletion .solcover.reputation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
8 changes: 2 additions & 6 deletions contracts/colony/Colony.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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");

Expand All @@ -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,
Expand Down
11 changes: 11 additions & 0 deletions contracts/colony/ColonyFunding.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
25 changes: 25 additions & 0 deletions contracts/colony/ColonyStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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");
_;
Expand Down Expand Up @@ -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;
}
Expand Down
8 changes: 8 additions & 0 deletions contracts/colony/IColony.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
19 changes: 16 additions & 3 deletions contracts/testHelpers/RequireExecuteCall.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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");
}
}
}
}
26 changes: 15 additions & 11 deletions contracts/testHelpers/TestExtensions.sol
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ pragma solidity 0.7.3;
pragma experimental ABIEncoderV2;

import "../extensions/ColonyExtension.sol";
import "./RequireExecuteCall.sol";


abstract contract TestExtension is ColonyExtension {
Expand Down Expand Up @@ -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");
}
}


7 changes: 5 additions & 2 deletions contracts/tokenLocking/TokenLocking.sol
Original file line number Diff line number Diff line change
Expand Up @@ -65,21 +65,24 @@ 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];
}

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 ++
Expand Down
2 changes: 1 addition & 1 deletion contracts/tokenLocking/TokenLockingDataTypes.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
3 changes: 3 additions & 0 deletions contracts/tokenLocking/TokenLockingStorage.sol
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
32 changes: 32 additions & 0 deletions docs/_Interface_IColony.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
11 changes: 8 additions & 3 deletions helpers/test-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
10 changes: 5 additions & 5 deletions test-smoke/colony-storage-consistent.js
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
});
Loading