diff --git a/contracts/colony/Colony.sol b/contracts/colony/Colony.sol index 4b512dc8b5..e1c0fbfafb 100755 --- a/contracts/colony/Colony.sol +++ b/contracts/colony/Colony.sol @@ -20,11 +20,11 @@ pragma experimental ABIEncoderV2; import "./../common/ERC20Extended.sol"; import "./../common/IEtherRouter.sol"; -import "./../common/MultiChain.sol"; +import "./../common/BasicMetaTransaction.sol"; import "./../tokenLocking/ITokenLocking.sol"; import "./ColonyStorage.sol"; -contract Colony is ColonyStorage, PatriciaTreeProofs, MultiChain { +contract Colony is BasicMetaTransaction, ColonyStorage, PatriciaTreeProofs { // V8: Ebony Lightweight Spaceship // This function, exactly as defined, is used in build scripts. Take care when updating. @@ -39,144 +39,8 @@ contract Colony is ColonyStorage, PatriciaTreeProofs, MultiChain { return token; } - bytes4 constant APPROVE_SIG = bytes4(keccak256("approve(address,uint256)")); - bytes4 constant TRANSFER_SIG = bytes4(keccak256("transfer(address,uint256)")); - bytes4 constant TRANSFER_FROM_SIG = bytes4(keccak256("transferFrom(address,address,uint256)")); - bytes4 constant BURN_SIG = bytes4(keccak256("burn(uint256)")); - bytes4 constant BURN_GUY_SIG = bytes4(keccak256("burn(address,uint256)")); - - function makeArbitraryTransaction(address _to, bytes memory _action) - public stoppable auth - returns (bool) - { - return this.makeSingleArbitraryTransaction(_to, _action); - } - - function makeArbitraryTransactions(address[] memory _targets, bytes[] memory _actions, bool _strict) - public stoppable auth - returns (bool) - { - require(_targets.length == _actions.length, "colony-targets-and-actions-length-mismatch"); - for (uint256 i; i < _targets.length; i += 1){ - bool success = true; - // slither-disable-next-line unused-return - try this.makeSingleArbitraryTransaction(_targets[i], _actions[i]) returns (bool ret){ - if (_strict){ - success = ret; - } - } catch { - // We failed in a require, which is only okay if we're not in strict mode - if (_strict){ - success = false; - } - } - require(success, "colony-arbitrary-transaction-failed"); - } - return true; - } - - function makeSingleArbitraryTransaction(address _to, bytes memory _action) - external stoppable self - returns (bool) - { - // 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"); - - // Prevent transactions to transfer held tokens - bytes4 sig; - assembly { sig := mload(add(_action, 0x20)) } - - if (sig == APPROVE_SIG) { approveTransactionPreparation(_to, _action); } - else if (sig == BURN_SIG) { burnTransactionPreparation(_to, _action); } - else if (sig == TRANSFER_SIG) { transferTransactionPreparation(_to, _action); } - else if (sig == BURN_GUY_SIG || sig == TRANSFER_FROM_SIG) { burnGuyOrTransferFromTransactionPreparation(_action); } - - // Prevent transactions to network-managed extensions installed in this colony - require(isContract(_to), "colony-to-must-be-contract"); - // slither-disable-next-line unused-return - try ColonyExtension(_to).identifier() returns (bytes32 extensionId) { - require( - IColonyNetwork(colonyNetworkAddress).getExtensionInstallation(extensionId, address(this)) != _to, - "colony-cannot-target-extensions" - ); - } catch {} - - bool res = executeCall(_to, 0, _action); - - if (sig == APPROVE_SIG) { approveTransactionCleanup(_to, _action); } - - return res; - } - - function approveTransactionPreparation(address _to, bytes memory _action) internal { - address spender; - assembly { - spender := mload(add(_action, 0x24)) - } - updateApprovalAmountInternal(_to, spender, false); - } - - function approveTransactionCleanup(address _to, bytes memory _action) internal { - address spender; - assembly { - spender := mload(add(_action, 0x24)) - } - updateApprovalAmountInternal(_to, spender, true); - } - - function burnTransactionPreparation(address _to, bytes memory _action) internal { - uint256 amount; - assembly { - amount := mload(add(_action, 0x24)) - } - fundingPots[1].balance[_to] = sub(fundingPots[1].balance[_to], amount); - require(fundingPots[1].balance[_to] >= tokenApprovalTotals[_to], "colony-not-enough-tokens"); - } - - function transferTransactionPreparation(address _to, bytes memory _action) internal { - uint256 amount; - assembly { - amount := mload(add(_action, 0x44)) - } - fundingPots[1].balance[_to] = sub(fundingPots[1].balance[_to], amount); - require(fundingPots[1].balance[_to] >= tokenApprovalTotals[_to], "colony-not-enough-tokens"); - } - - function burnGuyOrTransferFromTransactionPreparation(bytes memory _action) internal { - address spender; - assembly { - spender := mload(add(_action, 0x24)) - } - require(spender != address(this), "colony-cannot-spend-own-allowance"); - } - - function updateApprovalAmount(address _token, address _spender) stoppable public { - updateApprovalAmountInternal(_token, _spender, false); - } - - function updateApprovalAmountInternal(address _token, address _spender, bool _postApproval) internal { - uint256 recordedApproval = tokenApprovals[_token][_spender]; - uint256 actualApproval = ERC20Extended(_token).allowance(address(this), _spender); - if (recordedApproval == actualApproval) { - return; - } - - if (recordedApproval > actualApproval && !_postApproval){ - // They've spend some tokens out of root. Adjust balances accordingly - // If we are post approval, then they have not spent tokens - fundingPots[1].balance[_token] = add(sub(fundingPots[1].balance[_token], recordedApproval), actualApproval); - } - - tokenApprovalTotals[_token] = add(sub(tokenApprovalTotals[_token], recordedApproval), actualApproval); - require(fundingPots[1].balance[_token] >= tokenApprovalTotals[_token], "colony-approval-exceeds-balance"); - - tokenApprovals[_token][_spender] = actualApproval; - } - function annotateTransaction(bytes32 _txHash, string memory _metadata) public always { - emit Annotation(msg.sender, _txHash, _metadata); + emit Annotation(msgSender(), _txHash, _metadata); } function emitDomainReputationReward(uint256 _domainId, address _user, int256 _amount) @@ -256,19 +120,13 @@ contract Colony is ColonyStorage, PatriciaTreeProofs, MultiChain { // Set initial colony reward inverse amount to the max indicating a zero rewards to start with rewardInverse = 2**256 - 1; - emit ColonyInitialised(msg.sender, _colonyNetworkAddress, _token); - } - - function initialiseColony(address _colonyNetworkAddress, address _token, string memory _metadata) public stoppable { - initialiseColony(_colonyNetworkAddress, _token); - - emit ColonyMetadata(msg.sender, _metadata); + emit ColonyInitialised(msgSender(), _colonyNetworkAddress, _token); } function editColony(string memory _metadata) public stoppable auth { - emit ColonyMetadata(msg.sender, _metadata); + emit ColonyMetadata(msgSender(), _metadata); } function bootstrapColony(address[] memory _users, int[] memory _amounts) public @@ -288,7 +146,17 @@ contract Colony is ColonyStorage, PatriciaTreeProofs, MultiChain { IColonyNetwork(colonyNetworkAddress).appendReputationUpdateLog(_users[i], _amounts[i], domains[1].skillId); } - emit ColonyBootstrapped(msg.sender, _users, _amounts); + emit ColonyBootstrapped(msgSender(), _users, _amounts); + } + + function burnTokens(address _token, uint256 _amount) public stoppable auth { + // Check the root funding pot has enought + require(fundingPots[1].balance[_token] >= _amount, "colony-not-enough-tokens"); + fundingPots[1].balance[_token] -= _amount; + + ERC20Extended(_token).burn(_amount); + + emit TokensBurned(msgSender(), _token, _amount); } function mintTokens(uint _wad) public @@ -297,7 +165,7 @@ contract Colony is ColonyStorage, PatriciaTreeProofs, MultiChain { { ERC20Extended(token).mint(address(this), _wad); // ignore-swc-107 - emit TokensMinted(msg.sender, address(this), _wad); + emit TokensMinted(msgSender(), address(this), _wad); } function mintTokensFor(address _guy, uint _wad) public @@ -306,12 +174,12 @@ contract Colony is ColonyStorage, PatriciaTreeProofs, MultiChain { { ERC20Extended(token).mint(_guy, _wad); // ignore-swc-107 - emit TokensMinted(msg.sender, _guy, _wad); + emit TokensMinted(msgSender(), _guy, _wad); } function mintTokensForColonyNetwork(uint _wad) public stoppable { // Only the colony Network can call this function - require(msg.sender == colonyNetworkAddress, "colony-access-denied-only-network-allowed"); + require(msgSender() == colonyNetworkAddress, "colony-access-denied-only-network-allowed"); // Function only valid on the Meta Colony require(address(this) == IColonyNetwork(colonyNetworkAddress).getMetaColony(), "colony-access-denied-only-meta-colony-allowed"); // Not callable on Xdai @@ -320,7 +188,7 @@ contract Colony is ColonyStorage, PatriciaTreeProofs, MultiChain { ERC20Extended(token).mint(_wad); assert(ERC20Extended(token).transfer(colonyNetworkAddress, _wad)); - emit TokensMinted(msg.sender, colonyNetworkAddress, _wad); + emit TokensMinted(msgSender(), colonyNetworkAddress, _wad); } function registerColonyLabel(string memory colonyName, string memory orbitdb) public stoppable auth { @@ -429,7 +297,7 @@ contract Colony is ColonyStorage, PatriciaTreeProofs, MultiChain { initialiseDomain(newLocalSkill); if (keccak256(abi.encodePacked(_metadata)) != keccak256(abi.encodePacked(""))) { - emit DomainMetadata(msg.sender, domainCount, _metadata); + emit DomainMetadata(msgSender(), domainCount, _metadata); } } @@ -438,7 +306,7 @@ contract Colony is ColonyStorage, PatriciaTreeProofs, MultiChain { authDomain(_permissionDomainId, _childSkillIndex, _domainId) { if (keccak256(abi.encodePacked(_metadata)) != keccak256(abi.encodePacked(""))) { - emit DomainMetadata(msg.sender, _domainId, _metadata); + emit DomainMetadata(msgSender(), _domainId, _metadata); } } @@ -467,7 +335,7 @@ contract Colony is ColonyStorage, PatriciaTreeProofs, MultiChain { userAddress >>= 96; // Require that the user is proving their own reputation in this colony. - if (address(colonyAddress) != address(this) || address(userAddress) != msg.sender) { + if (address(colonyAddress) != address(this) || address(userAddress) != msgSender()) { return false; } @@ -495,7 +363,7 @@ contract Colony is ColonyStorage, PatriciaTreeProofs, MultiChain { // we need to do once we know what's in it! this.finishUpgrade(); - emit ColonyUpgraded(msg.sender, currentVersion, _newVersion); + emit ColonyUpgraded(msgSender(), currentVersion, _newVersion); } // v7 to v8 @@ -513,25 +381,39 @@ contract Colony is ColonyStorage, PatriciaTreeProofs, MultiChain { colonyAuthority.setRoleCapability(uint8(ColonyRole.Arbitration), address(this), sig, true); } + function getMetatransactionNonce(address _user) override public view returns (uint256 nonce){ + return metatransactionNonces[_user]; + } + + function incrementMetatransactionNonce(address _user) override internal { + // We need to protect the metatransaction nonce slots, otherwise those with recovery + // permissions could replay metatransactions, which would be a disaster. + // What slot are we setting? + // This mapping is in slot 34 (see ColonyStorage.sol); + uint256 slot = uint256(keccak256(abi.encode(uint256(_user), uint256(METATRANSACTION_NONCES_SLOT)))); + protectSlot(slot); + metatransactionNonces[_user] = add(metatransactionNonces[_user], 1); + } + function checkNotAdditionalProtectedVariable(uint256 _slot) public view recovery { require(_slot != COLONY_NETWORK_SLOT, "colony-protected-variable"); } function approveStake(address _approvee, uint256 _domainId, uint256 _amount) public stoppable { - approvals[msg.sender][_approvee][_domainId] = add(approvals[msg.sender][_approvee][_domainId], _amount); + approvals[msgSender()][_approvee][_domainId] = add(approvals[msgSender()][_approvee][_domainId], _amount); - ITokenLocking(tokenLockingAddress).approveStake(msg.sender, _amount, token); + ITokenLocking(tokenLockingAddress).approveStake(msgSender(), _amount, token); } function obligateStake(address _user, uint256 _domainId, uint256 _amount) public stoppable { - approvals[_user][msg.sender][_domainId] = sub(approvals[_user][msg.sender][_domainId], _amount); - obligations[_user][msg.sender][_domainId] = add(obligations[_user][msg.sender][_domainId], _amount); + approvals[_user][msgSender()][_domainId] = sub(approvals[_user][msgSender()][_domainId], _amount); + obligations[_user][msgSender()][_domainId] = add(obligations[_user][msgSender()][_domainId], _amount); ITokenLocking(tokenLockingAddress).obligateStake(_user, _amount, token); } function deobligateStake(address _user, uint256 _domainId, uint256 _amount) public stoppable { - obligations[_user][msg.sender][_domainId] = sub(obligations[_user][msg.sender][_domainId], _amount); + obligations[_user][msgSender()][_domainId] = sub(obligations[_user][msgSender()][_domainId], _amount); ITokenLocking(tokenLockingAddress).deobligateStake(_user, _amount, token); } @@ -586,7 +468,7 @@ contract Colony is ColonyStorage, PatriciaTreeProofs, MultiChain { fundingPotId: fundingPotCount }); - emit DomainAdded(msg.sender, domainCount); + emit DomainAdded(msgSender(), domainCount); emit FundingPotAdded(fundingPotCount); } diff --git a/contracts/colony/ColonyArbitraryTransaction.sol b/contracts/colony/ColonyArbitraryTransaction.sol new file mode 100644 index 0000000000..a77db43895 --- /dev/null +++ b/contracts/colony/ColonyArbitraryTransaction.sol @@ -0,0 +1,165 @@ +/* + This file is part of The Colony Network. + + The Colony Network is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + The Colony Network is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with The Colony Network. If not, see . +*/ + +pragma solidity 0.7.3; +pragma experimental ABIEncoderV2; + +import "./../common/ERC20Extended.sol"; +import "./../common/IEtherRouter.sol"; +import "./../common/MultiChain.sol"; +import "./../tokenLocking/ITokenLocking.sol"; +import "./ColonyStorage.sol"; + +contract ColonyArbitraryTransaction is ColonyStorage { + + bytes4 constant APPROVE_SIG = bytes4(keccak256("approve(address,uint256)")); + bytes4 constant TRANSFER_SIG = bytes4(keccak256("transfer(address,uint256)")); + bytes4 constant TRANSFER_FROM_SIG = bytes4(keccak256("transferFrom(address,address,uint256)")); + bytes4 constant BURN_SIG = bytes4(keccak256("burn(uint256)")); + bytes4 constant BURN_GUY_SIG = bytes4(keccak256("burn(address,uint256)")); + + function makeArbitraryTransaction(address _to, bytes memory _action) + public stoppable auth + returns (bool) + { + return this.makeSingleArbitraryTransaction(_to, _action); + } + + function makeArbitraryTransactions(address[] memory _targets, bytes[] memory _actions, bool _strict) + public stoppable auth + returns (bool) + { + require(_targets.length == _actions.length, "colony-targets-and-actions-length-mismatch"); + for (uint256 i; i < _targets.length; i += 1){ + bool success = true; + // slither-disable-next-line unused-return + try this.makeSingleArbitraryTransaction(_targets[i], _actions[i]) returns (bool ret){ + if (_strict){ + success = ret; + } + } catch { + // We failed in a require, which is only okay if we're not in strict mode + if (_strict){ + success = false; + } + } + require(success, "colony-arbitrary-transaction-failed"); + } + return true; + } + + function makeSingleArbitraryTransaction(address _to, bytes memory _action) + external stoppable self + returns (bool) + { + // 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"); + + // Prevent transactions to transfer held tokens + bytes4 sig; + assembly { sig := mload(add(_action, 0x20)) } + + if (sig == APPROVE_SIG) { approveTransactionPreparation(_to, _action); } + else if (sig == BURN_SIG) { burnTransactionPreparation(_to, _action); } + else if (sig == TRANSFER_SIG) { transferTransactionPreparation(_to, _action); } + else if (sig == BURN_GUY_SIG || sig == TRANSFER_FROM_SIG) { burnGuyOrTransferFromTransactionPreparation(_action); } + + // Prevent transactions to network-managed extensions installed in this colony + require(isContract(_to), "colony-to-must-be-contract"); + // slither-disable-next-line unused-return + try ColonyExtension(_to).identifier() returns (bytes32 extensionId) { + require( + IColonyNetwork(colonyNetworkAddress).getExtensionInstallation(extensionId, address(this)) != _to, + "colony-cannot-target-extensions" + ); + } catch {} + + bool res = executeCall(_to, 0, _action); + + if (sig == APPROVE_SIG) { approveTransactionCleanup(_to, _action); } + + return res; + } + + function approveTransactionPreparation(address _to, bytes memory _action) internal { + address spender; + assembly { + spender := mload(add(_action, 0x24)) + } + updateApprovalAmountInternal(_to, spender, false); + } + + function approveTransactionCleanup(address _to, bytes memory _action) internal { + address spender; + assembly { + spender := mload(add(_action, 0x24)) + } + updateApprovalAmountInternal(_to, spender, true); + } + + function burnTransactionPreparation(address _to, bytes memory _action) internal { + uint256 amount; + assembly { + amount := mload(add(_action, 0x24)) + } + fundingPots[1].balance[_to] = sub(fundingPots[1].balance[_to], amount); + require(fundingPots[1].balance[_to] >= tokenApprovalTotals[_to], "colony-not-enough-tokens"); + } + + function transferTransactionPreparation(address _to, bytes memory _action) internal { + uint256 amount; + assembly { + amount := mload(add(_action, 0x44)) + } + fundingPots[1].balance[_to] = sub(fundingPots[1].balance[_to], amount); + require(fundingPots[1].balance[_to] >= tokenApprovalTotals[_to], "colony-not-enough-tokens"); + } + + function burnGuyOrTransferFromTransactionPreparation(bytes memory _action) internal { + address spender; + assembly { + spender := mload(add(_action, 0x24)) + } + require(spender != address(this), "colony-cannot-spend-own-allowance"); + } + + function updateApprovalAmount(address _token, address _spender) stoppable public { + updateApprovalAmountInternal(_token, _spender, false); + } + + function updateApprovalAmountInternal(address _token, address _spender, bool _postApproval) internal { + uint256 recordedApproval = tokenApprovals[_token][_spender]; + uint256 actualApproval = ERC20Extended(_token).allowance(address(this), _spender); + if (recordedApproval == actualApproval) { + return; + } + + if (recordedApproval > actualApproval && !_postApproval){ + // They've spend some tokens out of root. Adjust balances accordingly + // If we are post approval, then they have not spent tokens + fundingPots[1].balance[_token] = add(sub(fundingPots[1].balance[_token], recordedApproval), actualApproval); + } + + tokenApprovalTotals[_token] = add(sub(tokenApprovalTotals[_token], recordedApproval), actualApproval); + require(fundingPots[1].balance[_token] >= tokenApprovalTotals[_token], "colony-approval-exceeds-balance"); + + tokenApprovals[_token][_spender] = actualApproval; + } + +} \ No newline at end of file diff --git a/contracts/colony/ColonyExpenditure.sol b/contracts/colony/ColonyExpenditure.sol index 590aa4be98..3bbba7f024 100644 --- a/contracts/colony/ColonyExpenditure.sol +++ b/contracts/colony/ColonyExpenditure.sol @@ -34,7 +34,7 @@ contract ColonyExpenditure is ColonyStorage { { defaultGlobalClaimDelay = _defaultGlobalClaimDelay; - emit ExpenditureGlobalClaimDelaySet(msg.sender, _defaultGlobalClaimDelay); + emit ExpenditureGlobalClaimDelaySet(msgSender(), _defaultGlobalClaimDelay); } function makeExpenditure(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _domainId) @@ -51,7 +51,7 @@ contract ColonyExpenditure is ColonyStorage { expenditures[expenditureCount] = Expenditure({ status: ExpenditureStatus.Draft, - owner: msg.sender, + owner: msgSender(), fundingPotId: fundingPotCount, domainId: _domainId, finalizedTimestamp: 0, @@ -59,7 +59,7 @@ contract ColonyExpenditure is ColonyStorage { }); emit FundingPotAdded(fundingPotCount); - emit ExpenditureAdded(msg.sender, expenditureCount); + emit ExpenditureAdded(msgSender(), expenditureCount); return expenditureCount; } @@ -73,7 +73,7 @@ contract ColonyExpenditure is ColonyStorage { { expenditures[_id].owner = _newOwner; - emit ExpenditureTransferred(msg.sender, _id, _newOwner); + emit ExpenditureTransferred(msgSender(), _id, _newOwner); } // Deprecated @@ -91,7 +91,7 @@ contract ColonyExpenditure is ColonyStorage { { expenditures[_id].owner = _newOwner; - emit ExpenditureTransferred(msg.sender, _id, _newOwner); + emit ExpenditureTransferred(msgSender(), _id, _newOwner); } function cancelExpenditure(uint256 _id) @@ -103,7 +103,7 @@ contract ColonyExpenditure is ColonyStorage { { expenditures[_id].status = ExpenditureStatus.Cancelled; - emit ExpenditureCancelled(msg.sender, _id); + emit ExpenditureCancelled(msgSender(), _id); } function lockExpenditure(uint256 _id) @@ -115,7 +115,7 @@ contract ColonyExpenditure is ColonyStorage { { expenditures[_id].status = ExpenditureStatus.Locked; - emit ExpenditureLocked(msg.sender, _id); + emit ExpenditureLocked(msgSender(), _id); } function finalizeExpenditure(uint256 _id) @@ -131,7 +131,7 @@ contract ColonyExpenditure is ColonyStorage { expenditures[_id].status = ExpenditureStatus.Finalized; expenditures[_id].finalizedTimestamp = block.timestamp; - emit ExpenditureFinalized(msg.sender, _id); + emit ExpenditureFinalized(msgSender(), _id); } function setExpenditureMetadata(uint256 _id, string memory _metadata) @@ -141,7 +141,7 @@ contract ColonyExpenditure is ColonyStorage { expenditureDraft(_id) expenditureOnlyOwner(_id) { - emit ExpenditureMetadataSet(msg.sender, _id, _metadata); + emit ExpenditureMetadataSet(msgSender(), _id, _metadata); } function setExpenditureMetadata( @@ -155,7 +155,7 @@ contract ColonyExpenditure is ColonyStorage { expenditureExists(_id) authDomain(_permissionDomainId, _childSkillIndex, expenditures[_id].domainId) { - emit ExpenditureMetadataSet(msg.sender, _id, _metadata); + emit ExpenditureMetadataSet(msgSender(), _id, _metadata); } function setExpenditureRecipients(uint256 _id, uint256[] memory _slots, address payable[] memory _recipients) @@ -170,7 +170,7 @@ contract ColonyExpenditure is ColonyStorage { for (uint256 i; i < _slots.length; i++) { expenditureSlots[_id][_slots[i]].recipient = _recipients[i]; - emit ExpenditureRecipientSet(msg.sender, _id, _slots[i], _recipients[i]); + emit ExpenditureRecipientSet(msgSender(), _id, _slots[i], _recipients[i]); } } @@ -200,7 +200,7 @@ contract ColonyExpenditure is ColonyStorage { expenditureSlots[_id][_slots[i]].skills = new uint256[](1); expenditureSlots[_id][_slots[i]].skills[0] = _skillIds[i]; - emit ExpenditureSkillSet(msg.sender, _id, _slots[i], _skillIds[i]); + emit ExpenditureSkillSet(msgSender(), _id, _slots[i], _skillIds[i]); } } @@ -216,7 +216,7 @@ contract ColonyExpenditure is ColonyStorage { for (uint256 i; i < _slots.length; i++) { expenditureSlots[_id][_slots[i]].claimDelay = _claimDelays[i]; - emit ExpenditureClaimDelaySet(msg.sender, _id, _slots[i], _claimDelays[i]); + emit ExpenditureClaimDelaySet(msgSender(), _id, _slots[i], _claimDelays[i]); } } @@ -232,7 +232,7 @@ contract ColonyExpenditure is ColonyStorage { for (uint256 i; i < _slots.length; i++) { expenditureSlots[_id][_slots[i]].payoutModifier = _payoutModifiers[i]; - emit ExpenditurePayoutModifierSet(msg.sender, _id, _slots[i], _payoutModifiers[i]); + emit ExpenditurePayoutModifierSet(msgSender(), _id, _slots[i], _payoutModifiers[i]); } } diff --git a/contracts/colony/ColonyFunding.sol b/contracts/colony/ColonyFunding.sol index 5f4cc1b4b0..d16f0f774f 100755 --- a/contracts/colony/ColonyFunding.sol +++ b/contracts/colony/ColonyFunding.sol @@ -25,12 +25,12 @@ import "./ColonyStorage.sol"; contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 function lockToken() public stoppable onlyOwnExtension returns (uint256) { uint256 lockId = ITokenLocking(tokenLockingAddress).lockToken(token); - tokenLocks[msg.sender][lockId] = true; + tokenLocks[msgSender()][lockId] = true; return lockId; } function unlockTokenForUser(address _user, uint256 _lockId) public stoppable onlyOwnExtension { - require(tokenLocks[msg.sender][_lockId], "colony-bad-lock-id"); + require(tokenLocks[msgSender()][_lockId], "colony-bad-lock-id"); ITokenLocking(tokenLockingAddress).unlockTokenForUser(token, _user, _lockId); } @@ -183,7 +183,7 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 updatePayoutsWeCannotMakeAfterBudgetChange(payment.fundingPotId, _token, currentTotalAmount); - emit PaymentPayoutSet(msg.sender, _id, _token, _amount); + emit PaymentPayoutSet(msgSender(), _id, _token, _amount); } function getFundingPotCount() public view returns (uint256 count) { @@ -267,7 +267,7 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 fundingPots[1].balance[_token] = add(fundingPots[1].balance[_token], remainder); fundingPots[0].balance[_token] = add(fundingPots[0].balance[_token], feeToPay); - emit ColonyFundsClaimed(msg.sender, _token, feeToPay, remainder); + emit ColonyFundsClaimed(msgSender(), _token, feeToPay, remainder); } function getNonRewardPotsTotal(address _token) public view returns (uint256) { @@ -309,7 +309,7 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 false ); - emit RewardPayoutCycleStarted(msg.sender, totalLockCount); + emit RewardPayoutCycleStarted(msgSender(), totalLockCount); } // slither-disable-next-line reentrancy-no-eth @@ -325,7 +325,7 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 uint256 userReputation = checkReputation( rewardPayoutCycles[_payoutId].reputationState, domains[1].skillId, - msg.sender, + msgSender(), key, value, branchMask, @@ -336,7 +336,7 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 uint256 reward; (tokenAddress, reward) = calculateRewardForUser(_payoutId, _squareRoots, userReputation); - ITokenLocking(tokenLockingAddress).unlockTokenForUser(token, msg.sender, _payoutId); + ITokenLocking(tokenLockingAddress).unlockTokenForUser(token, msgSender(), _payoutId); uint fee = calculateNetworkFeeForPayout(reward); uint remainder = sub(reward, fee); @@ -348,10 +348,10 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 ); rewardPayoutCycles[_payoutId].amountRemaining = sub(rewardPayoutCycles[_payoutId].amountRemaining, reward); - assert(ERC20Extended(tokenAddress).transfer(msg.sender, remainder)); + assert(ERC20Extended(tokenAddress).transfer(msgSender(), remainder)); assert(ERC20Extended(tokenAddress).transfer(colonyNetworkAddress, fee)); - emit RewardPayoutClaimed(_payoutId, msg.sender, fee, remainder); + emit RewardPayoutClaimed(_payoutId, msgSender(), fee, remainder); } function finalizeRewardPayout(uint256 _payoutId) public stoppable { @@ -363,7 +363,7 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 rewardPayoutCycles[_payoutId].finalized = true; pendingRewardPayments[payout.tokenAddress] = sub(pendingRewardPayments[payout.tokenAddress], payout.amountRemaining); - emit RewardPayoutCycleEnded(msg.sender, _payoutId); + emit RewardPayoutCycleEnded(msgSender(), _payoutId); } function getRewardPayoutInfo(uint256 _payoutId) public view returns (RewardPayoutCycle memory rewardPayoutCycle) { @@ -377,7 +377,7 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 require(_rewardInverse > 0, "colony-reward-inverse-cannot-be-zero"); rewardInverse = _rewardInverse; - emit ColonyRewardInverseSet(msg.sender, _rewardInverse); + emit ColonyRewardInverseSet(msgSender(), _rewardInverse); } function getRewardInverse() public view returns (uint256) { @@ -422,7 +422,7 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 // Checking if payout is active require(block.timestamp - payout.blockTimestamp <= 60 days, "colony-reward-payout-not-active"); - uint256 userTokens = ITokenLocking(tokenLockingAddress).getUserLock(token, msg.sender).balance; + uint256 userTokens = ITokenLocking(tokenLockingAddress).getUserLock(token, msgSender()).balance; require(userTokens > 0, "colony-reward-payout-invalid-user-tokens"); require(userReputation > 0, "colony-reward-payout-invalid-user-reputation"); @@ -509,7 +509,7 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 nonRewardPotsTotal[_token] = sub(nonRewardPotsTotal[_token], _amount); } - emit ColonyFundsMovedBetweenFundingPots(msg.sender, _fromPot, _toPot, _amount, _token); + emit ColonyFundsMovedBetweenFundingPots(msgSender(), _fromPot, _toPot, _amount, _token); } @@ -584,7 +584,7 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 fundingPot.payouts[_token] = add(sub(currentTotal, currentPayout), _amounts[i]); - emit ExpenditurePayoutSet(msg.sender, _id, _slots[i], _token, _amounts[i]); + emit ExpenditurePayoutSet(msgSender(), _id, _slots[i], _token, _amounts[i]); } updatePayoutsWeCannotMakeAfterBudgetChange(expenditures[_id].fundingPotId, _token, currentTotal); @@ -648,7 +648,7 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 } } - emit PayoutClaimed(msg.sender, _fundingPotId, _token, remainder); + emit PayoutClaimed(msgSender(), _fundingPotId, _token, remainder); } function calculateNetworkFeeForPayout(uint256 _payout) private view returns (uint256 fee) { @@ -663,14 +663,4 @@ contract ColonyFunding is ColonyStorage, PatriciaTreeProofs { // ignore-swc-123 } } - function burnTokens(address _token, uint256 _amount) public stoppable auth { - // Check the root funding pot has enought - require(fundingPots[1].balance[_token] >= _amount, "colony-not-enough-tokens"); - fundingPots[1].balance[_token] -= _amount; - - ERC20Extended(_token).burn(_amount); - - emit TokensBurned(msg.sender, _token, _amount); - } - } diff --git a/contracts/colony/ColonyPayment.sol b/contracts/colony/ColonyPayment.sol index 0de466106f..f3dae35e20 100644 --- a/contracts/colony/ColonyPayment.sol +++ b/contracts/colony/ColonyPayment.sol @@ -56,16 +56,16 @@ contract ColonyPayment is ColonyStorage { payments[paymentCount] = payment; emit FundingPotAdded(fundingPotCount); - emit PaymentAdded(msg.sender, paymentCount); + emit PaymentAdded(msgSender(), paymentCount); if (_skillId > 0) { setPaymentSkill(_permissionDomainId, _childSkillIndex, paymentCount, _skillId); - emit PaymentSkillSet(msg.sender, paymentCount, _skillId); + emit PaymentSkillSet(msgSender(), paymentCount, _skillId); } - emit PaymentRecipientSet(msg.sender, paymentCount, _recipient); - emit PaymentPayoutSet(msg.sender, paymentCount, _token, _amount); + emit PaymentRecipientSet(msgSender(), paymentCount, _recipient); + emit PaymentPayoutSet(msgSender(), paymentCount, _token, _amount); return paymentCount; } @@ -92,7 +92,7 @@ contract ColonyPayment is ColonyStorage { } } - emit PaymentFinalized(msg.sender, _id); + emit PaymentFinalized(msgSender(), _id); } function setPaymentRecipient(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _id, address payable _recipient) public @@ -103,7 +103,7 @@ contract ColonyPayment is ColonyStorage { require(_recipient != address(0x0), "colony-payment-invalid-recipient"); payments[_id].recipient = _recipient; - emit PaymentRecipientSet(msg.sender, _id, _recipient); + emit PaymentRecipientSet(msgSender(), _id, _recipient); } function setPaymentSkill(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _id, uint256 _skillId) public @@ -115,7 +115,7 @@ contract ColonyPayment is ColonyStorage { { payments[_id].skills[0] = _skillId; - emit PaymentSkillSet(msg.sender, _id, _skillId); + emit PaymentSkillSet(msgSender(), _id, _skillId); } function getPayment(uint256 _id) public view returns (Payment memory) { diff --git a/contracts/colony/ColonyRoles.sol b/contracts/colony/ColonyRoles.sol index d38462bf73..bf18b3ab44 100644 --- a/contracts/colony/ColonyRoles.sol +++ b/contracts/colony/ColonyRoles.sol @@ -27,7 +27,7 @@ contract ColonyRoles is ColonyStorage, ContractRecoveryDataTypes { function setRootRole(address _user, bool _setTo) public stoppable auth { ColonyAuthority(address(authority)).setUserRole(_user, uint8(ColonyRole.Root), _setTo); - emit ColonyRoleSet(msg.sender, _user, 1, uint8(ColonyRole.Root), _setTo); + emit ColonyRoleSet(msgSender(), _user, 1, uint8(ColonyRole.Root), _setTo); } function setArbitrationRole( @@ -40,7 +40,7 @@ contract ColonyRoles is ColonyStorage, ContractRecoveryDataTypes { { ColonyAuthority(address(authority)).setUserRole(_user, _domainId, uint8(ColonyRole.Arbitration), _setTo); - emit ColonyRoleSet(msg.sender, _user, _domainId, uint8(ColonyRole.Arbitration), _setTo); + emit ColonyRoleSet(msgSender(), _user, _domainId, uint8(ColonyRole.Arbitration), _setTo); } function setArchitectureRole( @@ -53,7 +53,7 @@ contract ColonyRoles is ColonyStorage, ContractRecoveryDataTypes { { ColonyAuthority(address(authority)).setUserRole(_user, _domainId, uint8(ColonyRole.Architecture), _setTo); - emit ColonyRoleSet(msg.sender, _user, _domainId, uint8(ColonyRole.Architecture), _setTo); + emit ColonyRoleSet(msgSender(), _user, _domainId, uint8(ColonyRole.Architecture), _setTo); } function setFundingRole( @@ -66,7 +66,7 @@ contract ColonyRoles is ColonyStorage, ContractRecoveryDataTypes { { ColonyAuthority(address(authority)).setUserRole(_user, _domainId, uint8(ColonyRole.Funding), _setTo); - emit ColonyRoleSet(msg.sender, _user, _domainId, uint8(ColonyRole.Funding), _setTo); + emit ColonyRoleSet(msgSender(), _user, _domainId, uint8(ColonyRole.Funding), _setTo); } function setAdministrationRole( @@ -79,7 +79,7 @@ contract ColonyRoles is ColonyStorage, ContractRecoveryDataTypes { { ColonyAuthority(address(authority)).setUserRole(_user, _domainId, uint8(ColonyRole.Administration), _setTo); - emit ColonyRoleSet(msg.sender, _user, _domainId, uint8(ColonyRole.Administration), _setTo); + emit ColonyRoleSet(msgSender(), _user, _domainId, uint8(ColonyRole.Administration), _setTo); } function setUserRoles( @@ -112,7 +112,7 @@ contract ColonyRoles is ColonyStorage, ContractRecoveryDataTypes { } emit RecoveryRoleSet(_user, setTo); } - emit ColonyRoleSet(msg.sender, _user, _domainId, roleId, setTo); + emit ColonyRoleSet(msgSender(), _user, _domainId, roleId, setTo); } roles >>= 1; diff --git a/contracts/colony/ColonyStorage.sol b/contracts/colony/ColonyStorage.sol index 68189ed074..e38726c5e0 100755 --- a/contracts/colony/ColonyStorage.sol +++ b/contracts/colony/ColonyStorage.sol @@ -31,7 +31,7 @@ import "./ColonyDataTypes.sol"; // ignore-file-swc-108 -contract ColonyStorage is CommonStorage, ColonyDataTypes, ColonyNetworkDataTypes, DSMath { +contract ColonyStorage is ColonyDataTypes, ColonyNetworkDataTypes, DSMath, CommonStorage { uint256 constant COLONY_NETWORK_SLOT = 6; // Storage @@ -104,6 +104,9 @@ contract ColonyStorage is CommonStorage, ColonyDataTypes, ColonyNetworkDataTypes uint256 defaultGlobalClaimDelay; // Storage slot 34 + uint256 constant METATRANSACTION_NONCES_SLOT = 35; + mapping(address => uint256) metatransactionNonces; // Storage slot 35 + // Constants uint256 constant MAX_PAYOUT = 2**128 - 1; // 340,282,366,920,938,463,463 WADs @@ -135,7 +138,7 @@ contract ColonyStorage is CommonStorage, ColonyDataTypes, ColonyNetworkDataTypes modifier confirmTaskRoleIdentity(uint256 _id, TaskRole _role) { Role storage role = tasks[_id].roles[uint8(_role)]; - require(msg.sender == role.user, "colony-task-role-identity-mismatch"); + require(msgSender() == role.user, "colony-task-role-identity-mismatch"); _; } @@ -184,7 +187,7 @@ contract ColonyStorage is CommonStorage, ColonyDataTypes, ColonyNetworkDataTypes } modifier expenditureOnlyOwner(uint256 _id) { - require(expenditures[_id].owner == msg.sender, "colony-expenditure-not-owner"); + require(expenditures[_id].owner == msgSender(), "colony-expenditure-not-owner"); _; } @@ -234,30 +237,31 @@ contract ColonyStorage is CommonStorage, ColonyDataTypes, ColonyNetworkDataTypes } modifier self() { - require(address(this) == msg.sender, "colony-not-self"); + require(address(this) == msgSender(), "colony-not-self"); _; } modifier onlyOwnExtension() { - require(isOwnExtension(msg.sender), "colony-must-be-own-extension"); + require(isOwnExtension(msgSender()), "colony-must-be-own-extension"); + assert(msgSender() == msg.sender); _; } modifier auth override { - require(isAuthorized(msg.sender, 1, msg.sig), "ds-auth-unauthorized"); + require(isAuthorized(msgSender(), 1, msg.sig), "ds-auth-unauthorized"); _; } modifier authDomain(uint256 _permissionDomainId, uint256 _childSkillIndex, uint256 _childDomainId) { require(domainExists(_permissionDomainId), "ds-auth-permission-domain-does-not-exist"); require(domainExists(_childDomainId), "ds-auth-child-domain-does-not-exist"); - require(isAuthorized(msg.sender, _permissionDomainId, msg.sig), "ds-auth-unauthorized"); + require(isAuthorized(msgSender(), _permissionDomainId, msg.sig), "ds-auth-unauthorized"); require(validateDomainInheritance(_permissionDomainId, _childSkillIndex, _childDomainId), "ds-auth-invalid-domain-inheritence"); _; } modifier archSubdomain(uint256 _permissionDomainId, uint256 _childDomainId) { - if (canCallOnlyBecauseArchitect(msg.sender, _permissionDomainId, msg.sig)) { + if (canCallOnlyBecauseArchitect(msgSender(), _permissionDomainId, msg.sig)) { require(_permissionDomainId != _childDomainId, "ds-auth-only-authorized-in-child-domain"); } _; diff --git a/contracts/colony/ColonyTask.sol b/contracts/colony/ColonyTask.sol index 015b4b37f1..e861a0454b 100755 --- a/contracts/colony/ColonyTask.sol +++ b/contracts/colony/ColonyTask.sol @@ -29,9 +29,9 @@ contract ColonyTask is ColonyStorage { // Manager rated by worker // Worker rated by evaluator if (_role == TaskRole.Manager) { - require(tasks[_id].roles[uint8(TaskRole.Worker)].user == msg.sender, "colony-user-cannot-rate-task-manager"); + require(tasks[_id].roles[uint8(TaskRole.Worker)].user == msgSender(), "colony-user-cannot-rate-task-manager"); } else if (_role == TaskRole.Worker) { - require(tasks[_id].roles[uint8(TaskRole.Evaluator)].user == msg.sender, "colony-user-cannot-rate-task-worker"); + require(tasks[_id].roles[uint8(TaskRole.Evaluator)].user == msgSender(), "colony-user-cannot-rate-task-worker"); } else { revert("colony-unsupported-role-to-rate"); } @@ -111,8 +111,8 @@ contract ColonyTask is ColonyStorage { tasks[taskCount].fundingPotId = fundingPotCount; tasks[taskCount].domainId = _domainId; tasks[taskCount].skills = new uint256[](1); - tasks[taskCount].roles[uint8(TaskRole.Manager)].user = msg.sender; - tasks[taskCount].roles[uint8(TaskRole.Evaluator)].user = msg.sender; + tasks[taskCount].roles[uint8(TaskRole.Manager)].user = msgSender(); + tasks[taskCount].roles[uint8(TaskRole.Evaluator)].user = msgSender(); if (_skillId > 0) { this.setTaskSkill(taskCount, _skillId); @@ -127,7 +127,7 @@ contract ColonyTask is ColonyStorage { this.setTaskDueDate(taskCount, dueDate); emit FundingPotAdded(fundingPotCount); - emit TaskAdded(msg.sender, taskCount); + emit TaskAdded(msgSender(), taskCount); } function getTaskCount() public view returns (uint256) { @@ -289,7 +289,7 @@ contract ColonyTask is ColonyStorage { require(rating != TaskRatings.None, "colony-task-rating-missing"); tasks[_id].roles[uint8(_role)].rating = rating; - emit TaskWorkRatingRevealed(msg.sender, _id, _role, _rating); + emit TaskWorkRatingRevealed(msgSender(), _id, _role, _rating); } function generateSecret(bytes32 _salt, uint256 _value) public pure returns (bytes32) { @@ -380,7 +380,7 @@ contract ColonyTask is ColonyStorage { { tasks[_id].deliverableHash = _deliverableHash; markTaskCompleted(_id); - emit TaskDeliverableSubmitted(msg.sender, _id, _deliverableHash); + emit TaskDeliverableSubmitted(msgSender(), _id, _deliverableHash); } function submitTaskDeliverableAndRating(uint256 _id, bytes32 _deliverableHash, bytes32 _ratingSecret) public @@ -424,7 +424,7 @@ contract ColonyTask is ColonyStorage { } } - emit TaskFinalized(msg.sender, _id); + emit TaskFinalized(msgSender(), _id); } function cancelTask(uint256 _id) public @@ -467,7 +467,7 @@ contract ColonyTask is ColonyStorage { function markTaskCompleted(uint256 _id) internal { tasks[_id].completionTimestamp = block.timestamp; - emit TaskCompleted(msg.sender, _id); + emit TaskCompleted(msgSender(), _id); } function updateReputation(TaskRole taskRole, Task storage task) internal { diff --git a/contracts/colony/IColony.sol b/contracts/colony/IColony.sol index c56760608c..0a5d43af99 100644 --- a/contracts/colony/IColony.sol +++ b/contracts/colony/IColony.sol @@ -19,12 +19,13 @@ pragma solidity >=0.7.3; // ignore-swc-103 pragma experimental ABIEncoderV2; import "./../common/IRecovery.sol"; +import "./../common/IBasicMetaTransaction.sol"; import "./ColonyDataTypes.sol"; /// @title Colony interface /// @notice All externally available functions are available here and registered to work with EtherRouter Network contract -interface IColony is ColonyDataTypes, IRecovery { +interface IColony is ColonyDataTypes, IRecovery, IBasicMetaTransaction { // Implemented in DSAuth.sol /// @notice Get the `ColonyAuthority` for the colony. /// @return colonyAuthority The `ColonyAuthority` contract address diff --git a/contracts/colonyNetwork/ColonyNetwork.sol b/contracts/colonyNetwork/ColonyNetwork.sol index c8ea75bb08..ce15c6624e 100644 --- a/contracts/colonyNetwork/ColonyNetwork.sol +++ b/contracts/colonyNetwork/ColonyNetwork.sol @@ -20,7 +20,7 @@ pragma experimental "ABIEncoderV2"; import "./../common/EtherRouter.sol"; import "./../common/ERC20Extended.sol"; -import "./../common/MultiChain.sol"; +import "./../common/BasicMetaTransaction.sol"; import "./../colony/ColonyAuthority.sol"; import "./../colony/IColony.sol"; import "./../colony/IMetaColony.sol"; @@ -28,14 +28,14 @@ import "./../reputationMiningCycle/IReputationMiningCycle.sol"; import "./ColonyNetworkStorage.sol"; -contract ColonyNetwork is ColonyNetworkStorage, MultiChain { +contract ColonyNetwork is BasicMetaTransaction, ColonyNetworkStorage { // Meta Colony allowed to manage Global skills // All colonies are able to manage their Local (domain associated) skills modifier allowedToAddSkill(bool globalSkill) { if (globalSkill) { - require(msg.sender == metaColony, "colony-must-be-meta-colony"); + require(msgSender() == metaColony, "colony-must-be-meta-colony"); } else { - require(_isColony[msg.sender] || msg.sender == address(this), "colony-caller-must-be-colony"); + require(_isColony[msgSender()] || msgSender() == address(this), "colony-caller-must-be-colony"); } _; } @@ -107,21 +107,6 @@ contract ColonyNetwork is ColonyNetworkStorage, MultiChain { return tokenLocking; } - function setMiningResolver(address _miningResolver) public - stoppable - auth - { - require(_miningResolver != address(0x0), "colony-mining-resolver-cannot-be-zero"); - - miningCycleResolver = _miningResolver; - - emit MiningCycleResolverSet(_miningResolver); - } - - function getMiningResolver() public view returns (address) { - return miningCycleResolver; - } - function createMetaColony(address _tokenAddress) public stoppable auth @@ -315,7 +300,7 @@ contract ColonyNetwork is ColonyNetworkStorage, MultiChain { _user, _amount, _skillId, - msg.sender, + msgSender(), nParents, nChildren ); @@ -349,6 +334,20 @@ contract ColonyNetwork is ColonyNetworkStorage, MultiChain { emit TokenWhitelisted(_token, _status); } + function getMetatransactionNonce(address _user) override public view returns (uint256 nonce){ + return metatransactionNonces[_user]; + } + + function incrementMetatransactionNonce(address _user) override internal { + // We need to protect the metatransaction nonce slots, otherwise those with recovery + // permissions could replay metatransactions, which would be a disaster. + // What slot are we setting? + // This mapping is in slot 41 (see ColonyNetworkStorage.sol); + uint256 slot = uint256(keccak256(abi.encode(uint256(_user), uint256(METATRANSACTION_NONCES_SLOT)))); + protectSlot(slot); + metatransactionNonces[_user] = add(metatransactionNonces[_user], 1); + } + function deployColony(address _tokenAddress, uint256 _version) internal returns (address) { require(_tokenAddress != address(0x0), "colony-token-invalid-address"); require(colonyVersionResolver[_version] != address(0x00), "colony-network-invalid-version"); @@ -385,12 +384,12 @@ contract ColonyNetwork is ColonyNetworkStorage, MultiChain { // Assign all permissions in root domain IColony colony = IColony(_colonyAddress); - colony.setRecoveryRole(msg.sender); - colony.setRootRole(msg.sender, true); - colony.setArbitrationRole(1, UINT256_MAX, msg.sender, 1, true); - colony.setArchitectureRole(1, UINT256_MAX, msg.sender, 1, true); - colony.setFundingRole(1, UINT256_MAX, msg.sender, 1, true); - colony.setAdministrationRole(1, UINT256_MAX, msg.sender, 1, true); + colony.setRecoveryRole(msgSender()); + colony.setRootRole(msgSender(), true); + colony.setArbitrationRole(1, UINT256_MAX, msgSender(), 1, true); + colony.setArchitectureRole(1, UINT256_MAX, msgSender(), 1, true); + colony.setFundingRole(1, UINT256_MAX, msgSender(), 1, true); + colony.setAdministrationRole(1, UINT256_MAX, msgSender(), 1, true); // Colony will not have owner DSAuth dsauth = DSAuth(_colonyAddress); diff --git a/contracts/colonyNetwork/ColonyNetworkAuction.sol b/contracts/colonyNetwork/ColonyNetworkAuction.sol index 6805ec1b96..1d41b95ee7 100644 --- a/contracts/colonyNetwork/ColonyNetworkAuction.sol +++ b/contracts/colonyNetwork/ColonyNetworkAuction.sol @@ -19,6 +19,7 @@ pragma solidity 0.7.3; import "./ColonyNetworkStorage.sol"; import "./../common/MultiChain.sol"; +import "./../common/BasicMetaTransaction.sol"; contract ColonyNetworkAuction is ColonyNetworkStorage, MultiChain { function startTokenAuction(address _token) public @@ -59,7 +60,7 @@ contract ColonyNetworkAuction is ColonyNetworkStorage, MultiChain { } -contract DutchAuction is DSMath, MultiChain { +contract DutchAuction is DSMath, MultiChain, BasicMetaTransaction { address payable public colonyNetwork; address public metaColonyAddress; ERC20Extended public clnyToken; @@ -81,6 +82,7 @@ contract DutchAuction is DSMath, MultiChain { // Final price in CLNY per 10**18 Tokens (min 1, max 1e36) uint public finalPrice; bool public finalized; + mapping(address => uint256) metatransactionNonces; mapping (address => uint256) public bids; @@ -127,12 +129,20 @@ contract DutchAuction is DSMath, MultiChain { constructor(address _clnyToken, address _token, address _metaColonyAddress) public { require(_metaColonyAddress != address(0x0), "colony-auction-metacolony-cannot-be-zero"); - colonyNetwork = msg.sender; + colonyNetwork = msgSender(); metaColonyAddress = _metaColonyAddress; clnyToken = ERC20Extended(_clnyToken); token = ERC20Extended(_token); } + function getMetatransactionNonce(address userAddress) override public view returns (uint256 nonce){ + return metatransactionNonces[userAddress]; + } + + function incrementMetatransactionNonce(address user) override internal { + metatransactionNonces[user] = add(metatransactionNonces[user], 1); + } + function start() public auctionNotStarted { @@ -207,16 +217,16 @@ contract DutchAuction is DSMath, MultiChain { return; } - if (bids[msg.sender] == 0) { + if (bids[msgSender()] == 0) { bidCount += 1; } - bids[msg.sender] = add(bids[msg.sender], amount); + bids[msgSender()] = add(bids[msgSender()], amount); receivedTotal = add(receivedTotal, amount); - require(clnyToken.transferFrom(msg.sender, address(this), amount), "colony-auction-bid-transfer-failed"); + require(clnyToken.transferFrom(msgSender(), address(this), amount), "colony-auction-bid-transfer-failed"); - emit AuctionBid(msg.sender, amount, sub(_remainingToEndAuction, amount)); + emit AuctionBid(msgSender(), amount, sub(_remainingToEndAuction, amount)); } // Finalize the auction and set the final Token price diff --git a/contracts/colonyNetwork/ColonyNetworkDataTypes.sol b/contracts/colonyNetwork/ColonyNetworkDataTypes.sol index 2ef049b925..6ce7d711a3 100755 --- a/contracts/colonyNetwork/ColonyNetworkDataTypes.sol +++ b/contracts/colonyNetwork/ColonyNetworkDataTypes.sol @@ -137,6 +137,14 @@ interface ColonyNetworkDataTypes { /// @param colony The address of the colony event ExtensionUninstalled(bytes32 indexed extensionId, address indexed colony); + /// @notice Event logged when a token is deployed via transaction through the colony network + /// @param tokenAddress The address of the token deployed + event TokenDeployed(address tokenAddress); + + /// @notice Event logged when a token authority is deployed via transaction through the colony network + /// @param tokenAuthorityAddress The address of the token authority deployed + event TokenAuthorityDeployed(address tokenAuthorityAddress); + struct Skill { // total number of parent skills uint128 nParents; diff --git a/contracts/colonyNetwork/ColonyNetworkENS.sol b/contracts/colonyNetwork/ColonyNetworkENS.sol index 50be5361ab..aa8f83503e 100644 --- a/contracts/colonyNetwork/ColonyNetworkENS.sol +++ b/contracts/colonyNetwork/ColonyNetworkENS.sol @@ -64,19 +64,19 @@ contract ColonyNetworkENS is ColonyNetworkStorage, MultiChain { unowned(userNode, username) { require(bytes(username).length > 0, "colony-user-label-invalid"); - require(bytes(userLabels[msg.sender]).length == 0, "colony-user-label-already-owned"); + require(bytes(userLabels[msgSender()]).length == 0, "colony-user-label-already-owned"); bytes32 subnode = keccak256(abi.encodePacked(username)); bytes32 node = keccak256(abi.encodePacked(userNode, subnode)); - userLabels[msg.sender] = username; - records[node].addr = msg.sender; + userLabels[msgSender()] = username; + records[node].addr = msgSender(); records[node].orbitdb = orbitdb; ENS(ens).setSubnodeOwner(userNode, subnode, address(this)); ENS(ens).setResolver(node, address(this)); - emit UserLabelRegistered(msg.sender, subnode); + emit UserLabelRegistered(msgSender(), subnode); } function registerColonyLabel(string memory colonyName, string memory orbitdb) @@ -86,19 +86,19 @@ contract ColonyNetworkENS is ColonyNetworkStorage, MultiChain { stoppable { require(bytes(colonyName).length > 0, "colony-colony-label-invalid"); - require(bytes(colonyLabels[msg.sender]).length == 0, "colony-already-labeled"); + require(bytes(colonyLabels[msgSender()]).length == 0, "colony-already-labeled"); bytes32 subnode = keccak256(abi.encodePacked(colonyName)); bytes32 node = keccak256(abi.encodePacked(colonyNode, subnode)); - colonyLabels[msg.sender] = colonyName; - records[node].addr = msg.sender; + colonyLabels[msgSender()] = colonyName; + records[node].addr = msgSender(); records[node].orbitdb = orbitdb; ENS(ens).setSubnodeOwner(colonyNode, subnode, address(this)); ENS(ens).setResolver(node, address(this)); - emit ColonyLabelRegistered(msg.sender, subnode); + emit ColonyLabelRegistered(msgSender(), subnode); } function updateColonyOrbitDB(string memory orbitdb) @@ -106,7 +106,7 @@ contract ColonyNetworkENS is ColonyNetworkStorage, MultiChain { calledByColony stoppable { - string storage label = colonyLabels[msg.sender]; + string storage label = colonyLabels[msgSender()]; require(bytes(label).length > 0, "colony-colony-not-labeled"); bytes32 subnode = keccak256(abi.encodePacked(label)); bytes32 node = keccak256(abi.encodePacked(colonyNode, subnode)); @@ -118,7 +118,7 @@ contract ColonyNetworkENS is ColonyNetworkStorage, MultiChain { notCalledByColony stoppable { - string storage label = userLabels[msg.sender]; + string storage label = userLabels[msgSender()]; require(bytes(label).length > 0, "colony-user-not-labeled"); bytes32 subnode = keccak256(abi.encodePacked(label)); bytes32 node = keccak256(abi.encodePacked(userNode, subnode)); diff --git a/contracts/colonyNetwork/ColonyNetworkExtensions.sol b/contracts/colonyNetwork/ColonyNetworkExtensions.sol index 6c27b8b75b..dc4609b4d9 100644 --- a/contracts/colonyNetwork/ColonyNetworkExtensions.sol +++ b/contracts/colonyNetwork/ColonyNetworkExtensions.sol @@ -22,6 +22,8 @@ import "../colony/ColonyDataTypes.sol"; import "../colonyNetwork/IColonyNetwork.sol"; import "../extensions/ColonyExtension.sol"; import "./ColonyNetworkStorage.sol"; +import "./../metaTxToken/MetaTxToken.sol"; +import "./../common/TokenAuthority.sol"; contract ColonyNetworkExtensions is ColonyNetworkStorage { @@ -52,15 +54,15 @@ contract ColonyNetworkExtensions is ColonyNetworkStorage { calledByColony { require(resolvers[_extensionId][_version] != address(0x0), "colony-network-extension-bad-version"); - require(installations[_extensionId][msg.sender] == address(0x0), "colony-network-extension-already-installed"); + require(installations[_extensionId][msgSender()] == address(0x0), "colony-network-extension-already-installed"); EtherRouter extension = new EtherRouter(); - installations[_extensionId][msg.sender] = address(extension); + installations[_extensionId][msgSender()] = address(extension); extension.setResolver(resolvers[_extensionId][_version]); - ColonyExtension(address(extension)).install(msg.sender); + ColonyExtension(address(extension)).install(msgSender()); - emit ExtensionInstalled(_extensionId, msg.sender, _version); + emit ExtensionInstalled(_extensionId, msgSender(), _version); } function upgradeExtension(bytes32 _extensionId, uint256 _newVersion) @@ -68,9 +70,9 @@ contract ColonyNetworkExtensions is ColonyNetworkStorage { stoppable calledByColony { - require(installations[_extensionId][msg.sender] != address(0x0), "colony-network-extension-not-installed"); + require(installations[_extensionId][msgSender()] != address(0x0), "colony-network-extension-not-installed"); - address payable extension = installations[_extensionId][msg.sender]; + address payable extension = installations[_extensionId][msgSender()]; require(_newVersion == ColonyExtension(extension).version() + 1, "colony-network-extension-bad-increment"); require(resolvers[_extensionId][_newVersion] != address(0x0), "colony-network-extension-bad-version"); @@ -78,7 +80,7 @@ contract ColonyNetworkExtensions is ColonyNetworkStorage { ColonyExtension(extension).finishUpgrade(); assert(ColonyExtension(extension).version() == _newVersion); - emit ExtensionUpgraded(_extensionId, msg.sender, _newVersion); + emit ExtensionUpgraded(_extensionId, msgSender(), _newVersion); } function deprecateExtension(bytes32 _extensionId, bool _deprecated) @@ -86,9 +88,9 @@ contract ColonyNetworkExtensions is ColonyNetworkStorage { stoppable calledByColony { - ColonyExtension(installations[_extensionId][msg.sender]).deprecate(_deprecated); + ColonyExtension(installations[_extensionId][msgSender()]).deprecate(_deprecated); - emit ExtensionDeprecated(_extensionId, msg.sender, _deprecated); + emit ExtensionDeprecated(_extensionId, msgSender(), _deprecated); } function uninstallExtension(bytes32 _extensionId) @@ -96,13 +98,13 @@ contract ColonyNetworkExtensions is ColonyNetworkStorage { stoppable calledByColony { - require(installations[_extensionId][msg.sender] != address(0x0), "colony-network-extension-not-installed"); + require(installations[_extensionId][msgSender()] != address(0x0), "colony-network-extension-not-installed"); - ColonyExtension extension = ColonyExtension(installations[_extensionId][msg.sender]); - installations[_extensionId][msg.sender] = address(0x0); + ColonyExtension extension = ColonyExtension(installations[_extensionId][msgSender()]); + installations[_extensionId][msgSender()] = address(0x0); extension.uninstall(); - emit ExtensionUninstalled(_extensionId, msg.sender); + emit ExtensionUninstalled(_extensionId, msgSender()); } // Public view functions @@ -138,4 +140,25 @@ contract ColonyNetworkExtensions is ColonyNetworkStorage { address extension = Resolver(_resolver).lookup(VERSION_SIG); return ColonyExtension(extension).version(); } + + function deployTokenViaNetwork(string memory _name, string memory _symbol, uint8 _decimals) public + stoppable + returns (address) + { + MetaTxToken token = new MetaTxToken(_name, _symbol, _decimals); + token.setOwner(msgSender()); + + emit TokenDeployed(address(token)); + } + + function deployTokenAuthority(address _token, address _colony, address[] memory allowedToTransfer) public + stoppable + returns (address) + { + TokenAuthority tokenAuthority = new TokenAuthority(_token, _colony, allowedToTransfer); + + emit TokenAuthorityDeployed(address(tokenAuthority)); + } + + } diff --git a/contracts/colonyNetwork/ColonyNetworkMining.sol b/contracts/colonyNetwork/ColonyNetworkMining.sol index beab607517..bb456a5728 100644 --- a/contracts/colonyNetwork/ColonyNetworkMining.sol +++ b/contracts/colonyNetwork/ColonyNetworkMining.sol @@ -30,7 +30,7 @@ contract ColonyNetworkMining is ColonyNetworkStorage, MultiChain { // TODO: Can we handle a dispute regarding the very first hash that should be set? modifier onlyReputationMiningCycle () { - require(msg.sender == activeReputationMiningCycle, "colony-reputation-mining-sender-not-active-reputation-cycle"); + require(msgSender() == activeReputationMiningCycle, "colony-reputation-mining-sender-not-active-reputation-cycle"); _; } @@ -227,21 +227,21 @@ contract ColonyNetworkMining is ColonyNetworkStorage, MultiChain { function stakeForMining(uint256 _amount) public stoppable { address clnyToken = IMetaColony(metaColony).getToken(); - uint256 existingObligation = ITokenLocking(tokenLocking).getObligation(msg.sender, clnyToken, address(this)); + uint256 existingObligation = ITokenLocking(tokenLocking).getObligation(msgSender(), clnyToken, address(this)); - ITokenLocking(tokenLocking).approveStake(msg.sender, _amount, clnyToken); - ITokenLocking(tokenLocking).obligateStake(msg.sender, _amount, clnyToken); + ITokenLocking(tokenLocking).approveStake(msgSender(), _amount, clnyToken); + ITokenLocking(tokenLocking).obligateStake(msgSender(), _amount, clnyToken); - miningStakes[msg.sender].timestamp = getNewTimestamp(existingObligation, _amount, miningStakes[msg.sender].timestamp, block.timestamp); - miningStakes[msg.sender].amount = add(miningStakes[msg.sender].amount, _amount); + miningStakes[msgSender()].timestamp = getNewTimestamp(existingObligation, _amount, miningStakes[msgSender()].timestamp, block.timestamp); + miningStakes[msgSender()].amount = add(miningStakes[msgSender()].amount, _amount); } function unstakeForMining(uint256 _amount) public stoppable { address clnyToken = IMetaColony(metaColony).getToken(); // Prevent those involved in a mining cycle withdrawing stake during the mining process. - require(!IReputationMiningCycle(activeReputationMiningCycle).userInvolvedInMiningCycle(msg.sender), "colony-network-hash-submitted"); - ITokenLocking(tokenLocking).deobligateStake(msg.sender, _amount, clnyToken); - miningStakes[msg.sender].amount = sub(miningStakes[msg.sender].amount, _amount); + require(!IReputationMiningCycle(activeReputationMiningCycle).userInvolvedInMiningCycle(msgSender()), "colony-network-hash-submitted"); + ITokenLocking(tokenLocking).deobligateStake(msgSender(), _amount, clnyToken); + miningStakes[msgSender()].amount = sub(miningStakes[msgSender()].amount, _amount); } function getMiningStake(address _user) public stoppable returns (MiningStake memory) { @@ -287,4 +287,19 @@ contract ColonyNetworkMining is ColonyNetworkStorage, MultiChain { return add(mul(prevWeight, _prevTime), mul(currWeight, _currTime)) / add(prevWeight, currWeight); } + + function setMiningResolver(address _miningResolver) public + stoppable + auth + { + require(_miningResolver != address(0x0), "colony-mining-resolver-cannot-be-zero"); + + miningCycleResolver = _miningResolver; + + emit MiningCycleResolverSet(_miningResolver); + } + + function getMiningResolver() public view returns (address) { + return miningCycleResolver; + } } diff --git a/contracts/colonyNetwork/ColonyNetworkStorage.sol b/contracts/colonyNetwork/ColonyNetworkStorage.sol index dc846174e3..45d05b341a 100644 --- a/contracts/colonyNetwork/ColonyNetworkStorage.sol +++ b/contracts/colonyNetwork/ColonyNetworkStorage.sol @@ -27,7 +27,7 @@ import "./ColonyNetworkDataTypes.sol"; // ignore-file-swc-108 -contract ColonyNetworkStorage is CommonStorage, ColonyNetworkDataTypes, DSMath { +contract ColonyNetworkStorage is ColonyNetworkDataTypes, DSMath, CommonStorage { // Number of colonies in the network uint256 colonyCount; // Storage slot 6 // uint256 version number of the latest deployed Colony contract, used in creating new colonies @@ -100,18 +100,24 @@ contract ColonyNetworkStorage is CommonStorage, ColonyNetworkDataTypes, DSMath { // Used for whitelisting payout tokens mapping (address => bool) payoutWhitelist; // Storage slot 40 + uint256 constant METATRANSACTION_NONCES_SLOT = 41; + mapping(address => uint256) metatransactionNonces; // Storage slot 41 + + modifier calledByColony() { - require(_isColony[msg.sender], "colony-caller-must-be-colony"); + require(_isColony[msgSender()], "colony-caller-must-be-colony"); + assert(msgSender() == msg.sender); _; } modifier notCalledByColony() { - require(!_isColony[msg.sender], "colony-caller-must-not-be-colony"); + require(!_isColony[msgSender()], "colony-caller-must-not-be-colony"); _; } modifier calledByMetaColony() { - require(msg.sender == metaColony, "colony-caller-must-be-meta-colony"); + require(msgSender() == metaColony, "colony-caller-must-be-meta-colony"); + assert(msgSender() == msg.sender); _; } } diff --git a/contracts/colonyNetwork/IColonyNetwork.sol b/contracts/colonyNetwork/IColonyNetwork.sol index 3a5a696b9f..bc512481fc 100644 --- a/contracts/colonyNetwork/IColonyNetwork.sol +++ b/contracts/colonyNetwork/IColonyNetwork.sol @@ -19,12 +19,14 @@ pragma solidity >=0.7.3; // ignore-swc-103 pragma experimental "ABIEncoderV2"; import "./../common/IRecovery.sol"; +import "./../common/IBasicMetaTransaction.sol"; + import "./ColonyNetworkDataTypes.sol"; /// @title Colony Network interface /// @notice All externally available functions are available here and registered to work with EtherRouter Network contract -interface IColonyNetwork is ColonyNetworkDataTypes, IRecovery { +interface IColonyNetwork is ColonyNetworkDataTypes, IRecovery, IBasicMetaTransaction { /// @notice Query if a contract implements an interface /// @param interfaceID The interface identifier, as specified in ERC-165 @@ -403,4 +405,21 @@ interface IColonyNetwork is ColonyNetworkDataTypes, IRecovery { /// @notice Called to get the total per-cycle reputation mining reward. /// @return The CLNY awarded per mining cycle to the miners. function getReputationMiningCycleReward() external view returns (uint256); + + /// @notice Called to deploy a token. + /// @dev This is more expensive than deploying a token directly, but is able to be done via + /// a metatransaction + /// @param _name The name of the token + /// @param _symbol The short 'ticket' symbol for the token + /// @param _decimals The number of decimal places that 1 user-facing token can be divided up in to + /// In the case of ETH, and most tokens, this is 18. + function deployTokenViaNetwork(string memory _name, string memory _symbol, uint8 _decimals) external returns (address); + + /// @notice Called to deploy a token authority + /// @dev This is more expensive than deploying a token directly, but is able to be done via + /// a metatransaction + /// @param _token The address of the otken + /// @param _colony The address of the colony in control of the token + /// @param allowedToTransfer An array of addresses that are allowed to transfer the token even if it's locked + function deployTokenAuthority(address _token, address _colony, address[] memory allowedToTransfer) external returns (address); } diff --git a/contracts/common/BasicMetaTransaction.sol b/contracts/common/BasicMetaTransaction.sol new file mode 100644 index 0000000000..02182629d6 --- /dev/null +++ b/contracts/common/BasicMetaTransaction.sol @@ -0,0 +1,54 @@ +pragma solidity 0.7.3; + +import "../../lib/dappsys/math.sol"; +import "./MetaTransactionMsgSender.sol"; +import "./MultiChain.sol"; + +abstract contract BasicMetaTransaction is DSMath, MetaTransactionMsgSender, MultiChain { + + event MetaTransactionExecuted(address user, address payable relayerAddress, bytes functionSignature); + + function getMetatransactionNonce(address _user) public view virtual returns (uint256 nonce); + + // NB if implementing this functionality in a contract with recovery mode, + // you MUST prevent the metatransaction nonces from being editable with recovery mode. + function incrementMetatransactionNonce(address _user) internal virtual; + + /// @notice Main function to be called when user wants to execute meta transaction. + /// The actual function to be called should be passed as param with name functionSignature + /// Here the basic signature recovery is being used. Signature is expected to be generated using + /// personal_sign method. + /// @param _user Address of user trying to do meta transaction + /// @param _payload Function call to make via meta transaction + /// @param _sigR R part of the signature + /// @param _sigS S part of the signature + /// @param _sigV V part of the signature + // slither-disable-next-line locked-ether + function executeMetaTransaction(address _user, bytes memory _payload, + bytes32 _sigR, bytes32 _sigS, uint8 _sigV) public payable returns (bytes memory) { + + require(verify(_user, getMetatransactionNonce(_user), getChainId(), _payload, _sigR, _sigS, _sigV), "metatransaction-signer-signature-mismatch"); + incrementMetatransactionNonce(_user); + + // Append _user at the end to extract it from calling context + (bool success, bytes memory returnData) = address(this).call(abi.encodePacked(_payload, METATRANSACTION_FLAG, _user)); + require(success, "colony-metatx-function-call-unsuccessful"); + + emit MetaTransactionExecuted(_user, msgSender(), _payload); + return returnData; + } + + // Builds a prefixed hash to mimic the behavior of eth_sign. + function prefixed(bytes32 _hash) internal pure returns (bytes32) { + return keccak256(abi.encodePacked("\x19Ethereum Signed Message:\n32", _hash)); + } + + function verify(address _owner, uint256 _nonce, uint256 _chainId, bytes memory _payload, + bytes32 _sigR, bytes32 _sigS, uint8 _sigV) public view returns (bool) { + + bytes32 hash = prefixed(keccak256(abi.encodePacked(_nonce, this, _chainId, _payload))); + address signer = ecrecover(hash, _sigV, _sigR, _sigS); + require(signer != address(0), "colony-metatx-invalid-signature"); + return (_owner == signer); + } +} \ No newline at end of file diff --git a/contracts/common/CommonStorage.sol b/contracts/common/CommonStorage.sol index 96de8060ac..633766a33e 100644 --- a/contracts/common/CommonStorage.sol +++ b/contracts/common/CommonStorage.sol @@ -18,18 +18,29 @@ pragma solidity 0.7.3; import "./../../lib/dappsys/auth.sol"; +import "./../common/MetaTransactionMsgSender.sol"; // ignore-file-swc-131 // ignore-file-swc-108 -contract CommonStorage is DSAuth { +abstract contract CommonStorage is DSAuth, MetaTransactionMsgSender { uint256 constant UINT256_MAX = 2**256 - 1; uint256 constant AUTHORITY_SLOT = 0; uint256 constant OWNER_SLOT = 1; uint256 constant RESOLVER_SLOT = 2; + bytes32 constant PROTECTED = keccak256("Recovery Mode Protected Slot"); + + function protectSlot(uint256 _slot) internal always { + uint256 flagSlot = uint256(keccak256(abi.encodePacked("RECOVERY_PROTECTED", _slot))); + uint256 protectFlag = uint256(PROTECTED); + assembly { + sstore(flagSlot, protectFlag) // ignore-swc-124 + } + } + // Address of the Resolver contract used by EtherRouter for lookups and routing address resolver; // Storage slot 2 (from DSAuth there is authority and owner at storage slots 0 and 1 respectively) diff --git a/contracts/common/ContractRecovery.sol b/contracts/common/ContractRecovery.sol index 415ad5821c..01b83fd14a 100644 --- a/contracts/common/ContractRecovery.sol +++ b/contracts/common/ContractRecovery.sol @@ -34,6 +34,14 @@ contract ContractRecovery is ContractRecoveryDataTypes, CommonStorage { // ignor require(_slot != OWNER_SLOT, "colony-common-protected-variable"); require(_slot != RESOLVER_SLOT, "colony-common-protected-variable"); + bytes32 flag; + uint256 flagSlot = uint256(keccak256(abi.encodePacked("RECOVERY_PROTECTED", _slot))); + assembly { + flag := sload(flagSlot) + } + + require(flag != PROTECTED, "colony-protected-variable"); + // NB. This isn't necessarily a colony - could be ColonyNetwork. But they both have this function, so it's okay. IRecovery(address(this)).checkNotAdditionalProtectedVariable(_slot); // ignore-swc-123 @@ -50,6 +58,9 @@ contract ContractRecovery is ContractRecoveryDataTypes, CommonStorage { // ignor sstore(x, y) // ignore-swc-124 } + // Make sure we're not trying to change a flag protecting something else + require(oldValue != PROTECTED, "colony-protected-variable"); + // Restore key variables recoveryRolesCount = _recoveryRolesCount; @@ -58,7 +69,7 @@ contract ContractRecovery is ContractRecoveryDataTypes, CommonStorage { // ignor recoveryApprovalCount = 0; recoveryEditedTimestamp = block.timestamp; - emit RecoveryStorageSlotSet(msg.sender, _slot, oldValue, _value); + emit RecoveryStorageSlotSet(msgSender(), _slot, oldValue, _value); } function isInRecoveryMode() public view returns (bool) { @@ -70,15 +81,15 @@ contract ContractRecovery is ContractRecoveryDataTypes, CommonStorage { // ignor recoveryApprovalCount = 0; recoveryEditedTimestamp = block.timestamp; - emit RecoveryModeEntered(msg.sender); + emit RecoveryModeEntered(msgSender()); } function approveExitRecovery() public recovery auth { - require(recoveryApprovalTimestamps[msg.sender] < recoveryEditedTimestamp, "colony-recovery-approval-already-given"); // ignore-swc-116 - recoveryApprovalTimestamps[msg.sender] = block.timestamp; + require(recoveryApprovalTimestamps[msgSender()] < recoveryEditedTimestamp, "colony-recovery-approval-already-given"); // ignore-swc-116 + recoveryApprovalTimestamps[msgSender()] = block.timestamp; recoveryApprovalCount++; - emit RecoveryModeExitApproved(msg.sender); + emit RecoveryModeExitApproved(msgSender()); } function exitRecoveryMode() public recovery auth { @@ -91,7 +102,7 @@ contract ContractRecovery is ContractRecoveryDataTypes, CommonStorage { // ignor require(recoveryApprovalCount >= numRequired, "colony-recovery-exit-insufficient-approvals"); recoveryMode = false; - emit RecoveryModeExited(msg.sender); + emit RecoveryModeExited(msgSender()); } // Can only be called by the root role. diff --git a/contracts/common/IBasicMetaTransaction.sol b/contracts/common/IBasicMetaTransaction.sol new file mode 100644 index 0000000000..ae005d4bb8 --- /dev/null +++ b/contracts/common/IBasicMetaTransaction.sol @@ -0,0 +1,21 @@ +pragma solidity 0.7.3; + + +interface IBasicMetaTransaction { + + event MetaTransactionExecuted(address userAddress, address payable relayerAddress, bytes payload); + + /// @notice Executes a metatransaction targeting this contract + /// @param userAddress The address of the user that signed the metatransaction + /// @param payload The transaction data that will be executed if signature valid + /// @param sigR The 'r' part of the signature + /// @param sigS The 's' part of the signature + /// @param sigV The 'v' part of the signature + function executeMetaTransaction(address userAddress, bytes memory payload, + bytes32 sigR, bytes32 sigS, uint8 sigV) external payable returns(bytes memory); + + /// @notice Gets the next metatransaction nonce for user that should be used targeting this contract + /// @param userAddress The address of the user that will sign the metatransaction + function getMetatransactionNonce(address userAddress) external view returns(uint256 nonce); + +} diff --git a/contracts/common/MetaTransactionMsgSender.sol b/contracts/common/MetaTransactionMsgSender.sol new file mode 100644 index 0000000000..c41999ed1c --- /dev/null +++ b/contracts/common/MetaTransactionMsgSender.sol @@ -0,0 +1,28 @@ +pragma solidity 0.7.3; + +import "../../lib/dappsys/math.sol"; + +abstract contract MetaTransactionMsgSender is DSMath { + + bytes32 constant METATRANSACTION_FLAG = keccak256("METATRANSACTION"); + + function msgSender() internal view returns(address payable sender) { + uint256 index = msg.data.length; + if(msg.sender == address(this) && index >= 52) { + bytes memory array = msg.data; + bytes32 flag; + assembly { + flag := mload(add(array, sub(index, 20))) + } + if (flag != METATRANSACTION_FLAG){ + return msg.sender; + } + assembly { + // Load the 32 bytes word from memory with the address on the lower 20 bytes, and mask those. + sender := and(mload(add(array, index)), 0xffffffffffffffffffffffffffffffffffffffff) + } + } else { + return msg.sender; + } + } +} \ No newline at end of file diff --git a/contracts/common/TokenAuthority.sol b/contracts/common/TokenAuthority.sol new file mode 100644 index 0000000000..b33c759934 --- /dev/null +++ b/contracts/common/TokenAuthority.sol @@ -0,0 +1,59 @@ +/* + This file is part of The Colony Network. + + The Colony Network is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + The Colony Network is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with The Colony Network. If not, see . +*/ + +pragma solidity 0.7.3; + +import "../../lib/dappsys/auth.sol"; + + +contract TokenAuthority is DSAuthority { + address public token; + mapping(address => mapping(bytes4 => bool)) authorizations; + + bytes4 constant BURN_FUNC_SIG = bytes4(keccak256("burn(uint256)")); + bytes4 constant BURN_OVERLOAD_FUNC_SIG = bytes4(keccak256("burn(address,uint256)")); + + constructor(address _token, address _colony, address[] memory allowedToTransfer) { + token = _token; + bytes4 transferSig = bytes4(keccak256("transfer(address,uint256)")); + bytes4 transferFromSig = bytes4(keccak256("transferFrom(address,address,uint256)")); + bytes4 mintSig = bytes4(keccak256("mint(uint256)")); + bytes4 mintSigOverload = bytes4(keccak256("mint(address,uint256)")); + + authorizations[_colony][transferSig] = true; + authorizations[_colony][mintSig] = true; + authorizations[_colony][mintSigOverload] = true; + + for (uint i = 0; i < allowedToTransfer.length; i++) { + authorizations[allowedToTransfer[i]][transferSig] = true; + authorizations[allowedToTransfer[i]][transferFromSig] = true; + } + } + + function canCall(address src, address dst, bytes4 sig) public view override returns (bool) { + if (sig == BURN_FUNC_SIG || sig == BURN_OVERLOAD_FUNC_SIG) { + // We allow anyone to burn their own tokens even when the token is still locked + return true; + } + + if (dst != token) { + return false; + } + + return authorizations[src][sig]; + } +} diff --git a/contracts/extensions/CoinMachine.sol b/contracts/extensions/CoinMachine.sol index 155c25d80e..c1b3c6cfb5 100644 --- a/contracts/extensions/CoinMachine.sol +++ b/contracts/extensions/CoinMachine.sol @@ -65,10 +65,19 @@ contract CoinMachine is ColonyExtension { uint256 soldTotal; // Total tokens sold by the coin machine mapping(address => uint256) soldUser; // Tokens sold to a particular user + mapping(address => uint256) metatransactionNonces; + function getMetatransactionNonce(address userAddress) override public view returns (uint256 nonce){ + return metatransactionNonces[userAddress]; + } + + function incrementMetatransactionNonce(address user) override internal { + metatransactionNonces[user]++; + } + // Modifiers modifier onlyRoot() { - require(colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Root), "coin-machine-caller-not-root"); + require(colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root), "coin-machine-caller-not-root"); _; } @@ -196,7 +205,7 @@ contract CoinMachine is ColonyExtension { updatePeriod(); require( - whitelist == address(0x0) || Whitelist(whitelist).isApproved(msg.sender), + whitelist == address(0x0) || Whitelist(whitelist).isApproved(msgSender()), "coin-machine-unauthorised" ); @@ -222,15 +231,15 @@ contract CoinMachine is ColonyExtension { if (purchaseToken == address(0x0)) { require(msg.value >= totalCost, "coin-machine-insufficient-funds"); - if (msg.value > totalCost) { msg.sender.transfer(msg.value - totalCost); } // Refund any balance + if (msg.value > totalCost) { msgSender().transfer(msg.value - totalCost); } // Refund any balance payable(address(colony)).transfer(totalCost); } else { - require(ERC20(purchaseToken).transferFrom(msg.sender, address(colony), totalCost), "coin-machine-purchase-failed"); + require(ERC20(purchaseToken).transferFrom(msgSender(), address(colony), totalCost), "coin-machine-purchase-failed"); } - require(ERC20(token).transfer(msg.sender, numTokens), "coin-machine-transfer-failed"); + require(ERC20(token).transfer(msgSender(), numTokens), "coin-machine-transfer-failed"); - emit TokensBought(msg.sender, numTokens, totalCost); + emit TokensBought(msgSender(), numTokens, totalCost); } /// @notice Bring the token accounting current diff --git a/contracts/extensions/ColonyExtension.sol b/contracts/extensions/ColonyExtension.sol index c9ec705d99..3570228c51 100644 --- a/contracts/extensions/ColonyExtension.sol +++ b/contracts/extensions/ColonyExtension.sol @@ -22,9 +22,9 @@ import "./../../lib/dappsys/math.sol"; import "./../common/EtherRouter.sol"; import "./../colony/IColony.sol"; import "./../colony/ColonyDataTypes.sol"; +import "./../common/BasicMetaTransaction.sol"; - -abstract contract ColonyExtension is DSAuth, DSMath { +abstract contract ColonyExtension is DSAuth, DSMath, BasicMetaTransaction { uint256 constant UINT256_MAX = 2**256 - 1; diff --git a/contracts/extensions/EvaluatedExpenditure.sol b/contracts/extensions/EvaluatedExpenditure.sol index 6f41b4e74c..b4cbaea803 100644 --- a/contracts/extensions/EvaluatedExpenditure.sol +++ b/contracts/extensions/EvaluatedExpenditure.sol @@ -29,6 +29,7 @@ contract EvaluatedExpenditure is ColonyExtension { uint256 constant PAYOUT_MODIFIER_OFFSET = 2; bool constant MAPPING = false; bool constant ARRAY = true; + mapping(address => uint256) metatransactionNonces; /// @notice Returns the identifier of the extension function identifier() public override pure returns (bytes32) { @@ -61,6 +62,14 @@ contract EvaluatedExpenditure is ColonyExtension { selfdestruct(address(uint160(address(colony)))); } + function getMetatransactionNonce(address _userAddress) override public view returns (uint256 nonce){ + return metatransactionNonces[_userAddress]; + } + + function incrementMetatransactionNonce(address _user) override internal { + metatransactionNonces[_user] = add(metatransactionNonces[_user], 1); + } + /// @notice Sets the payout modifiers in given expenditure slots, using the arbitration permission /// @param _permissionDomainId The domainId in which the extension has the arbitration permission /// @param _childSkillIndex The index that the `_domainId` is relative to `_permissionDomainId` diff --git a/contracts/extensions/FundingQueue.sol b/contracts/extensions/FundingQueue.sol index c70c948c7b..674cd38a09 100644 --- a/contracts/extensions/FundingQueue.sol +++ b/contracts/extensions/FundingQueue.sol @@ -71,6 +71,14 @@ contract FundingQueue is ColonyExtension, PatriciaTreeProofs { mapping (uint256 => mapping (address => uint256)) supporters; // Technically a circular singly-linked list mapping (uint256 => uint256) queue; // proposalId => nextProposalId + mapping(address => uint256) metatransactionNonces; + function getMetatransactionNonce(address userAddress) override public view returns (uint256 nonce){ + return metatransactionNonces[userAddress]; + } + + function incrementMetatransactionNonce(address user) override internal { + metatransactionNonces[user]++; + } // Public functions @@ -143,7 +151,7 @@ contract FundingQueue is ColonyExtension, PatriciaTreeProofs { proposalCount++; proposals[proposalCount] = Proposal( ProposalState.Inactive, - msg.sender, + msgSender(), _token, _domainId, 0, @@ -166,7 +174,7 @@ contract FundingQueue is ColonyExtension, PatriciaTreeProofs { require(proposal.state != ProposalState.Cancelled, "funding-queue-already-cancelled"); require(proposal.state != ProposalState.Completed, "funding-queue-already-completed"); - require(proposal.creator == msg.sender, "funding-queue-not-creator"); + require(proposal.creator == msgSender(), "funding-queue-not-creator"); require(queue[_prevId] == _id, "funding-queue-bad-prev-id"); proposal.state = ProposalState.Cancelled; @@ -192,13 +200,13 @@ contract FundingQueue is ColonyExtension, PatriciaTreeProofs { Proposal storage proposal = proposals[_id]; require(proposal.state == ProposalState.Inactive, "funding-queue-not-inactive"); - require(proposal.creator == msg.sender, "funding-queue-not-creator"); + require(proposal.creator == msgSender(), "funding-queue-not-creator"); proposal.state = ProposalState.Active; proposal.domainTotalRep = checkReputation(_id, address(0x0), _key, _value, _branchMask, _siblings); uint256 stake = wmul(proposal.domainTotalRep, STAKE_FRACTION); - colony.obligateStake(msg.sender, proposal.domainId, stake); + colony.obligateStake(msgSender(), proposal.domainId, stake); emit ProposalStaked(_id, proposal.domainTotalRep); } @@ -220,17 +228,17 @@ contract FundingQueue is ColonyExtension, PatriciaTreeProofs { require(proposal.state == ProposalState.Active, "funding-queue-proposal-not-active"); require(_id != _newPrevId, "funding-queue-cannot-insert-after-self"); // NOTE: this may be redundant - uint256 userRep = checkReputation(_id, msg.sender, _key, _value, _branchMask, _siblings); + uint256 userRep = checkReputation(_id, msgSender(), _key, _value, _branchMask, _siblings); require(_backing <= userRep, "funding-queue-insufficient-reputation"); // Update the user's reputation backing - uint256 prevBacking = supporters[_id][msg.sender]; + uint256 prevBacking = supporters[_id][msgSender()]; if (_backing >= prevBacking) { proposal.totalSupport = add(proposal.totalSupport, sub(_backing, prevBacking)); } else { proposal.totalSupport = sub(proposal.totalSupport, sub(prevBacking, _backing)); } - supporters[_id][msg.sender] = _backing; + supporters[_id][msgSender()] = _backing; // Remove the proposal from its current position, if exists require(queue[_currPrevId] == _id, "funding-queue-bad-prev-id"); @@ -257,7 +265,7 @@ contract FundingQueue is ColonyExtension, PatriciaTreeProofs { queue[_newPrevId] = _id; // prev proposal => this proposal queue[_id] = nextId; // this proposal => next proposal - emit ProposalBacked(_id, _newPrevId, msg.sender, _backing, prevBacking); + emit ProposalBacked(_id, _newPrevId, msgSender(), _backing, prevBacking); } function pingProposal(uint256 _id) public { diff --git a/contracts/extensions/OneTxPayment.sol b/contracts/extensions/OneTxPayment.sol index 28195389bc..ecbfcb4aa8 100644 --- a/contracts/extensions/OneTxPayment.sol +++ b/contracts/extensions/OneTxPayment.sol @@ -29,6 +29,15 @@ contract OneTxPayment is ColonyExtension { ColonyDataTypes.ColonyRole constant ADMINISTRATION = ColonyDataTypes.ColonyRole.Administration; ColonyDataTypes.ColonyRole constant FUNDING = ColonyDataTypes.ColonyRole.Funding; + mapping(address => uint256) metatransactionNonces; + function getMetatransactionNonce(address userAddress) override public view returns (uint256 nonce){ + return metatransactionNonces[userAddress]; + } + + function incrementMetatransactionNonce(address user) override internal { + metatransactionNonces[user]++; + } + /// @notice Returns the identifier of the extension function identifier() public override pure returns (bytes32) { return keccak256("OneTxPayment"); @@ -109,8 +118,8 @@ contract OneTxPayment is ColonyExtension { require(_workers.length == _tokens.length && _workers.length == _amounts.length, "one-tx-payment-invalid-input"); require( - colony.hasInheritedUserRole(msg.sender, 1, FUNDING, _childSkillIndex, _domainId) && - colony.hasInheritedUserRole(msg.sender, _callerPermissionDomainId, ADMINISTRATION, _callerChildSkillIndex, _domainId), + colony.hasInheritedUserRole(msgSender(), 1, FUNDING, _childSkillIndex, _domainId) && + colony.hasInheritedUserRole(msgSender(), _callerPermissionDomainId, ADMINISTRATION, _callerChildSkillIndex, _domainId), "one-tx-payment-not-authorized" ); @@ -125,7 +134,7 @@ contract OneTxPayment is ColonyExtension { colony.finalizePayment(1, _childSkillIndex, paymentId); colony.claimPayment(paymentId, _tokens[0]); - emit OneTxPaymentMade(msg.sender, paymentId, _workers.length); + emit OneTxPaymentMade(msgSender(), paymentId, _workers.length); } else { uint256 expenditureId = colony.makeExpenditure(1, _childSkillIndex, _domainId); @@ -157,7 +166,7 @@ contract OneTxPayment is ColonyExtension { finalizeAndClaim(expenditureId, _workers, _tokens); - emit OneTxPaymentMade(msg.sender, expenditureId, _workers.length); + emit OneTxPaymentMade(msgSender(), expenditureId, _workers.length); } } @@ -190,8 +199,8 @@ contract OneTxPayment is ColonyExtension { require(_workers.length == _tokens.length && _workers.length == _amounts.length, "one-tx-payment-invalid-input"); require( - colony.hasInheritedUserRole(msg.sender, _callerPermissionDomainId, FUNDING, _callerChildSkillIndex, _domainId) && - colony.hasInheritedUserRole(msg.sender, _callerPermissionDomainId, ADMINISTRATION, _callerChildSkillIndex, _domainId), + colony.hasInheritedUserRole(msgSender(), _callerPermissionDomainId, FUNDING, _callerChildSkillIndex, _domainId) && + colony.hasInheritedUserRole(msgSender(), _callerPermissionDomainId, ADMINISTRATION, _callerChildSkillIndex, _domainId), "one-tx-payment-not-authorized" ); @@ -207,7 +216,7 @@ contract OneTxPayment is ColonyExtension { colony.finalizePayment(_permissionDomainId, _childSkillIndex, paymentId); colony.claimPayment(paymentId, _tokens[0]); - emit OneTxPaymentMade(msg.sender, paymentId, _workers.length); + emit OneTxPaymentMade(msgSender(), paymentId, _workers.length); } else { uint256 expenditureId = colony.makeExpenditure(_permissionDomainId, _childSkillIndex, _domainId); @@ -240,7 +249,7 @@ contract OneTxPayment is ColonyExtension { finalizeAndClaim(expenditureId, _workers, _tokens); - emit OneTxPaymentMade(msg.sender, expenditureId, _workers.length); + emit OneTxPaymentMade(msgSender(), expenditureId, _workers.length); } } diff --git a/contracts/extensions/TokenSupplier.sol b/contracts/extensions/TokenSupplier.sol index fe2d220597..340712c267 100644 --- a/contracts/extensions/TokenSupplier.sol +++ b/contracts/extensions/TokenSupplier.sol @@ -39,6 +39,14 @@ contract TokenSupplier is ColonyExtension { uint256 tokenIssuanceRate; uint256 lastIssue; uint256 lastRateUpdate; + mapping(address => uint256) metatransactionNonces; + function getMetatransactionNonce(address userAddress) override public view returns (uint256 nonce){ + return metatransactionNonces[userAddress]; + } + + function incrementMetatransactionNonce(address user) override internal { + metatransactionNonces[user]++; + } // Modifiers @@ -168,11 +176,11 @@ contract TokenSupplier is ColonyExtension { // Internal functions function isRoot() internal view returns (bool) { - return colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Root); + return colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root); } function isRootFunding() internal view returns (bool) { - return colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Funding); + return colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Funding); } } diff --git a/contracts/extensions/VotingReputation.sol b/contracts/extensions/VotingReputation.sol index 3b6021767f..7d4f335465 100644 --- a/contracts/extensions/VotingReputation.sol +++ b/contracts/extensions/VotingReputation.sol @@ -90,11 +90,19 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs { uint256 submitPeriod; // Length of time for submitting votes uint256 revealPeriod; // Length of time for revealing votes uint256 escalationPeriod; // Length of time for escalating after a vote + mapping(address => uint256) metatransactionNonces; + function getMetatransactionNonce(address userAddress) override public view returns (uint256 nonce){ + return metatransactionNonces[userAddress]; + } + + function incrementMetatransactionNonce(address user) override internal { + metatransactionNonces[user]++; + } // Modifiers modifier onlyRoot() { - require(colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Root), "voting-rep-caller-not-root"); + require(colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root), "voting-rep-caller-not-root"); _; } @@ -284,7 +292,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs { motion.altTarget = _altTarget; motion.action = _action; - emit MotionCreated(motionCount, msg.sender, _domainId); + emit MotionCreated(motionCount, msgSender(), _domainId); } /// @notice Create a motion in the root domain (DEPRECATED) @@ -360,10 +368,10 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs { uint256 amount = min(_amount, sub(requiredStake, motion.stakes[_vote])); require(amount > 0, "voting-rep-bad-amount"); - uint256 stakerTotalAmount = add(stakes[_motionId][msg.sender][_vote], amount); + uint256 stakerTotalAmount = add(stakes[_motionId][msgSender()][_vote], amount); require( - stakerTotalAmount <= getReputationFromProof(_motionId, msg.sender, _key, _value, _branchMask, _siblings), + stakerTotalAmount <= getReputationFromProof(_motionId, msgSender(), _key, _value, _branchMask, _siblings), "voting-rep-insufficient-rep" ); require( @@ -374,7 +382,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs { // Update the stake motion.stakes[_vote] = add(motion.stakes[_vote], amount); - stakes[_motionId][msg.sender][_vote] = stakerTotalAmount; + stakes[_motionId][msgSender()][_vote] = stakerTotalAmount; // Increment counter & extend claim delay if staking for an expenditure state change if ( @@ -391,7 +399,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs { require(executeCall(_motionId, claimDelayAction), "voting-rep-expenditure-lock-failed"); } - emit MotionStaked(_motionId, msg.sender, _vote, amount); + emit MotionStaked(_motionId, msgSender(), _vote, amount); // Move to vote submission once both sides are fully staked if (motion.stakes[NAY] == requiredStake && motion.stakes[YAY] == requiredStake) { @@ -419,8 +427,8 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs { // Do the external bookkeeping tokenLocking.deposit(token, 0, true); // Faux deposit to clear any locks - colony.obligateStake(msg.sender, motion.domainId, amount); - colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), msg.sender, motion.domainId, amount, address(this)); + colony.obligateStake(msgSender(), motion.domainId, amount); + colony.transferStake(_permissionDomainId, _childSkillIndex, address(this), msgSender(), motion.domainId, amount, address(this)); } /// @notice Submit a vote secret for a motion @@ -444,16 +452,16 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs { require(getMotionState(_motionId) == MotionState.Submit, "voting-rep-motion-not-open"); require(_voteSecret != bytes32(0), "voting-rep-invalid-secret"); - uint256 userRep = getReputationFromProof(_motionId, msg.sender, _key, _value, _branchMask, _siblings); + uint256 userRep = getReputationFromProof(_motionId, msgSender(), _key, _value, _branchMask, _siblings); // Count reputation if first submission - if (voteSecrets[_motionId][msg.sender] == bytes32(0)) { + if (voteSecrets[_motionId][msgSender()] == bytes32(0)) { motion.repSubmitted = add(motion.repSubmitted, userRep); } - voteSecrets[_motionId][msg.sender] = _voteSecret; + voteSecrets[_motionId][msgSender()] = _voteSecret; - emit MotionVoteSubmitted(_motionId, msg.sender); + emit MotionVoteSubmitted(_motionId, msgSender()); if (motion.repSubmitted >= wmul(motion.skillRep, maxVoteFraction)) { motion.events[SUBMIT_END] = uint64(block.timestamp); @@ -486,17 +494,17 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs { require(getMotionState(_motionId) == MotionState.Reveal, "voting-rep-motion-not-reveal"); require(_vote <= 1, "voting-rep-bad-vote"); - uint256 userRep = getReputationFromProof(_motionId, msg.sender, _key, _value, _branchMask, _siblings); + uint256 userRep = getReputationFromProof(_motionId, msgSender(), _key, _value, _branchMask, _siblings); motion.votes[_vote] = add(motion.votes[_vote], userRep); - bytes32 voteSecret = voteSecrets[_motionId][msg.sender]; + bytes32 voteSecret = voteSecrets[_motionId][msgSender()]; require(voteSecret == getVoteSecret(_salt, _vote), "voting-rep-secret-no-match"); - delete voteSecrets[_motionId][msg.sender]; + delete voteSecrets[_motionId][msgSender()]; uint256 voterReward = getVoterReward(_motionId, userRep); motion.paidVoterComp = add(motion.paidVoterComp, voterReward); - emit MotionVoteRevealed(_motionId, msg.sender, _vote); + emit MotionVoteRevealed(_motionId, msgSender(), _vote); // See if reputation revealed matches reputation submitted if (add(motion.votes[NAY], motion.votes[YAY]) == motion.repSubmitted) { @@ -505,7 +513,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs { emit MotionEventSet(_motionId, REVEAL_END); } - tokenLocking.transfer(token, voterReward, msg.sender, true); + tokenLocking.transfer(token, voterReward, msgSender(), true); } /// @notice Escalate a motion to a higher domain @@ -553,7 +561,7 @@ contract VotingReputation is ColonyExtension, PatriciaTreeProofs { motion.escalated = true; - emit MotionEscalated(_motionId, msg.sender, domainId, _newDomainId); + emit MotionEscalated(_motionId, msgSender(), domainId, _newDomainId); if (motion.events[STAKE_END] <= uint64(block.timestamp)) { emit MotionEventSet(_motionId, STAKE_END); diff --git a/contracts/extensions/Whitelist.sol b/contracts/extensions/Whitelist.sol index c6078fd8f2..76a9a3f166 100644 --- a/contracts/extensions/Whitelist.sol +++ b/contracts/extensions/Whitelist.sol @@ -37,6 +37,16 @@ contract Whitelist is ColonyExtension { mapping (address => bool) approvals; mapping (address => bool) signatures; + mapping(address => uint256) metatransactionNonces; + + function getMetatransactionNonce(address userAddress) override public view returns (uint256 nonce){ + return metatransactionNonces[userAddress]; + } + + function incrementMetatransactionNonce(address user) override internal { + metatransactionNonces[user]++; + } + // Modifiers @@ -82,7 +92,7 @@ contract Whitelist is ColonyExtension { /// @param _useApprovals Whether or not to require administrative approval /// @param _agreementHash An agreement hash (such as an IPFS URI) function initialise(bool _useApprovals, string memory _agreementHash) public { - require(colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Root), "whitelist-unauthorised"); + require(colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Root), "whitelist-unauthorised"); require(!useApprovals && bytes(agreementHash).length == 0, "whitelist-already-initialised"); require(_useApprovals || bytes(_agreementHash).length > 0, "whitelist-bad-initialisation"); @@ -97,7 +107,7 @@ contract Whitelist is ColonyExtension { /// @param _status The whitelist status to set function approveUsers(address[] memory _users, bool _status) public initialised notDeprecated { require(useApprovals, "whitelist-no-approvals"); - require(colony.hasUserRole(msg.sender, 1, ColonyDataTypes.ColonyRole.Administration), "whitelist-unauthorised"); + require(colony.hasUserRole(msgSender(), 1, ColonyDataTypes.ColonyRole.Administration), "whitelist-unauthorised"); for (uint256 i; i < _users.length; i++) { approvals[_users[i]] = _status; @@ -116,9 +126,9 @@ contract Whitelist is ColonyExtension { "whitelist-bad-signature" ); - signatures[msg.sender] = true; + signatures[msgSender()] = true; - emit AgreementSigned(msg.sender); + emit AgreementSigned(msgSender()); } /// @notice Get the user's overall whitelist status diff --git a/contracts/metaTxToken/DSAuthMeta.sol b/contracts/metaTxToken/DSAuthMeta.sol new file mode 100644 index 0000000000..e0955b5bf6 --- /dev/null +++ b/contracts/metaTxToken/DSAuthMeta.sol @@ -0,0 +1,61 @@ +// This program 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. + +// This program 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 this program. If not, see . +import "./../common/ERC20Extended.sol"; +import "./../common/BasicMetaTransaction.sol"; +import "./../common/ERC20Extended.sol"; +import "./../../lib/dappsys/auth.sol"; + +pragma solidity 0.7.3; + +abstract contract DSAuthMeta is DSAuthEvents, BasicMetaTransaction { + DSAuthority public authority; + address public owner; + + constructor() { + owner = msgSender(); + emit LogSetOwner(msgSender()); + } + + function setOwner(address owner_) + public + auth + { + owner = owner_; + emit LogSetOwner(owner); + } + + function setAuthority(DSAuthority authority_) + public + auth + { + authority = authority_; + emit LogSetAuthority(address(authority)); + } + + modifier auth { + require(isAuthorized(msgSender(), msg.sig), "ds-auth-unauthorized"); + _; + } + + function isAuthorized(address src, bytes4 sig) internal view returns (bool) { + if (src == address(this)) { + return true; + } else if (src == owner) { + return true; + } else if (authority == DSAuthority(0)) { + return false; + } else { + return authority.canCall(src, address(this), sig); + } + } +} \ No newline at end of file diff --git a/contracts/metaTxToken/DSTokenBaseMeta.sol b/contracts/metaTxToken/DSTokenBaseMeta.sol new file mode 100644 index 0000000000..34c0760b77 --- /dev/null +++ b/contracts/metaTxToken/DSTokenBaseMeta.sol @@ -0,0 +1,75 @@ +/// base.sol -- basic ERC20 implementation + +// Copyright (C) 2015, 2016, 2017 DappHub, LLC + +// This program 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. + +// This program 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 this program. If not, see . + +// Modified to inherit BasicMetaTransaction and use msgSender() where appropriate + +pragma solidity 0.7.3; + +import "./../../lib/dappsys/erc20.sol"; +import "./../../lib/dappsys/math.sol"; +import "./../common/BasicMetaTransaction.sol"; + +abstract contract DSTokenBaseMeta is ERC20, DSMath, BasicMetaTransaction { + uint256 _supply; + mapping (address => uint256) _balances; + mapping (address => mapping (address => uint256)) _approvals; + + constructor(uint256 supply) { + _balances[msgSender()] = supply; + _supply = supply; + } + + function totalSupply() public override view returns (uint) { + return _supply; + } + function balanceOf(address src) public override view returns (uint) { + return _balances[src]; + } + function allowance(address src, address guy) public override view returns (uint) { + return _approvals[src][guy]; + } + + function transfer(address dst, uint256 wad) public override returns (bool) { + return transferFrom(msgSender(), dst, wad); + } + + function transferFrom(address src, address dst, uint256 wad) + public override virtual + returns (bool) + { + if (src != msgSender()) { + require(_approvals[src][msgSender()] >= wad, "ds-token-insufficient-approval"); + _approvals[src][msgSender()] = sub(_approvals[src][msgSender()], wad); + } + + require(_balances[src] >= wad, "ds-token-insufficient-balance"); + _balances[src] = sub(_balances[src], wad); + _balances[dst] = add(_balances[dst], wad); + + emit Transfer(src, dst, wad); + + return true; + } + + function approve(address guy, uint256 wad) public override returns (bool) { + _approvals[msgSender()][guy] = wad; + + emit Approval(msgSender(), guy, wad); + + return true; + } +} diff --git a/contracts/metaTxToken/MetaTxToken.sol b/contracts/metaTxToken/MetaTxToken.sol new file mode 100644 index 0000000000..0286f7ee91 --- /dev/null +++ b/contracts/metaTxToken/MetaTxToken.sol @@ -0,0 +1,138 @@ +/* + This file is part of The Colony Network. + + The Colony Network is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + The Colony Network is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with The Colony Network. If not, see . +*/ + +pragma solidity 0.7.3; + +import "./DSTokenBaseMeta.sol"; +import "./DSAuthMeta.sol"; + +contract MetaTxToken is DSTokenBaseMeta(0), DSAuthMeta { + uint8 public decimals; + string public symbol; + string public name; + + bool public locked; + bytes32 public DOMAIN_SEPARATOR; + + mapping(address => uint256) metatransactionNonces; + + event Mint(address indexed guy, uint256 wad); + event Burn(address indexed guy, uint256 wad); + + function getMetatransactionNonce(address _user) override public view returns (uint256 nonce){ + return metatransactionNonces[_user]; + } + + function incrementMetatransactionNonce(address _user) override internal { + metatransactionNonces[_user]++; + } + + modifier unlocked { + if (locked) { + require(isAuthorized(msgSender(), msg.sig), "colony-token-unauthorised"); + } + _; + } + + constructor(string memory _name, string memory _symbol, uint8 _decimals) { + name = _name; + symbol = _symbol; + decimals = _decimals; + locked = true; + + uint256 chainId; + assembly { + chainId := chainid() + } + + DOMAIN_SEPARATOR = keccak256( + abi.encode( + keccak256("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)"), + keccak256(bytes(name)), + keccak256(bytes("1")), + chainId, + address(this) + ) + ); + } + + function transferFrom(address src, address dst, uint256 wad) public + unlocked override + returns (bool) + { + return super.transferFrom(src, dst, wad); + } + + function mint(uint256 wad) public auth { + mint(msgSender(), wad); + } + + function burn(uint256 wad) public { + burn(msgSender(), wad); + } + + function mint(address guy, uint256 wad) public auth { + _balances[guy] = add(_balances[guy], wad); + _supply = add(_supply, wad); + + emit Mint(guy, wad); + emit Transfer(address(0x0), guy, wad); + } + + function burn(address guy, uint256 wad) public { + if (guy != msgSender()) { + require(_approvals[guy][msgSender()] >= wad, "ds-token-insufficient-approval"); + _approvals[guy][msgSender()] = sub(_approvals[guy][msgSender()], wad); + } + + require(_balances[guy] >= wad, "ds-token-insufficient-balance"); + _balances[guy] = sub(_balances[guy], wad); + _supply = sub(_supply, wad); + + emit Burn(guy, wad); + } + + function unlock() public + auth + { + locked = false; + } + + // Pinched from https://github.com/Uniswap/uniswap-v2-core/blob/master/contracts/UniswapV2ERC20.sol + // Which is also licenced under GPL V3 + + // keccak256("Permit(address owner,address spender,uint256 value,uint256 nonce,uint256 deadline)"); + bytes32 public constant PERMIT_TYPEHASH = 0x6e71edae12b1b97f4d1f60370fef10105fa2faae0126114a169c64845d6126c9; + string constant EIP_712_PREFIX = "\x19\x01"; + + function permit(address owner, address spender, uint256 value, uint256 deadline, uint8 v, bytes32 r, bytes32 s) external unlocked { + require(deadline >= block.timestamp, "colony-token-expired-deadline"); + + bytes32 digest = keccak256( + abi.encodePacked( + EIP_712_PREFIX, + DOMAIN_SEPARATOR, + keccak256(abi.encode(PERMIT_TYPEHASH, owner, spender, value, metatransactionNonces[owner]++, deadline)) + ) + ); + address recoveredAddress = ecrecover(digest, v, r, s); + require(recoveredAddress != address(0) && recoveredAddress == owner, "colony-token-invalid-signature"); + _approvals[owner][spender] = value; + + emit Approval(owner, spender, value); + } +} \ No newline at end of file diff --git a/contracts/testHelpers/TestExtensions.sol b/contracts/testHelpers/TestExtensions.sol index e61e048fd4..0503cc7dc6 100644 --- a/contracts/testHelpers/TestExtensions.sol +++ b/contracts/testHelpers/TestExtensions.sol @@ -37,6 +37,16 @@ abstract contract TestExtension is ColonyExtension { function uninstall() public override auth { selfdestruct(address(uint160(address(colony)))); } + + mapping(address => uint256) metatransactionNonces; + function getMetatransactionNonce(address userAddress) override public view returns (uint256 nonce){ + return metatransactionNonces[userAddress]; + } + + function incrementMetatransactionNonce(address user) override internal { + metatransactionNonces[user] = add(metatransactionNonces[user], 1); + } + } diff --git a/contracts/tokenLocking/ITokenLocking.sol b/contracts/tokenLocking/ITokenLocking.sol index 7ce262bbcb..74336b4a2f 100644 --- a/contracts/tokenLocking/ITokenLocking.sol +++ b/contracts/tokenLocking/ITokenLocking.sol @@ -19,9 +19,10 @@ pragma solidity >=0.7.3; // ignore-swc-103 pragma experimental "ABIEncoderV2"; import "./TokenLockingDataTypes.sol"; +import "./../common/IBasicMetaTransaction.sol"; -interface ITokenLocking is TokenLockingDataTypes { +interface ITokenLocking is TokenLockingDataTypes, IBasicMetaTransaction { /// @notice Set the ColonyNetwork contract address. /// @dev ColonyNetwork is used for checking if sender is a colony created on colony network. diff --git a/contracts/tokenLocking/TokenLocking.sol b/contracts/tokenLocking/TokenLocking.sol index 51e3f1dd21..1a11cd1b32 100644 --- a/contracts/tokenLocking/TokenLocking.sol +++ b/contracts/tokenLocking/TokenLocking.sol @@ -22,14 +22,15 @@ import "./../../lib/dappsys/math.sol"; import "./../colony/IMetaColony.sol"; import "./../colonyNetwork/IColonyNetwork.sol"; import "./../common/ERC20Extended.sol"; +import "./../common/BasicMetaTransaction.sol"; import "./../reputationMiningCycle/IReputationMiningCycle.sol"; import "./../tokenLocking/TokenLockingStorage.sol"; -contract TokenLocking is TokenLockingStorage, DSMath { // ignore-swc-123 +contract TokenLocking is TokenLockingStorage, DSMath, BasicMetaTransaction { // ignore-swc-123 modifier calledByColonyOrNetwork() { require( - colonyNetwork == msg.sender || IColonyNetwork(colonyNetwork).isColony(msg.sender), + colonyNetwork == msgSender() || IColonyNetwork(colonyNetwork).isColony(msgSender()), "colony-token-locking-sender-not-colony-or-network" ); _; @@ -37,15 +38,15 @@ contract TokenLocking is TokenLockingStorage, DSMath { // ignore-swc-123 modifier tokenNotLocked(address _token, bool _force) { if (_force) { - userLocks[_token][msg.sender].lockCount = totalLockCount[_token]; + userLocks[_token][msgSender()].lockCount = totalLockCount[_token]; } - require(isTokenUnlocked(_token, msg.sender), "colony-token-locking-token-locked"); + require(isTokenUnlocked(_token, msgSender()), "colony-token-locking-token-locked"); _; } modifier notObligated(address _token, uint256 _amount) { require( - sub(userLocks[_token][msg.sender].balance, _amount) >= totalObligations[msg.sender][_token], + sub(userLocks[_token][msgSender()].balance, _amount) >= totalObligations[msgSender()][_token], "colony-token-locking-excess-obligation" ); _; @@ -53,6 +54,14 @@ contract TokenLocking is TokenLockingStorage, DSMath { // ignore-swc-123 // Public functions + function getMetatransactionNonce(address userAddress) override public view returns (uint256 nonce){ + return metatransactionNonces[userAddress]; + } + + function incrementMetatransactionNonce(address user) override internal { + metatransactionNonces[user] = add(metatransactionNonces[user], 1); + } + function setColonyNetwork(address _colonyNetwork) public auth { require(_colonyNetwork != address(0x0), "colony-token-locking-network-cannot-be-zero"); @@ -67,9 +76,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; + lockers[_token][totalLockCount[_token]] = msgSender(); - emit TokenLocked(_token, msg.sender, totalLockCount[_token]); + emit TokenLocked(_token, msgSender(), totalLockCount[_token]); return totalLockCount[_token]; } @@ -77,7 +86,7 @@ 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"); + require(lockers[_token][_lockId] == msgSender(), "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"); @@ -93,8 +102,8 @@ contract TokenLocking is TokenLockingStorage, DSMath { // ignore-swc-123 } function incrementLockCounterTo(address _token, uint256 _lockId) public { - require(_lockId <= totalLockCount[_token] && _lockId > userLocks[_token][msg.sender].lockCount, "colony-token-locking-invalid-lock-id"); - userLocks[_token][msg.sender].lockCount = _lockId; + require(_lockId <= totalLockCount[_token] && _lockId > userLocks[_token][msgSender()].lockCount, "colony-token-locking-invalid-lock-id"); + userLocks[_token][msgSender()].lockCount = _lockId; } // Deprecated interface @@ -103,7 +112,7 @@ contract TokenLocking is TokenLockingStorage, DSMath { // ignore-swc-123 } function deposit(address _token, uint256 _amount, bool _force) public tokenNotLocked(_token, _force) { - Lock storage lock = userLocks[_token][msg.sender]; + Lock storage lock = userLocks[_token][msgSender()]; lock.balance = add(lock.balance, _amount); // Handle the pendingBalance, if any (idempotent operation) @@ -113,13 +122,13 @@ contract TokenLocking is TokenLockingStorage, DSMath { // ignore-swc-123 } // Actually claim the tokens - require(ERC20Extended(_token).transferFrom(msg.sender, address(this), _amount), "colony-token-locking-transfer-failed"); // ignore-swc-123 + require(ERC20Extended(_token).transferFrom(msgSender(), address(this), _amount), "colony-token-locking-transfer-failed"); // ignore-swc-123 - emit UserTokenDeposited(_token, msg.sender, lock.balance); + emit UserTokenDeposited(_token, msgSender(), lock.balance); } function depositFor(address _token, uint256 _amount, address _recipient) public { - require(ERC20Extended(_token).transferFrom(msg.sender, address(this), _amount), "colony-token-locking-transfer-failed"); // ignore-swc-123 + require(ERC20Extended(_token).transferFrom(msgSender(), address(this), _amount), "colony-token-locking-transfer-failed"); // ignore-swc-123 makeConditionalDeposit(_token, _amount, _recipient); @@ -130,12 +139,12 @@ contract TokenLocking is TokenLockingStorage, DSMath { // ignore-swc-123 notObligated(_token, _amount) tokenNotLocked(_token, _force) { - Lock storage userLock = userLocks[_token][msg.sender]; + Lock storage userLock = userLocks[_token][msgSender()]; userLock.balance = sub(userLock.balance, _amount); makeConditionalDeposit(_token, _amount, _recipient); - emit UserTokenTransferred(_token, msg.sender, _recipient, _amount); + emit UserTokenTransferred(_token, msgSender(), _recipient, _amount); } // Deprecated interface @@ -147,23 +156,23 @@ contract TokenLocking is TokenLockingStorage, DSMath { // ignore-swc-123 notObligated(_token, _amount) tokenNotLocked(_token, _force) { - Lock storage lock = userLocks[_token][msg.sender]; + Lock storage lock = userLocks[_token][msgSender()]; lock.balance = sub(lock.balance, _amount); - require(ERC20Extended(_token).transfer(msg.sender, _amount), "colony-token-locking-transfer-failed"); + require(ERC20Extended(_token).transfer(msgSender(), _amount), "colony-token-locking-transfer-failed"); - emit UserTokenWithdrawn(_token, msg.sender, _amount); + emit UserTokenWithdrawn(_token, msgSender(), _amount); } function approveStake(address _user, uint256 _amount, address _token) public calledByColonyOrNetwork() { - approvals[_user][_token][msg.sender] = add(approvals[_user][_token][msg.sender], _amount); + approvals[_user][_token][msgSender()] = add(approvals[_user][_token][msgSender()], _amount); emit UserTokenApproved(_token, _user, msg.sender, _amount); } function obligateStake(address _user, uint256 _amount, address _token) public calledByColonyOrNetwork() { - approvals[_user][_token][msg.sender] = sub(approvals[_user][_token][msg.sender], _amount); - obligations[_user][_token][msg.sender] = add(obligations[_user][_token][msg.sender], _amount); + approvals[_user][_token][msgSender()] = sub(approvals[_user][_token][msgSender()], _amount); + obligations[_user][_token][msgSender()] = add(obligations[_user][_token][msgSender()], _amount); totalObligations[_user][_token] = add(totalObligations[_user][_token], _amount); require(userLocks[_token][_user].balance >= totalObligations[_user][_token], "colony-token-locking-insufficient-deposit"); @@ -172,14 +181,14 @@ contract TokenLocking is TokenLockingStorage, DSMath { // ignore-swc-123 } function deobligateStake(address _user, uint256 _amount, address _token) public calledByColonyOrNetwork() { - obligations[_user][_token][msg.sender] = sub(obligations[_user][_token][msg.sender], _amount); + obligations[_user][_token][msgSender()] = sub(obligations[_user][_token][msgSender()], _amount); totalObligations[_user][_token] = sub(totalObligations[_user][_token], _amount); emit UserTokenDeobligated(_token, _user, msg.sender, _amount); } function transferStake(address _user, uint256 _amount, address _token, address _recipient) public calledByColonyOrNetwork() { - obligations[_user][_token][msg.sender] = sub(obligations[_user][_token][msg.sender], _amount); + obligations[_user][_token][msgSender()] = sub(obligations[_user][_token][msgSender()], _amount); totalObligations[_user][_token] = sub(totalObligations[_user][_token], _amount); // Transfer the the tokens diff --git a/contracts/tokenLocking/TokenLockingStorage.sol b/contracts/tokenLocking/TokenLockingStorage.sol index ad9780db07..616f424a90 100644 --- a/contracts/tokenLocking/TokenLockingStorage.sol +++ b/contracts/tokenLocking/TokenLockingStorage.sol @@ -46,4 +46,6 @@ contract TokenLockingStorage is TokenLockingDataTypes, DSAuth { // Keep track of which colony is placing which lock ([token][lockId] => colony) mapping (address => mapping (uint256 => address)) lockers; + + mapping(address => uint256) metatransactionNonces; } diff --git a/docs/_Interface_IColonyNetwork.md b/docs/_Interface_IColonyNetwork.md index e0a507aa88..d60e5204ce 100644 --- a/docs/_Interface_IColonyNetwork.md +++ b/docs/_Interface_IColonyNetwork.md @@ -221,6 +221,46 @@ Create the Meta Colony, same as a normal colony plus the root skill. |_tokenAddress|address|Address of the CLNY token +### `deployTokenAuthority` + +Called to deploy a token authority + +*Note: This is more expensive than deploying a token directly, but is able to be done via a metatransaction* + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_token|address|The address of the otken +|_colony|address|The address of the colony in control of the token +|allowedToTransfer|address[]|An array of addresses that are allowed to transfer the token even if it's locked + +**Return Parameters** + +|Name|Type|Description| +|---|---|---| +|address|address| + +### `deployTokenViaNetwork` + +Called to deploy a token. + +*Note: This is more expensive than deploying a token directly, but is able to be done via a metatransaction* + +**Parameters** + +|Name|Type|Description| +|---|---|---| +|_name|string|The name of the token +|_symbol|string|The short 'ticket' symbol for the token +|_decimals|uint8|The number of decimal places that 1 user-facing token can be divided up in to In the case of ETH, and most tokens, this is 18. + +**Return Parameters** + +|Name|Type|Description| +|---|---|---| +|address|address| + ### `deprecateExtension` Set the deprecation of an extension in a colony. Can only be called by a Colony. diff --git a/helpers/test-data-generator.js b/helpers/test-data-generator.js index 94c38472f9..dff585d955 100644 --- a/helpers/test-data-generator.js +++ b/helpers/test-data-generator.js @@ -20,7 +20,7 @@ import { DELIVERABLE_HASH, } from "./constants"; -import { getTokenArgs, web3GetAccounts, getChildSkillIndex } from "./test-helper"; +import { getTokenArgs, web3GetAccounts, getChildSkillIndex, web3SignTypedData } from "./test-helper"; import { executeSignedTaskChange, executeSignedRoleAssignment } from "./task-review-signing"; const IColony = artifacts.require("IColony"); @@ -28,8 +28,11 @@ const IMetaColony = artifacts.require("IMetaColony"); const ITokenLocking = artifacts.require("ITokenLocking"); const Token = artifacts.require("Token"); const TokenAuthority = artifacts.require("./TokenAuthority"); +const BasicMetaTransaction = artifacts.require("BasicMetaTransaction"); +const MultiChain = artifacts.require("MultiChain"); const EtherRouter = artifacts.require("EtherRouter"); const Resolver = artifacts.require("Resolver"); +const MetaTxToken = artifacts.require("MetaTxToken"); const IColonyNetwork = artifacts.require("IColonyNetwork"); export async function makeTask({ colonyNetwork, colony, hash = SPECIFICATION_HASH, domainId = 1, skillId = 3, dueDate = 0, manager }) { @@ -393,3 +396,102 @@ export async function setupColony(colonyNetwork, tokenAddress) { const colony = await IColony.at(colonyAddress); return colony; } + +export async function getMetaTransactionParameters(txData, userAddress, targetAddress) { + const contract = await BasicMetaTransaction.at(targetAddress); + const nonce = await contract.getMetatransactionNonce(userAddress); + // We should just be able to get the chain id via a web3 call, but until ganache sort their stuff out, + // we dance around the houses. + const multichain = await MultiChain.new(); + const chainId = await multichain.getChainId(); + + // Sign data + const msg = web3.utils.soliditySha3( + { t: "uint256", v: nonce.toString() }, + { t: "address", v: targetAddress }, + { t: "uint256", v: chainId }, + { t: "bytes", v: txData } + ); + const sig = await web3.eth.sign(msg, userAddress); + + const r = `0x${sig.substring(2, 66)}`; + const s = `0x${sig.substring(66, 130)}`; + const v = parseInt(sig.substring(130), 16) + 27; + + return { r, s, v }; +} + +export async function getPermitParameters(owner, spender, amount, deadline, targetAddress) { + const contract = await MetaTxToken.at(targetAddress); + const nonce = await contract.getMetatransactionNonce(owner); + const multichain = await MultiChain.new(); + const chainId = await multichain.getChainId(); + const name = await contract.name(); + + const sigObject = { + types: { + EIP712Domain: [ + { + name: "name", + type: "string", + }, + { + name: "version", + type: "string", + }, + { + name: "chainId", + type: "uint256", + }, + { + name: "verifyingContract", + type: "address", + }, + ], + Permit: [ + { + name: "owner", + type: "address", + }, + { + name: "spender", + type: "address", + }, + { + name: "value", + type: "uint256", + }, + { + name: "nonce", + type: "uint256", + }, + { + name: "deadline", + type: "uint256", + }, + ], + }, + primaryType: "Permit", + domain: { + name, + version: "1", + chainId: chainId.toNumber(), + verifyingContract: contract.address, + }, + message: { + owner, + spender, + value: amount, + nonce, + deadline, + }, + }; + + const sig = await web3SignTypedData(owner, sigObject); + + const r = `0x${sig.substring(2, 66)}`; + const s = `0x${sig.substring(66, 130)}`; + const v = parseInt(sig.substring(130), 16); + + return { r, s, v }; +} diff --git a/helpers/test-helper.js b/helpers/test-helper.js index 69b4c86dae..2aa6a5048f 100644 --- a/helpers/test-helper.js +++ b/helpers/test-helper.js @@ -118,6 +118,22 @@ export function web3GetChainId() { }); } +export function web3SignTypedData(address, typedData) { + const packet = { + jsonrpc: "2.0", + method: "eth_signTypedData", + params: [address, typedData], + id: new Date().getTime(), + }; + + return new Promise((resolve, reject) => { + web3.currentProvider.send(packet, (err, res) => { + if (err !== null) return reject(err); + return resolve(res.result); + }); + }); +} + export function web3GetRawCall(params) { const packet = { jsonrpc: "2.0", diff --git a/helpers/upgradable-contracts.js b/helpers/upgradable-contracts.js index ddf0b92c02..9e2117327b 100644 --- a/helpers/upgradable-contracts.js +++ b/helpers/upgradable-contracts.js @@ -78,6 +78,7 @@ export async function setupColonyVersionResolver( colonyFunding, colonyRoles, contractRecovery, + colonyArbitraryTransaction, resolver ) { const deployedImplementations = {}; @@ -88,6 +89,7 @@ export async function setupColonyVersionResolver( deployedImplementations.ColonyPayment = colonyPayment.address; deployedImplementations.ColonyFunding = colonyFunding.address; deployedImplementations.ContractRecovery = contractRecovery.address; + deployedImplementations.ColonyArbitraryTransaction = colonyArbitraryTransaction.address; await setupEtherRouter("IMetaColony", deployedImplementations, resolver); } diff --git a/migrations/4_setup_colony_version_resolver.js b/migrations/4_setup_colony_version_resolver.js index b45ce3881f..00724cab7c 100644 --- a/migrations/4_setup_colony_version_resolver.js +++ b/migrations/4_setup_colony_version_resolver.js @@ -9,6 +9,7 @@ const ColonyRoles = artifacts.require("./ColonyRoles"); const ColonyTask = artifacts.require("./ColonyTask"); const ColonyPayment = artifacts.require("./ColonyPayment"); const ContractRecovery = artifacts.require("./ContractRecovery"); +const ColonyArbitraryTransaction = artifacts.require("./ColonyArbitraryTransaction"); const EtherRouter = artifacts.require("./EtherRouter"); const Resolver = artifacts.require("./Resolver"); const IColonyNetwork = artifacts.require("./IColonyNetwork"); @@ -22,6 +23,7 @@ module.exports = async function (deployer) { const colonyRoles = await ColonyRoles.new(); const colonyTask = await ColonyTask.new(); const colonyPayment = await ColonyPayment.new(); + const colonyArbitraryTransaction = await ColonyArbitraryTransaction.new(); const contractRecovery = await ContractRecovery.deployed(); const version = await colony.version(); const resolver = await Resolver.new(); @@ -30,7 +32,17 @@ module.exports = async function (deployer) { const colonyNetwork = await IColonyNetwork.at(etherRouterDeployed.address); // Register the new Colony contract version with the newly setup Resolver - await setupColonyVersionResolver(colony, colonyExpenditure, colonyTask, colonyPayment, colonyFunding, colonyRoles, contractRecovery, resolver); + await setupColonyVersionResolver( + colony, + colonyExpenditure, + colonyTask, + colonyPayment, + colonyFunding, + colonyRoles, + contractRecovery, + colonyArbitraryTransaction, + resolver + ); await colonyNetwork.initialise(resolver.address, version); console.log("### Colony version", version.toString(), "set to Resolver", resolver.address); diff --git a/migrations/8_setup_meta_colony.js b/migrations/8_setup_meta_colony.js index a06c627cf4..cc72c535e0 100644 --- a/migrations/8_setup_meta_colony.js +++ b/migrations/8_setup_meta_colony.js @@ -62,6 +62,7 @@ module.exports = async function (deployer, network, accounts) { const ColonyTask = artifacts.require("./ColonyTask"); const ColonyPayment = artifacts.require("./ColonyPayment"); const ContractRecovery = artifacts.require("./ContractRecovery"); + const ColonyArbitraryTransaction = artifacts.require("./ColonyArbitraryTransaction"); const colony = await Colony.new(); const colonyFunding = await ColonyFunding.new(); @@ -70,15 +71,36 @@ module.exports = async function (deployer, network, accounts) { const colonyTask = await ColonyTask.new(); const colonyPayment = await ColonyPayment.new(); const contractRecovery = await ContractRecovery.deployed(); + const colonyArbitraryTransaction = await ColonyArbitraryTransaction.new(); const resolver3 = await Resolver.new(); - await setupColonyVersionResolver(colony, colonyExpenditure, colonyTask, colonyPayment, colonyFunding, colonyRoles, contractRecovery, resolver3); + await setupColonyVersionResolver( + colony, + colonyExpenditure, + colonyTask, + colonyPayment, + colonyFunding, + colonyRoles, + contractRecovery, + colonyArbitraryTransaction, + resolver3 + ); const v3responder = await Version3.new(); await resolver3.register("version()", v3responder.address); await metaColony.addNetworkColonyVersion(3, resolver3.address); const resolver4 = await Resolver.new(); - await setupColonyVersionResolver(colony, colonyExpenditure, colonyTask, colonyPayment, colonyFunding, colonyRoles, contractRecovery, resolver4); + await setupColonyVersionResolver( + colony, + colonyExpenditure, + colonyTask, + colonyPayment, + colonyFunding, + colonyRoles, + contractRecovery, + colonyArbitraryTransaction, + resolver4 + ); const v4responder = await Version4.new(); await resolver4.register("version()", v4responder.address); await metaColony.addNetworkColonyVersion(4, resolver4.address); diff --git a/scripts/check-auth.js b/scripts/check-auth.js index 00e8c7b7ca..1da9c7b4b1 100644 --- a/scripts/check-auth.js +++ b/scripts/check-auth.js @@ -84,6 +84,7 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/common/EtherRouter.sol", "contracts/common/IRecovery.sol", "contracts/common/Resolver.sol", + "contracts/common/TokenAuthority.sol", // Imported from colonyToken repo "contracts/ens/ENS.sol", "contracts/ens/ENSRegistry.sol", "contracts/gnosis/MultiSigWallet.sol", @@ -106,6 +107,9 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/Migrations.sol", "contracts/Token.sol", // Imported from colonyToken repo "contracts/TokenAuthority.sol", // Imported from colonyToken repo + "contracts/metaTxToken/MetaTxToken.sol", + "contracts/metaTxToken/DSAuthMeta.sol", + "contracts/metaTxToken/DSTokenBaseMeta.sol", ].indexOf(contractName) > -1 ) { return; diff --git a/scripts/check-recovery.js b/scripts/check-recovery.js index b9131c20a3..87f8e9547a 100644 --- a/scripts/check-recovery.js +++ b/scripts/check-recovery.js @@ -30,6 +30,8 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/colonyNetwork/ColonyNetworkAuthority.sol", "contracts/colonyNetwork/ColonyNetworkStorage.sol", "contracts/colonyNetwork/IColonyNetwork.sol", + "contracts/common/BasicMetaTransaction.sol", + "contracts/common/IBasicMetaTransaction.sol", "contracts/common/CommonAuthority.sol", "contracts/common/DomainRoles.sol", "contracts/common/ERC20Extended.sol", @@ -37,6 +39,7 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/common/IEtherRouter.sol", "contracts/common/IRecovery.sol", "contracts/common/Resolver.sol", + "contracts/common/TokenAuthority.sol", // Imported from colonyToken repo "contracts/ens/ENS.sol", "contracts/ens/ENSRegistry.sol", "contracts/extensions/CoinMachine.sol", @@ -75,6 +78,9 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/Migrations.sol", "contracts/Token.sol", // Imported from colonyToken repo "contracts/TokenAuthority.sol", // Imported from colonyToken repo + "contracts/metaTxToken/MetaTxToken.sol", + "contracts/metaTxToken/DSAuthMeta.sol", + "contracts/metaTxToken/DSTokenBaseMeta.sol", ].indexOf(contractName) > -1 ) { return; diff --git a/scripts/check-storage.js b/scripts/check-storage.js index ec3f1658ee..a7abe077c0 100644 --- a/scripts/check-storage.js +++ b/scripts/check-storage.js @@ -23,6 +23,7 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/common/DomainRoles.sol", "contracts/common/EtherRouter.sol", "contracts/common/Resolver.sol", + "contracts/common/TokenAuthority.sol", // Imported from colonyToken repo "contracts/ens/ENSRegistry.sol", // Not directly used by any colony contracts "contracts/extensions/CoinMachine.sol", "contracts/extensions/EvaluatedExpenditure.sol", @@ -36,10 +37,14 @@ walkSync("./contracts/").forEach((contractName) => { "contracts/patriciaTree/PatriciaTreeBase.sol", // Only used by mining clients "contracts/reputationMiningCycle/ReputationMiningCycleStorage.sol", "contracts/testHelpers/ToggleableToken.sol", + "contracts/testHelpers/TestExtensions.sol", "contracts/tokenLocking/TokenLockingStorage.sol", "contracts/Migrations.sol", "contracts/Token.sol", // Imported from colonyToken repo "contracts/TokenAuthority.sol", // Imported from colonyToken repo + "contracts/metaTxToken/MetaTxToken.sol", + "contracts/metaTxToken/DSAuthMeta.sol", + "contracts/metaTxToken/DSTokenBaseMeta.sol", ].indexOf(contractName) > -1 ) { return; diff --git a/scripts/generate-test-contracts.sh b/scripts/generate-test-contracts.sh index e0e8e06777..a47a74352c 100644 --- a/scripts/generate-test-contracts.sh +++ b/scripts/generate-test-contracts.sh @@ -22,7 +22,7 @@ sed -i.bak "s/address resolver;/address resolver;function isUpdated() public pur sed -i.bak "s/contract Colony/contract UpdatedColony/g" ./contracts/colony/UpdatedColony.sol sed -i.bak "s/ColonyStorage/UpdatedColonyStorage/g" ./contracts/colony/UpdatedColony.sol sed -i.bak "s/function version() public pure returns (uint256 colonyVersion) { return ${version}/function version() public pure returns (uint256 colonyVersion) { return ${updated_version}/g" ./contracts/colony/UpdatedColony.sol -sed -i.bak "s/contract UpdatedColony is UpdatedColonyStorage, PatriciaTreeProofs, MultiChain {/contract UpdatedColony is UpdatedColonyStorage, PatriciaTreeProofs, MultiChain {function isUpdated() external pure returns(bool) {return true;}/g" ./contracts/colony/UpdatedColony.sol +sed -i.bak "s/contract UpdatedColony is BasicMetaTransaction, UpdatedColonyStorage, PatriciaTreeProofs {/contract UpdatedColony is BasicMetaTransaction, UpdatedColonyStorage, PatriciaTreeProofs {function isUpdated() external pure returns(bool) {return true;}/g" ./contracts/colony/UpdatedColony.sol # Modify UpdatedColonyDataTypes contract sed -i.bak "s/ColonyDataTypes/UpdatedColonyDataTypes/g" ./contracts/colony/UpdatedColonyDataTypes.sol sed -i.bak "s/mapping (uint8 => mapping (address => uint256)) payouts;/mapping (uint8 => mapping (address => uint256)) payouts; uint256 x;/g" ./contracts/colony/UpdatedColonyDataTypes.sol @@ -32,7 +32,7 @@ sed -i.bak "s/ColonyDataTypes/UpdatedColonyDataTypes/g" ./contracts/colony/Updat # Modify IUpdatedColony contract sed -i.bak "s/interface IColony/interface IUpdatedColony/g" ./contracts/colony/IUpdatedColony.sol sed -i.bak "s/ColonyDataTypes/UpdatedColonyDataTypes/g" ./contracts/colony/IUpdatedColony.sol -sed -i.bak "s/interface IUpdatedColony is UpdatedColonyDataTypes, IRecovery {/interface IUpdatedColony is UpdatedColonyDataTypes, IRecovery {function isUpdated() external pure returns(bool);/g" ./contracts/colony/IUpdatedColony.sol +sed -i.bak "s/interface IUpdatedColony is UpdatedColonyDataTypes, IRecovery, IBasicMetaTransaction {/interface IUpdatedColony is UpdatedColonyDataTypes, IRecovery, IBasicMetaTransaction {function isUpdated() external pure returns(bool);/g" ./contracts/colony/IUpdatedColony.sol # Modify UpdatedReputationMiningCycle contract sed -i.bak "s/contract ReputationMiningCycle/contract UpdatedReputationMiningCycle/g" ./contracts/reputationMiningCycle/UpdatedReputationMiningCycle.sol sed -i.bak "s| is ReputationMiningCycleCommon {| is ReputationMiningCycleCommon {\nfunction isUpdated() public pure returns(bool) {return true;}|g" ./contracts/reputationMiningCycle/UpdatedReputationMiningCycle.sol diff --git a/test-smoke/colony-storage-consistent.js b/test-smoke/colony-storage-consistent.js index c49a6b1239..8a83d7a29e 100644 --- a/test-smoke/colony-storage-consistent.js +++ b/test-smoke/colony-storage-consistent.js @@ -154,11 +154,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("fc9c8702501fa11dce57db3f679b0909a2b252cf71a73aac17ef4a159271ff56"); - expect(colonyAccount.stateRoot.toString("hex")).to.equal("d8e895aa214956a2543325209231d4502c6c241027df357ce9bcf065215c4632"); - expect(metaColonyAccount.stateRoot.toString("hex")).to.equal("78294685a492256887e3159e26071ba06d62883c72ccb26fd323a883eefc30fd"); - expect(miningCycleAccount.stateRoot.toString("hex")).to.equal("e105190bcd647989da1579ac209ae54ed63e08224fbb2469bad9f596773fe558"); - expect(tokenLockingAccount.stateRoot.toString("hex")).to.equal("8bab6ab2024de44a08765ee14ec3d6bdc0fa5ae2a5ee221c9f928345a3710658"); + expect(colonyNetworkAccount.stateRoot.toString("hex")).to.equal("cbe7c27231f4c94f1fdff92a685599c6ada69af16a0f32ab3a72e85334a4a2ca"); + expect(colonyAccount.stateRoot.toString("hex")).to.equal("b198bd282eac14f7dc4d71817c4184462c6db7ae11a8f7662edecd4afbba6234"); + expect(metaColonyAccount.stateRoot.toString("hex")).to.equal("c91229b9b01734f45e65feea0561ed90bee1365c953ceb187e87e80e9a96ef86"); + expect(miningCycleAccount.stateRoot.toString("hex")).to.equal("f1ae4c855f083837446bc31b3355ca0291daa363dd9afb655de51829a46fc635"); + expect(tokenLockingAccount.stateRoot.toString("hex")).to.equal("860aa632a7a9a21119b0e27c30d0c3f4da5916eb5263c7870d4dda3eb7162e32"); }); }); }); diff --git a/test-upgrade/colony-upgrade.js b/test-upgrade/colony-upgrade.js index 96ca4b4962..eee98a3d4c 100644 --- a/test-upgrade/colony-upgrade.js +++ b/test-upgrade/colony-upgrade.js @@ -14,6 +14,7 @@ const ColonyPayment = artifacts.require("ColonyPayment"); const ColonyFunding = artifacts.require("ColonyFunding"); const ColonyRoles = artifacts.require("ColonyRoles"); const ContractRecovery = artifacts.require("ContractRecovery"); +const ColonyArbitraryTransaction = artifacts.require("ColonyArbitraryTransaction"); const UpdatedColony = artifacts.require("UpdatedColony"); const IUpdatedColony = artifacts.require("IUpdatedColony"); @@ -44,6 +45,7 @@ contract("Colony contract upgrade", (accounts) => { const colonyFunding = await ColonyFunding.new(); const colonyRoles = await ColonyRoles.new(); const contractRecovery = await ContractRecovery.new(); + const colonyArbitraryTransaction = await ColonyArbitraryTransaction.new(); dueDate = await currentBlockTime(); await makeTask({ colony, dueDate }); @@ -61,6 +63,7 @@ contract("Colony contract upgrade", (accounts) => { colonyFunding, colonyRoles, contractRecovery, + colonyArbitraryTransaction, resolver ); diff --git a/test/contracts-network/colony-network-auction.js b/test/contracts-network/colony-network-auction.js index 588574b936..fc0d8c1321 100644 --- a/test/contracts-network/colony-network-auction.js +++ b/test/contracts-network/colony-network-auction.js @@ -13,8 +13,15 @@ import { getBlockTime, getColonyEditable, } from "../../helpers/test-helper"; + import { WAD, SECONDS_PER_DAY } from "../../helpers/constants"; -import { setupColonyNetwork, setupMetaColonyWithLockedCLNYToken, unlockCLNYToken, giveUserCLNYTokens } from "../../helpers/test-data-generator"; +import { + getMetaTransactionParameters, + setupColonyNetwork, + setupMetaColonyWithLockedCLNYToken, + unlockCLNYToken, + giveUserCLNYTokens, +} from "../../helpers/test-data-generator"; const { expect } = chai; chai.use(bnChai(web3.utils.BN)); @@ -261,6 +268,22 @@ contract("Colony Network Auction", (accounts) => { expect(bidCount).to.eq.BN(1); }); + it("can bid via metatransaction", async () => { + await giveUserCLNYTokens(colonyNetwork, BIDDER_1, WAD); + await clnyToken.approve(tokenAuction.address, WAD, { from: BIDDER_1 }); + // await tokenAuction.bid(WAD, { from: BIDDER_1 }); + const txData = await tokenAuction.contract.methods.bid(WAD.toString()).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, BIDDER_1, tokenAuction.address); + + await tokenAuction.executeMetaTransaction(BIDDER_1, txData, r, s, v, { from: accounts[0] }); + + const bid = await tokenAuction.bids(BIDDER_1); + expect(bid).to.eq.BN(WAD); + const bidCount = await tokenAuction.bidCount(); + expect(bidCount).to.eq.BN(1); + }); + it("bid tokens are locked", async () => { await giveUserCLNYTokens(colonyNetwork, BIDDER_1, WAD); await clnyToken.approve(tokenAuction.address, WAD, { from: BIDDER_1 }); diff --git a/test/contracts-network/colony-network-recovery.js b/test/contracts-network/colony-network-recovery.js index 6c95e1cd57..30d5e0107e 100644 --- a/test/contracts-network/colony-network-recovery.js +++ b/test/contracts-network/colony-network-recovery.js @@ -17,11 +17,18 @@ import { web3GetStorageAt, getActiveRepCycle, advanceMiningCycleNoContest, + getTokenArgs, } from "../../helpers/test-helper"; -import { setupFinalizedTask, giveUserCLNYTokensAndStake, fundColonyWithTokens, setupRandomColony } from "../../helpers/test-data-generator"; +import { + setupFinalizedTask, + giveUserCLNYTokensAndStake, + fundColonyWithTokens, + setupRandomColony, + getMetaTransactionParameters, +} from "../../helpers/test-data-generator"; import ReputationMinerTestWrapper from "../../packages/reputation-miner/test/ReputationMinerTestWrapper"; import { setupEtherRouter } from "../../helpers/upgradable-contracts"; -import { DEFAULT_STAKE, MINING_CYCLE_DURATION } from "../../helpers/constants"; +import { DEFAULT_STAKE, MINING_CYCLE_DURATION, CURR_VERSION } from "../../helpers/constants"; const { expect } = chai; chai.use(bnChai(web3.utils.BN)); @@ -195,6 +202,45 @@ contract("Colony Network Recovery", (accounts) => { await colonyNetwork.exitRecoveryMode(); }); + it("should not allow editing of a protected variable in a mapping", async () => { + // First, set a variable in a protected mapping. Currently, the only way we do that + // is via a metatransaction + const tokenArgs = getTokenArgs(); + const token = await Token.new(...tokenArgs); + + let txData = await colonyNetwork.contract.methods["createColony(address,uint256,string)"](token.address, CURR_VERSION, "").encodeABI(); + + txData = await colonyNetwork.contract.methods.createColony(token.address, CURR_VERSION, "someColonyName").encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, accounts[1], colonyNetwork.address); + + await colonyNetwork.executeMetaTransaction(accounts[1], txData, r, s, v, { from: accounts[0] }); + + // Put network in to recovery mode + await colonyNetwork.enterRecoveryMode(); + // work out the storage slot + // Metatransaction nonce mapping is storage slot 41 + // So this user has their nonce stored at + const user0MetatransactionNonceSlot = await web3.utils.soliditySha3( + { type: "bytes32", value: ethers.utils.hexZeroPad(accounts[1], 32) }, + { type: "uint256", value: "41" } + ); + + // Try and edit that slot + await checkErrorRevert( + colonyNetwork.setStorageSlotRecovery(user0MetatransactionNonceSlot, "0x00000000000000000000000000000000000000000000000000000000000000ff"), + "colony-protected-variable" + ); + + // Try and edit the protection + const user0MetatransactionNonceProtectionSlot = web3.utils.soliditySha3("RECOVERY_PROTECTED", user0MetatransactionNonceSlot); + await checkErrorRevert(colonyNetwork.setStorageSlotRecovery(user0MetatransactionNonceProtectionSlot, "0x00"), "colony-protected-variable"); + + // Leave recovery mode + await colonyNetwork.approveExitRecovery(); + await colonyNetwork.exitRecoveryMode(); + }); + it("should not be able to call recovery functions while not in recovery mode", async () => { await checkErrorRevert(colonyNetwork.approveExitRecovery(), "colony-not-in-recovery-mode"); await checkErrorRevert(colonyNetwork.exitRecoveryMode(), "colony-not-in-recovery-mode"); diff --git a/test/contracts-network/colony-network.js b/test/contracts-network/colony-network.js index de30f4e0c2..9c2798d4af 100755 --- a/test/contracts-network/colony-network.js +++ b/test/contracts-network/colony-network.js @@ -2,6 +2,7 @@ import chai from "chai"; import bnChai from "bn-chai"; import { ethers } from "ethers"; +import { soliditySha3 } from "web3-utils"; import { getTokenArgs, @@ -13,7 +14,12 @@ import { getColonyEditable, } from "../../helpers/test-helper"; import { CURR_VERSION, GLOBAL_SKILL_ID, MIN_STAKE, IPFS_HASH } from "../../helpers/constants"; -import { setupColonyNetwork, setupMetaColonyWithLockedCLNYToken, setupRandomColony } from "../../helpers/test-data-generator"; +import { + setupColonyNetwork, + setupMetaColonyWithLockedCLNYToken, + setupRandomColony, + getMetaTransactionParameters, +} from "../../helpers/test-data-generator"; import { setupENSRegistrar } from "../../helpers/upgradable-contracts"; const namehash = require("eth-ens-namehash"); @@ -25,7 +31,10 @@ const ENSRegistry = artifacts.require("ENSRegistry"); const EtherRouter = artifacts.require("EtherRouter"); const Resolver = artifacts.require("Resolver"); const IColonyNetwork = artifacts.require("IColonyNetwork"); +const IColony = artifacts.require("IColony"); const Token = artifacts.require("Token"); +const TokenAuthority = artifacts.require("TokenAuthority"); +const MetaTxToken = artifacts.require("MetaTxToken"); const FunctionsNotAvailableOnColony = artifacts.require("FunctionsNotAvailableOnColony"); contract("Colony Network", (accounts) => { @@ -335,6 +344,38 @@ contract("Colony Network", (accounts) => { }); }); + describe("when users create tokens", () => { + it("should allow users to create new tokens", async () => { + const tx = await colonyNetwork.deployTokenViaNetwork("TEST", "TST", 18); + await expectEvent(tx, "TokenDeployed", []); + }); + + it("should have the user as the owner of the token", async () => { + const tx = await colonyNetwork.deployTokenViaNetwork("TEST", "TST", 18); + const address = tx.logs[0].args.tokenAddress; + const token = await MetaTxToken.at(address); + const owner = await token.owner(); + expect(owner).to.equal(accounts[0]); + }); + + it("should allow users to create new token authorities", async () => { + let tx = await colonyNetwork.deployTokenViaNetwork("TEST", "TST", 18); + const { tokenAddress } = tx.logs[0].args; + tx = await colonyNetwork.deployTokenAuthority(tokenAddress, metaColony.address, [accounts[0]]); + await expectEvent(tx, "TokenAuthorityDeployed", []); + const authorityAddress = tx.logs[0].args.tokenAuthorityAddress; + const authority = await TokenAuthority.at(authorityAddress); + + const transferSig = soliditySha3("transfer(address,uint256)").slice(0, 10); + let ableToTransfer = await authority.canCall(metaColony.address, tokenAddress, transferSig); + expect(ableToTransfer).to.be.true; + ableToTransfer = await authority.canCall(accounts[0], tokenAddress, transferSig); + expect(ableToTransfer).to.be.true; + ableToTransfer = await authority.canCall(accounts[1], tokenAddress, transferSig); + expect(ableToTransfer).to.be.false; + }); + }); + describe("when getting existing colonies", () => { it("should allow users to get the address of a colony by its index", async () => { const token = await Token.new(...TOKEN_ARGS); @@ -640,4 +681,62 @@ contract("Colony Network", (accounts) => { await checkErrorRevert(colony.updateColonyOrbitDB("anotherstring", { from: accounts[0] }), "colony-colony-not-labeled"); }); }); + + describe("when executing metatransactions", () => { + beforeEach(async () => { + const ensRegistry = await ENSRegistry.new(); + await setupENSRegistrar(colonyNetwork, ensRegistry, accounts[0]); + }); + + it("should allow colony creation via metatransactions, with ENS registration afterwards", async () => { + const tokenArgs = getTokenArgs(); + const token = await Token.new(...tokenArgs); + + let txData = await colonyNetwork.contract.methods.createColony(token.address, CURR_VERSION, "").encodeABI(); + + let { r, s, v } = await getMetaTransactionParameters(txData, accounts[1], colonyNetwork.address); + + let tx = await colonyNetwork.executeMetaTransaction(accounts[1], txData, r, s, v, { from: accounts[0] }); + + const colonyCount = await colonyNetwork.getColonyCount(); + const colonyAddress = await colonyNetwork.getColony(colonyCount); + await expectEvent(tx, "ColonyAdded", [colonyCount, colonyAddress, token.address]); + + const colony = await IColony.at(colonyAddress); + txData = await colony.contract.methods.registerColonyLabel("someColonyName", "").encodeABI(); + + ({ r, s, v } = await getMetaTransactionParameters(txData, accounts[1], colony.address)); + + tx = await colony.executeMetaTransaction(accounts[1], txData, r, s, v, { from: accounts[0] }); + }); + + it("should allow colony creation via metatransactions, with ENS registration at the time", async () => { + const tokenArgs = getTokenArgs(); + const token = await Token.new(...tokenArgs); + + let txData = await colonyNetwork.contract.methods["createColony(address,uint256,string)"](token.address, CURR_VERSION, "").encodeABI(); + + txData = await colonyNetwork.contract.methods.createColony(token.address, CURR_VERSION, "someColonyName").encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, accounts[1], colonyNetwork.address); + + const tx = await colonyNetwork.executeMetaTransaction(accounts[1], txData, r, s, v, { from: accounts[0] }); + + const colonyCount = await colonyNetwork.getColonyCount(); + const colonyAddress = await colonyNetwork.getColony(colonyCount); + await expectEvent(tx, "ColonyAdded", [colonyCount, colonyAddress, token.address]); + }); + + it("should have the user as the owner of a token deployed through ColonyNetwork via metatransaction", async () => { + const txData = await colonyNetwork.contract.methods["deployTokenViaNetwork(string,string,uint8)"]("Test token", "TST", 18).encodeABI(); + const { r, s, v } = await getMetaTransactionParameters(txData, accounts[1], colonyNetwork.address); + + const tx = await colonyNetwork.executeMetaTransaction(accounts[1], txData, r, s, v, { from: accounts[0] }); + + const address = tx.logs[0].args.tokenAddress; + const token = await MetaTxToken.at(address); + const owner = await token.owner(); + expect(owner).to.equal(accounts[1]); + }); + }); }); diff --git a/test/contracts-network/colony-recovery.js b/test/contracts-network/colony-recovery.js index a376ebe678..be8fe5e873 100644 --- a/test/contracts-network/colony-recovery.js +++ b/test/contracts-network/colony-recovery.js @@ -6,7 +6,7 @@ import { ethers } from "ethers"; import { UINT256_MAX, SPECIFICATION_HASH } from "../../helpers/constants"; import { web3GetStorageAt, checkErrorRevert, expectEvent } from "../../helpers/test-helper"; -import { setupRandomColony } from "../../helpers/test-data-generator"; +import { setupRandomColony, getMetaTransactionParameters } from "../../helpers/test-data-generator"; const EtherRouter = artifacts.require("EtherRouter"); const IColonyNetwork = artifacts.require("IColonyNetwork"); @@ -195,7 +195,7 @@ contract("Colony Recovery", (accounts) => { expect(unprotected).to.eq.BN(`0xdeadbeef${"0".repeat(56)}`); }); - it("should not allow editing of protected variables", async () => { + it("should not allow editing of protected variables in a protected slot", async () => { await colony.enterRecoveryMode(); await checkErrorRevert(colony.setStorageSlotRecovery(0, "0xdeadbeef"), "colony-common-protected-variable"); await checkErrorRevert(colony.setStorageSlotRecovery(1, "0xdeadbeef"), "colony-common-protected-variable"); @@ -204,6 +204,36 @@ contract("Colony Recovery", (accounts) => { await checkErrorRevert(colony.setStorageSlotRecovery(6, "0xdeadbeef"), "colony-protected-variable"); }); + it("should not allow editing of a protected variable in a mapping", async () => { + // First, set a variable in a protected mapping. Currently, the only way we do that + // is via a metatransaction + const txData = await colony.contract.methods.mintTokens(100).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, accounts[0], colony.address); + + await colony.executeMetaTransaction(accounts[0], txData, r, s, v, { from: accounts[1] }); + + // Put colony in to recovery mode + await colony.enterRecoveryMode(); + // work out the storage slot + // Metatransaction nonce mapping is storage slot 35 + // So this user has their nonce stored at + const user0MetatransactionNonceSlot = await web3.utils.soliditySha3( + { type: "bytes32", value: ethers.utils.hexZeroPad(accounts[0], 32) }, + { type: "uint256", value: "35" } + ); + + // Try and edit that slot + await checkErrorRevert( + colony.setStorageSlotRecovery(user0MetatransactionNonceSlot, "0x00000000000000000000000000000000000000000000000000000000000000ff"), + "colony-protected-variable" + ); + + // Try and edit the protection + const user0MetatransactionNonceProtectionSlot = web3.utils.soliditySha3("RECOVERY_PROTECTED", user0MetatransactionNonceSlot); + await checkErrorRevert(colony.setStorageSlotRecovery(user0MetatransactionNonceProtectionSlot, "0x00"), "colony-protected-variable"); + }); + it("should allow upgrade to be called on a colony in and out of recovery mode", async () => { // Note that we can't upgrade, because we don't have a new version. But this test is still valid, because we're getting the // 'version must be newer' error, not a `colony-not-in-recovery-mode` or `colony-in-recovery-mode` error. diff --git a/test/contracts-network/colony.js b/test/contracts-network/colony.js index 510723aa43..62eef25780 100755 --- a/test/contracts-network/colony.js +++ b/test/contracts-network/colony.js @@ -16,7 +16,7 @@ import { WAD, } from "../../helpers/constants"; import { getTokenArgs, web3GetBalance, checkErrorRevert, expectNoEvent, expectAllEvents, expectEvent } from "../../helpers/test-helper"; -import { makeTask, setupRandomColony } from "../../helpers/test-data-generator"; +import { makeTask, setupRandomColony, getMetaTransactionParameters } from "../../helpers/test-data-generator"; const { expect } = chai; chai.use(bnChai(web3.utils.BN)); @@ -327,6 +327,18 @@ contract("Colony", (accounts) => { }); }); + describe("when executing metatransactions", () => { + it("should allow a metatransaction to occur", async () => { + const txData = await colony.contract.methods.mintTokens(100).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER0, colony.address); + + const tx = await colony.executeMetaTransaction(USER0, txData, r, s, v, { from: USER1 }); + + await expectEvent(tx, "TokensMinted(address,address,uint256)", [USER0, colony.address, 100]); + }); + }); + describe("when burning tokens", async () => { beforeEach(async () => { await colony.mintTokens(WAD); diff --git a/test/contracts-network/metatx-token.js b/test/contracts-network/metatx-token.js new file mode 100644 index 0000000000..49b941f552 --- /dev/null +++ b/test/contracts-network/metatx-token.js @@ -0,0 +1,708 @@ +/* globals artifacts */ +import chai from "chai"; +import bnChai from "bn-chai"; +import { ethers } from "ethers"; +import { web3GetBalance, checkErrorRevert, expectEvent } from "../../helpers/test-helper"; +import { getMetaTransactionParameters, getPermitParameters, setupColony } from "../../helpers/test-data-generator"; + +const { expect } = chai; +chai.use(bnChai(web3.utils.BN)); + +const EtherRouter = artifacts.require("EtherRouter"); +const IColonyNetwork = artifacts.require("IColonyNetwork"); +const MetaTxToken = artifacts.require("MetaTxToken"); + +const ADDRESS_ZERO = ethers.constants.AddressZero; + +contract("MetaTxToken", (accounts) => { + const USER0 = accounts[0]; + const USER1 = accounts[1]; + const USER2 = accounts[2]; + + let colony; + let metaTxToken; + let colonyNetwork; + + before(async () => { + const etherRouter = await EtherRouter.deployed(); + colonyNetwork = await IColonyNetwork.at(etherRouter.address); + }); + + beforeEach(async () => { + metaTxToken = await MetaTxToken.new("Test", "TEST", 18); + colony = await setupColony(colonyNetwork, metaTxToken.address); + await colony.setRewardInverse(100); + }); + + describe("when using the contract directly", () => { + describe("when working with MetaTxToken, should behave like normal token", () => { + beforeEach("mint 1500000 tokens", async () => { + await metaTxToken.unlock({ from: USER0 }); + await metaTxToken.mint(USER0, 1500000, { from: USER0 }); + }); + + it("should be able to get total supply", async () => { + const total = await metaTxToken.totalSupply(); + expect(total).to.eq.BN(1500000); + }); + + it("should be able to get token balance", async () => { + const balance = await metaTxToken.balanceOf(USER0); + expect(balance).to.eq.BN(1500000); + }); + + it("should be able to get allowance for address", async () => { + await metaTxToken.approve(USER1, 200000, { from: USER0 }); + const allowance = await metaTxToken.allowance(USER0, USER1); + expect(allowance).to.eq.BN(200000); + }); + + it("should be able to transfer tokens from own address", async () => { + const success = await metaTxToken.transfer.call(USER1, 300000, { from: USER0 }); + expect(success).to.be.true; + await expectEvent(metaTxToken.transfer(USER1, 300000, { from: USER0 }), "Transfer", [USER0, USER1, 300000]); + const balanceAccount1 = await metaTxToken.balanceOf(USER0); + expect(balanceAccount1).to.eq.BN(1200000); + const balanceAccount2 = await metaTxToken.balanceOf(USER1); + expect(balanceAccount2).to.eq.BN(300000); + }); + + it("should NOT be able to transfer more tokens than they have", async () => { + await checkErrorRevert(metaTxToken.transfer(USER1, 1500001, { from: USER0 }), "ds-token-insufficient-balance"); + const balanceAccount2 = await metaTxToken.balanceOf(USER1); + expect(balanceAccount2).to.be.zero; + }); + + it("should be able to transfer pre-approved tokens from address different than own", async () => { + await metaTxToken.approve(USER1, 300000, { from: USER0 }); + const success = await metaTxToken.transferFrom.call(USER0, USER1, 300000, { from: USER1 }); + expect(success).to.be.true; + + await expectEvent(metaTxToken.transferFrom(USER0, USER1, 300000, { from: USER1 }), "Transfer", [USER0, USER1, 300000]); + const balanceAccount1 = await metaTxToken.balanceOf(USER0); + expect(balanceAccount1).to.eq.BN(1200000); + const balanceAccount2 = await metaTxToken.balanceOf(USER1); + expect(balanceAccount2).to.eq.BN(300000); + const allowance = await metaTxToken.allowance(USER0, USER1); + expect(allowance).to.be.zero; + }); + + it("should NOT be able to transfer tokens from another address if NOT pre-approved", async () => { + await checkErrorRevert(metaTxToken.transferFrom(USER0, USER1, 300000, { from: USER1 }), "ds-token-insufficient-approval"); + const balanceAccount2 = await metaTxToken.balanceOf(USER1); + expect(balanceAccount2).to.be.zero; + }); + + it("should NOT be able to transfer from another address more tokens than pre-approved", async () => { + await metaTxToken.approve(USER1, 300000); + await checkErrorRevert(metaTxToken.transferFrom(USER0, USER1, 300001, { from: USER1 }), "ds-token-insufficient-approval"); + + const balanceAccount2 = await metaTxToken.balanceOf(USER1); + expect(balanceAccount2).to.be.zero; + }); + + it("should NOT be able to transfer from another address more tokens than the source balance", async () => { + await metaTxToken.approve(USER1, 300000, { from: USER0 }); + await metaTxToken.transfer(USER2, 1500000, { from: USER0 }); + + await checkErrorRevert(metaTxToken.transferFrom(USER0, USER1, 300000, { from: USER1 }), "ds-token-insufficient-balance"); + const balanceAccount2 = await metaTxToken.balanceOf(USER1); + expect(balanceAccount2).to.be.zero; + }); + + it("should be able to approve token transfer for other accounts", async () => { + const success = await metaTxToken.approve.call(USER1, 200000, { from: USER0 }); + expect(success).to.be.true; + + await expectEvent(metaTxToken.approve(USER1, 200000, { from: USER0 }), "Approval", [USER0, USER1, 200000]); + const allowance = await metaTxToken.allowance(USER0, USER1); + expect(allowance).to.eq.BN(200000); + }); + }); + + describe("when working with ERC20 functions and token is locked", () => { + beforeEach(async () => { + await metaTxToken.mint(USER0, 1500000, { from: USER0 }); + await metaTxToken.transfer(USER1, 1500000, { from: USER0 }); + }); + + it("shouldn't be able to transfer tokens from own address", async () => { + await checkErrorRevert(metaTxToken.transfer(USER2, 300000, { from: USER1 }), "colony-token-unauthorised"); + + const balanceAccount1 = await metaTxToken.balanceOf(USER1); + expect(balanceAccount1).to.eq.BN(1500000); + const balanceAccount2 = await metaTxToken.balanceOf(USER2); + expect(balanceAccount2).to.be.zero; + }); + + it("shouldn't be able to transfer pre-approved tokens", async () => { + await metaTxToken.approve(USER2, 300000, { from: USER1 }); + await checkErrorRevert(metaTxToken.transferFrom(USER1, USER2, 300000, { from: USER2 }), "colony-token-unauthorised"); + + const balanceAccount1 = await metaTxToken.balanceOf(USER1); + expect(balanceAccount1).to.eq.BN(1500000); + const balanceAccount2 = await metaTxToken.balanceOf(USER2); + expect(balanceAccount2).to.be.zero; + const allowance = await metaTxToken.allowance(USER1, USER2); + expect(allowance).to.eq.BN(300000); + }); + }); + + describe("when working with additional functions", () => { + it("should be able to get the token decimals", async () => { + const decimals = await metaTxToken.decimals(); + expect(decimals).to.eq.BN(18); + }); + + it("should be able to get the token symbol", async () => { + const symbol = await metaTxToken.symbol(); + expect(symbol).to.equal("TEST"); + }); + + it("should be able to get the token name", async () => { + const name = await metaTxToken.name(); + expect(name).to.equal("Test"); + }); + + it("should be able to mint new tokens, when called by the Token owner", async () => { + await metaTxToken.mint(USER0, 1500000, { from: USER0 }); + + let totalSupply = await metaTxToken.totalSupply(); + expect(totalSupply).to.eq.BN(1500000); + + let balance = await metaTxToken.balanceOf(USER0); + expect(balance).to.eq.BN(1500000); + + // Mint some more tokens + await expectEvent(metaTxToken.mint(USER0, 1, { from: USER0 }), "Mint", [USER0, 1]); + totalSupply = await metaTxToken.totalSupply(); + expect(totalSupply).to.eq.BN(1500001); + + balance = await metaTxToken.balanceOf(USER0); + expect(balance).to.eq.BN(1500001); + }); + + it("should be able to mint new tokens directly to sender, when called by the Token owner", async () => { + // How truffle supports function overloads apparently + await metaTxToken.methods["mint(uint256)"](1500000, { from: USER0 }); + + const totalSupply = await metaTxToken.totalSupply(); + expect(totalSupply).to.eq.BN(1500000); + + const balance = await metaTxToken.balanceOf(USER0); + expect(balance).to.eq.BN(1500000); + }); + + it("should emit a Mint event when minting tokens", async () => { + await expectEvent(metaTxToken.mint(USER0, 1, { from: USER0 }), "Mint", [USER0, 1]); + await expectEvent(metaTxToken.methods["mint(uint256)"](1, { from: USER0 }), "Mint", [USER0, 1]); + }); + + it("should emit a Transfer event when minting tokens", async () => { + await expectEvent(metaTxToken.mint(USER0, 1, { from: USER0 }), "Transfer", [ADDRESS_ZERO, USER0, 1]); + await expectEvent(metaTxToken.methods["mint(uint256)"](1, { from: USER0 }), "Transfer", [ADDRESS_ZERO, USER0, 1]); + }); + + it("should NOT be able to mint new tokens, when called by anyone NOT the Token owner", async () => { + await checkErrorRevert(metaTxToken.mint(USER0, 1500000, { from: USER2 }), "ds-auth-unauthorized"); + const totalSupply = await metaTxToken.totalSupply(); + expect(totalSupply).to.be.zero; + }); + + it("should be able to burn others' tokens when they have approved them to do so", async () => { + await metaTxToken.mint(USER1, 1500000, { from: USER0 }); + await metaTxToken.methods["approve(address,uint256)"](USER2, 500000, { from: USER1 }); + await metaTxToken.methods["burn(address,uint256)"](USER1, 500000, { from: USER2 }); + + const totalSupply = await metaTxToken.totalSupply(); + expect(totalSupply).to.eq.BN(1000000); + + const balance = await metaTxToken.balanceOf(USER1); + expect(balance).to.eq.BN(1000000); + }); + + it("should NOT be able to burn others' tokens when the approved amount is less", async () => { + await metaTxToken.mint(USER1, 1500000, { from: USER0 }); + await metaTxToken.methods["approve(address,uint256)"](USER2, 500000, { from: USER1 }); + await checkErrorRevert(metaTxToken.methods["burn(address,uint256)"](USER1, 500001, { from: USER2 }), "ds-token-insufficient-approval"); + + const totalSupply = await metaTxToken.totalSupply(); + expect(totalSupply).to.eq.BN(1500000); + + const balance = await metaTxToken.balanceOf(USER1); + expect(balance).to.eq.BN(1500000); + }); + + it("should be able to burn own tokens", async () => { + // How truffle supports function overloads apparently + await metaTxToken.mint(USER1, 1500000, { from: USER0 }); + await metaTxToken.methods["burn(address,uint256)"](USER1, 500000, { from: USER1 }); + + let totalSupply = await metaTxToken.totalSupply(); + expect(totalSupply).to.eq.BN(1000000); + + let balance = await metaTxToken.balanceOf(USER1); + expect(balance).to.eq.BN(1000000); + + await metaTxToken.methods["burn(uint256)"](1000000, { from: USER1 }); + totalSupply = await metaTxToken.totalSupply(); + expect(totalSupply).to.be.zero; + + balance = await metaTxToken.balanceOf(USER1); + expect(balance).to.be.zero; + }); + + it("should NOT be able to burn tokens if there's insufficient balance", async () => { + await metaTxToken.mint(USER1, 5, { from: USER0 }); + await checkErrorRevert(metaTxToken.burn(6, { from: USER1 }), "ds-token-insufficient-balance"); + + const balance = await metaTxToken.balanceOf(USER1); + expect(balance).to.eq.BN(5); + }); + + it("should emit a Burn event when burning tokens", async () => { + await metaTxToken.methods["mint(uint256)"](1, { from: USER0 }); + await expectEvent(metaTxToken.burn(1, { from: USER0 }), "Burn", [USER0, 1]); + }); + + it("should be able to unlock token by owner", async () => { + // Note: due to an apparent bug, we cannot call a parameterless function with transaction params, e.g. { from: senderAccount } + // So change the owner to coinbase so we are able to call it without params + await metaTxToken.setOwner(USER0, { from: USER0 }); + await metaTxToken.unlock(); + await metaTxToken.setAuthority(USER0); + + const locked = await metaTxToken.locked(); + expect(locked).to.be.false; + + const tokenAuthorityLocal = await metaTxToken.authority(); + expect(tokenAuthorityLocal).to.equal(USER0); + }); + + it("shouldn't be able to unlock token by non-owner", async () => { + await checkErrorRevert(metaTxToken.unlock({ from: USER2 }), "ds-auth-unauthorized"); + await checkErrorRevert(metaTxToken.setAuthority(USER0, { from: USER2 }), "ds-auth-unauthorized"); + + const locked = await metaTxToken.locked(); + expect(locked).to.be.true; + }); + }); + + describe("when working with ether transfers", () => { + it("should NOT accept eth", async () => { + await checkErrorRevert(metaTxToken.send(2)); + const tokenBalance = await web3GetBalance(metaTxToken.address); + expect(tokenBalance).to.be.zero; + }); + }); + }); + + describe("when using the contract through metatransactions", () => { + describe("when working with MetaTxToken, should behave like normal token", () => { + beforeEach("mint 1500000 tokens", async () => { + await metaTxToken.unlock({ from: USER0 }); + await metaTxToken.mint(USER0, 1500000, { from: USER0 }); + }); + + it("should be able to transfer tokens from own address", async () => { + const txData = await metaTxToken.contract.methods.transfer(USER1, 300000).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER0, metaTxToken.address); + + const tx = await metaTxToken.executeMetaTransaction(USER0, txData, r, s, v, { from: USER1 }); + + await expectEvent(tx, "Transfer", [USER0, USER1, 300000]); + const balanceAccount1 = await metaTxToken.balanceOf(USER0); + expect(balanceAccount1).to.eq.BN(1200000); + const balanceAccount2 = await metaTxToken.balanceOf(USER1); + expect(balanceAccount2).to.eq.BN(300000); + }); + + it("metatransaction should not be able to be replayed", async () => { + const txData = await metaTxToken.contract.methods.transfer(USER1, 300000).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER0, metaTxToken.address); + + await metaTxToken.executeMetaTransaction(USER0, txData, r, s, v, { from: USER1 }); + + await checkErrorRevert( + metaTxToken.executeMetaTransaction(USER0, txData, r, s, v, { from: USER1 }), + "metatransaction-signer-signature-mismatch" + ); + }); + + it("should NOT be able to transfer more tokens than they have", async () => { + const txData = await metaTxToken.contract.methods.transfer(USER1, 1500001).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER0, metaTxToken.address); + + await checkErrorRevert( + metaTxToken.executeMetaTransaction(USER0, txData, r, s, v, { from: USER1 }), + "colony-metatx-function-call-unsuccessful" + ); + + const balanceAccount2 = await metaTxToken.balanceOf(USER1); + expect(balanceAccount2).to.be.zero; + }); + + it("should be able to transfer pre-approved tokens from address different than own", async () => { + await metaTxToken.approve(USER1, 300000, { from: USER0 }); + const txData = await metaTxToken.contract.methods.transferFrom(USER0, USER1, 300000).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER1, metaTxToken.address); + + await expectEvent(metaTxToken.executeMetaTransaction(USER1, txData, r, s, v, { from: USER2 }), "Transfer", [USER0, USER1, 300000]); + const balanceAccount1 = await metaTxToken.balanceOf(USER0); + expect(balanceAccount1).to.eq.BN(1200000); + const balanceAccount2 = await metaTxToken.balanceOf(USER1); + expect(balanceAccount2).to.eq.BN(300000); + const allowance = await metaTxToken.allowance(USER0, USER1); + expect(allowance).to.be.zero; + }); + + it("should NOT be able to transfer tokens from another address if NOT pre-approved", async () => { + const txData = await metaTxToken.contract.methods.transferFrom(USER0, USER1, 300000).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER1, metaTxToken.address); + + await checkErrorRevert( + metaTxToken.executeMetaTransaction(USER1, txData, r, s, v, { from: USER2 }), + "colony-metatx-function-call-unsuccessful" + ); + const balanceAccount2 = await metaTxToken.balanceOf(USER1); + expect(balanceAccount2).to.be.zero; + }); + + it("should NOT be able to transfer from another address more tokens than pre-approved", async () => { + await metaTxToken.approve(USER1, 300000); + const txData = await metaTxToken.contract.methods.transferFrom(USER0, USER1, 300001).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER1, metaTxToken.address); + + await checkErrorRevert( + metaTxToken.executeMetaTransaction(USER1, txData, r, s, v, { from: USER2 }), + "colony-metatx-function-call-unsuccessful" + ); + + const balanceAccount2 = await metaTxToken.balanceOf(USER1); + expect(balanceAccount2).to.be.zero; + }); + + it("should NOT be able to transfer from another address more tokens than the source balance", async () => { + await metaTxToken.approve(USER1, 300000, { from: USER0 }); + await metaTxToken.transfer(USER2, 1500000, { from: USER0 }); + const txData = await metaTxToken.contract.methods.transferFrom(USER0, USER1, 300001).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER1, metaTxToken.address); + + await checkErrorRevert( + metaTxToken.executeMetaTransaction(USER1, txData, r, s, v, { from: USER2 }), + "colony-metatx-function-call-unsuccessful" + ); + const balanceAccount2 = await metaTxToken.balanceOf(USER1); + expect(balanceAccount2).to.be.zero; + }); + + it("should be able to approve token transfer for other accounts", async () => { + const txData = await metaTxToken.contract.methods.approve(USER1, 200000).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER0, metaTxToken.address); + + await expectEvent(metaTxToken.executeMetaTransaction(USER0, txData, r, s, v, { from: USER2 }), "Approval", [USER0, USER1, 200000]); + const allowance = await metaTxToken.allowance(USER0, USER1); + expect(allowance).to.eq.BN(200000); + }); + }); + + describe("when working with ERC20 functions and token is locked", () => { + beforeEach(async () => { + await metaTxToken.mint(USER0, 1500000, { from: USER0 }); + await metaTxToken.transfer(USER1, 1500000, { from: USER0 }); + }); + + it("shouldn't be able to transfer tokens from own address", async () => { + const txData = await metaTxToken.contract.methods.transfer(USER2, 300000).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER1, metaTxToken.address); + + await checkErrorRevert( + metaTxToken.executeMetaTransaction(USER1, txData, r, s, v, { from: USER2 }), + "colony-metatx-function-call-unsuccessful" + ); + + const balanceAccount1 = await metaTxToken.balanceOf(USER1); + expect(balanceAccount1).to.eq.BN(1500000); + const balanceAccount2 = await metaTxToken.balanceOf(USER2); + expect(balanceAccount2).to.be.zero; + }); + + it("shouldn't be able to transfer pre-approved tokens", async () => { + await metaTxToken.approve(USER2, 300000, { from: USER1 }); + + const txData = await metaTxToken.contract.methods.transferFrom(USER1, USER2, 300000).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER2, metaTxToken.address); + + await checkErrorRevert( + metaTxToken.executeMetaTransaction(USER2, txData, r, s, v, { from: USER0 }), + "colony-metatx-function-call-unsuccessful" + ); + + const balanceAccount1 = await metaTxToken.balanceOf(USER1); + expect(balanceAccount1).to.eq.BN(1500000); + const balanceAccount2 = await metaTxToken.balanceOf(USER2); + expect(balanceAccount2).to.be.zero; + const allowance = await metaTxToken.allowance(USER1, USER2); + expect(allowance).to.eq.BN(300000); + }); + }); + + describe("when working with additional functions", () => { + it("should be able to mint new tokens, when called by the Token owner", async () => { + let txData = await metaTxToken.contract.methods.mint(USER0, 1500000).encodeABI(); + + let { r, s, v } = await getMetaTransactionParameters(txData, USER0, metaTxToken.address); + + await metaTxToken.executeMetaTransaction(USER0, txData, r, s, v, { from: USER1 }); + + let totalSupply = await metaTxToken.totalSupply(); + expect(totalSupply).to.eq.BN(1500000); + + let balance = await metaTxToken.balanceOf(USER0); + expect(balance).to.eq.BN(1500000); + + // Mint some more tokens + txData = await metaTxToken.contract.methods.mint(USER0, 1).encodeABI(); + + ({ r, s, v } = await getMetaTransactionParameters(txData, USER0, metaTxToken.address)); + + const tx = metaTxToken.executeMetaTransaction(USER0, txData, r, s, v, { from: USER1 }); + + await expectEvent(tx, "Mint", [USER0, 1]); + totalSupply = await metaTxToken.totalSupply(); + expect(totalSupply).to.eq.BN(1500001); + + balance = await metaTxToken.balanceOf(USER0); + expect(balance).to.eq.BN(1500001); + }); + + it("should be able to mint new tokens directly to sender, when called by the Token owner", async () => { + const txData = await metaTxToken.contract.methods["mint(uint256)"](1500000).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER0, metaTxToken.address); + + await metaTxToken.executeMetaTransaction(USER0, txData, r, s, v, { from: USER1 }); + + const totalSupply = await metaTxToken.totalSupply(); + expect(totalSupply).to.eq.BN(1500000); + + const balance = await metaTxToken.balanceOf(USER0); + expect(balance).to.eq.BN(1500000); + }); + + it("should NOT be able to mint new tokens, when called by anyone NOT the Token owner", async () => { + const txData = await metaTxToken.contract.methods.mint(USER0).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER1, metaTxToken.address); + + await checkErrorRevert( + metaTxToken.executeMetaTransaction(USER1, txData, r, s, v, { from: USER0 }), + "colony-metatx-function-call-unsuccessful" + ); + + const totalSupply = await metaTxToken.totalSupply(); + expect(totalSupply).to.be.zero; + }); + + it("should be able to burn others' tokens when they have approved them to do so", async () => { + await metaTxToken.mint(USER1, 1500000, { from: USER0 }); + await metaTxToken.methods["approve(address,uint256)"](USER2, 500000, { from: USER1 }); + // await metaTxToken.approve(USER2, 500000, { FROM: USER1 }); + // await metaTxToken.burn(USER1, 500000, { from: USER2 }); + + const txData = await metaTxToken.contract.methods["burn(address,uint256)"](USER1, 500000).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER2, metaTxToken.address); + + await metaTxToken.executeMetaTransaction(USER2, txData, r, s, v, { from: USER0 }); + + const totalSupply = await metaTxToken.totalSupply(); + expect(totalSupply).to.eq.BN(1000000); + + const balance = await metaTxToken.balanceOf(USER1); + expect(balance).to.eq.BN(1000000); + }); + + it("should NOT be able to burn others' tokens when the approved amount is less", async () => { + await metaTxToken.mint(USER1, 1500000, { from: USER0 }); + await metaTxToken.methods["approve(address,uint256)"](USER2, 500000, { from: USER1 }); + + const txData = await metaTxToken.contract.methods["burn(address,uint256)"](USER2, 500001).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER2, metaTxToken.address); + + await checkErrorRevert( + metaTxToken.executeMetaTransaction(USER2, txData, r, s, v, { from: USER0 }), + "colony-metatx-function-call-unsuccessful" + ); + + const totalSupply = await metaTxToken.totalSupply(); + expect(totalSupply).to.eq.BN(1500000); + + const balance = await metaTxToken.balanceOf(USER1); + expect(balance).to.eq.BN(1500000); + }); + + it("should be able to burn own tokens", async () => { + // How truffle supports function overloads apparently + await metaTxToken.mint(USER1, 1500000, { from: USER0 }); + + const txData = await metaTxToken.contract.methods["burn(address,uint256)"](USER1, 500000).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER1, metaTxToken.address); + + await metaTxToken.executeMetaTransaction(USER1, txData, r, s, v, { from: USER0 }); + + let totalSupply = await metaTxToken.totalSupply(); + expect(totalSupply).to.eq.BN(1000000); + + let balance = await metaTxToken.balanceOf(USER1); + expect(balance).to.eq.BN(1000000); + + await metaTxToken.methods["burn(uint256)"](1000000, { from: USER1 }); + totalSupply = await metaTxToken.totalSupply(); + expect(totalSupply).to.be.zero; + + balance = await metaTxToken.balanceOf(USER1); + expect(balance).to.be.zero; + }); + + it("should NOT be able to burn tokens if there's insufficient balance", async () => { + await metaTxToken.mint(USER1, 5, { from: USER0 }); + + const txData = await metaTxToken.contract.methods.burn(6).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER1, metaTxToken.address); + + await checkErrorRevert( + metaTxToken.executeMetaTransaction(USER1, txData, r, s, v, { from: USER0 }), + "colony-metatx-function-call-unsuccessful" + ); + + const balance = await metaTxToken.balanceOf(USER1); + expect(balance).to.eq.BN(5); + }); + + it("should be able to unlock token by owner", async () => { + // Note: due to an apparent bug, we cannot call a parameterless function with transaction params, e.g. { from: senderAccount } + // So change the owner to coinbase so we are able to call it without params + await metaTxToken.setOwner(USER0, { from: USER0 }); + + let txData = await metaTxToken.contract.methods.unlock().encodeABI(); + + let { r, s, v } = await getMetaTransactionParameters(txData, USER0, metaTxToken.address); + + await metaTxToken.executeMetaTransaction(USER0, txData, r, s, v, { from: USER1 }); + + txData = await metaTxToken.contract.methods.setAuthority(USER0).encodeABI(); + ({ r, s, v } = await getMetaTransactionParameters(txData, USER0, metaTxToken.address)); + + await metaTxToken.executeMetaTransaction(USER0, txData, r, s, v, { from: USER1 }); + + const locked = await metaTxToken.locked(); + expect(locked).to.be.false; + + const tokenAuthorityLocal = await metaTxToken.authority(); + expect(tokenAuthorityLocal).to.equal(USER0); + }); + + it("shouldn't be able to unlock token by non-owner", async () => { + let txData = await metaTxToken.contract.methods.unlock().encodeABI(); + + let { r, s, v } = await getMetaTransactionParameters(txData, USER2, metaTxToken.address); + + await checkErrorRevert( + metaTxToken.executeMetaTransaction(USER2, txData, r, s, v, { from: USER0 }), + "colony-metatx-function-call-unsuccessful" + ); + + txData = await metaTxToken.contract.methods.setAuthority(USER0).encodeABI(); + + ({ r, s, v } = await getMetaTransactionParameters(txData, USER1, metaTxToken.address)); + + await checkErrorRevert( + metaTxToken.executeMetaTransaction(USER1, txData, r, s, v, { from: USER0 }), + "colony-metatx-function-call-unsuccessful" + ); + + const locked = await metaTxToken.locked(); + expect(locked).to.be.true; + const tokenAuthorityLocal = await metaTxToken.authority(); + expect(tokenAuthorityLocal).to.equal(ADDRESS_ZERO); + }); + }); + }); + + describe("when using the permit functionality", () => { + it("permit should work", async () => { + await metaTxToken.unlock(); + + let allowance = await metaTxToken.allowance(USER0, USER1); + expect(allowance).to.eq.BN(0); + + const { r, s, v } = await getPermitParameters(USER0, USER1, 100, 1000000000000, metaTxToken.address); + + const tx = await metaTxToken.permit(USER0, USER1, 100, 1000000000000, v, r, s, { from: USER2 }); + + await expectEvent(tx, "Approval", [USER0, USER1, 100]); + + allowance = await metaTxToken.allowance(USER0, USER1); + expect(allowance).to.eq.BN(100); + }); + + it("permit with deadline in the past doesn't work", async () => { + await metaTxToken.unlock(); + + const { r, s, v } = await getPermitParameters(USER0, USER1, 100, 1, metaTxToken.address); + + await checkErrorRevert(metaTxToken.permit(USER0, USER1, 100, 1, v, r, s, { from: USER2 }), "colony-token-expired-deadline"); + + const allowance = await metaTxToken.allowance(USER0, USER1); + expect(allowance).to.eq.BN(0); + }); + + it("permit does not allow a tx to be replayed", async () => { + await metaTxToken.unlock(); + + let allowance = await metaTxToken.allowance(USER0, USER1); + expect(allowance).to.eq.BN(0); + + const { r, s, v } = await getPermitParameters(USER0, USER1, 100, 1000000000000, metaTxToken.address); + + await metaTxToken.permit(USER0, USER1, 100, 1000000000000, v, r, s, { from: USER2 }); + + await metaTxToken.approve(USER1, 300000, { from: USER0 }); + + await checkErrorRevert(metaTxToken.permit(USER0, USER1, 100, 1000000000000, v, r, s, { from: USER2 }), "colony-token-invalid-signature"); + + allowance = await metaTxToken.allowance(USER0, USER1); + expect(allowance).to.eq.BN(300000); + }); + + it("permit expects a valid signature", async () => { + await metaTxToken.unlock(); + + let allowance = await metaTxToken.allowance(USER0, USER1); + expect(allowance).to.eq.BN(0); + + const { r, s } = await getPermitParameters(USER0, USER1, 100, 1000000000000, metaTxToken.address); + + const v = 100; + + await checkErrorRevert(metaTxToken.permit(USER0, USER1, 100, 1000000000000, v, r, s, { from: USER2 }), "colony-token-invalid-signature"); + + allowance = await metaTxToken.allowance(USER0, USER1); + expect(allowance).to.eq.BN(0); + }); + }); +}); diff --git a/test/contracts-network/token-locking.js b/test/contracts-network/token-locking.js index 85bd76415a..2ada308eb8 100644 --- a/test/contracts-network/token-locking.js +++ b/test/contracts-network/token-locking.js @@ -7,7 +7,13 @@ import { soliditySha3 } from "web3-utils"; import TruffleLoader from "../../packages/reputation-miner/TruffleLoader"; import { getTokenArgs, checkErrorRevert, makeReputationKey, advanceMiningCycleNoContest, expectEvent } from "../../helpers/test-helper"; -import { giveUserCLNYTokensAndStake, setupColony, setupRandomColony, fundColonyWithTokens } from "../../helpers/test-data-generator"; +import { + giveUserCLNYTokensAndStake, + setupColony, + setupRandomColony, + fundColonyWithTokens, + getMetaTransactionParameters, +} from "../../helpers/test-data-generator"; import { UINT256_MAX, DEFAULT_STAKE } from "../../helpers/constants"; import { setupEtherRouter } from "../../helpers/upgradable-contracts"; @@ -176,6 +182,21 @@ contract("Token Locking", (addresses) => { const balance = await token.balanceOf(otherUserAddress); expect(balance).to.eq.BN(usersTokens); }); + + it("should allow deposits to be made via metatransaction", async () => { + await token.approve(tokenLocking.address, usersTokens, { from: userAddress }); + + const txData = await tokenLocking.contract.methods["deposit(address,uint256,bool)"](token.address, usersTokens, true).encodeABI(); + const { r, s, v } = await getMetaTransactionParameters(txData, userAddress, tokenLocking.address); + + await tokenLocking.executeMetaTransaction(userAddress, txData, r, s, v, { from: otherUserAddress }); + + const info = await tokenLocking.getUserLock(token.address, userAddress); + expect(info.balance).to.eq.BN(usersTokens); + + const tokenLockingContractBalance = await token.balanceOf(tokenLocking.address); + expect(tokenLockingContractBalance).to.eq.BN(usersTokens); + }); }); describe("when withdrawing tokens", async () => { diff --git a/test/extensions/coin-machine.js b/test/extensions/coin-machine.js index 2b569dd177..3cd89d764c 100644 --- a/test/extensions/coin-machine.js +++ b/test/extensions/coin-machine.js @@ -26,6 +26,7 @@ import { setupRandomColony, setupColony, setupMetaColonyWithLockedCLNYToken, + getMetaTransactionParameters, } from "../../helpers/test-data-generator"; const { expect } = chai; @@ -234,6 +235,26 @@ contract("Coin Machine", (accounts) => { await checkErrorRevert(coinMachine.buyTokens(WAD, { from: USER0 }), "ds-token-insufficient-balance"); }); + it("can buy tokens via metatransaction", async () => { + await purchaseToken.mint(USER0, WAD, { from: USER0 }); + await purchaseToken.approve(coinMachine.address, WAD, { from: USER0 }); + + const txData = await coinMachine.contract.methods.buyTokens(WAD.toString()).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER0, coinMachine.address); + + await coinMachine.executeMetaTransaction(USER0, txData, r, s, v, { from: USER1 }); + + const userBalance = await token.balanceOf(USER0); + expect(userBalance).to.eq.BN(WAD); + const colonyBalance = await purchaseToken.balanceOf(colony.address); + expect(colonyBalance).to.eq.BN(WAD); + + // But not with insufficient funds + await purchaseToken.approve(coinMachine.address, WAD, { from: USER0 }); + await checkErrorRevert(coinMachine.buyTokens(WAD, { from: USER0 }), "ds-token-insufficient-balance"); + }); + it("responds to getter functions correctly while running", async () => { await purchaseToken.mint(USER0, WAD, { from: USER0 }); await purchaseToken.approve(coinMachine.address, WAD, { from: USER0 }); diff --git a/test/extensions/funding-queue.js b/test/extensions/funding-queue.js index 86ef0211ec..b61b27fe4c 100644 --- a/test/extensions/funding-queue.js +++ b/test/extensions/funding-queue.js @@ -24,6 +24,7 @@ import { setupRandomColony, giveUserCLNYTokensAndStake, setupMetaColonyWithLockedCLNYToken, + getMetaTransactionParameters, } from "../../helpers/test-data-generator"; import { setupEtherRouter } from "../../helpers/upgradable-contracts"; @@ -228,6 +229,23 @@ contract("Funding Queues", (accounts) => { expect(proposal.state).to.eq.BN(STATE_INACTIVE); }); + it("can create a basic proposal via metatransaction", async () => { + await fundingQueue.createProposal(1, UINT256_MAX, 0, 1, 2, WAD, token.address, { from: USER0 }); + const txData = await fundingQueue.contract.methods + .createProposal(1, UINT256_MAX.toString(), 0, 1, 2, WAD.toString(), token.address) + .encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER0, fundingQueue.address); + + await fundingQueue.executeMetaTransaction(USER0, txData, r, s, v, { from: USER1 }); + + const proposalId = await fundingQueue.getProposalCount(); + + const proposal = await fundingQueue.getProposal(proposalId); + expect(proposal.domainId).to.eq.BN(1); + expect(proposal.state).to.eq.BN(STATE_INACTIVE); + }); + it("cannot create a basic proposal if deprecated", async () => { let deprecated = await fundingQueue.getDeprecated(); expect(deprecated).to.equal(false); diff --git a/test/extensions/one-tx-payment.js b/test/extensions/one-tx-payment.js index 65459f1d43..0c3f5a4b5f 100644 --- a/test/extensions/one-tx-payment.js +++ b/test/extensions/one-tx-payment.js @@ -7,7 +7,13 @@ import { soliditySha3 } from "web3-utils"; import { UINT256_MAX, WAD, INITIAL_FUNDING, GLOBAL_SKILL_ID, FUNDING_ROLE, ADMINISTRATION_ROLE } from "../../helpers/constants"; import { checkErrorRevert, web3GetCode, rolesToBytes32, expectEvent } from "../../helpers/test-helper"; -import { setupColonyNetwork, setupMetaColonyWithLockedCLNYToken, setupRandomColony, fundColonyWithTokens } from "../../helpers/test-data-generator"; +import { + setupColonyNetwork, + setupMetaColonyWithLockedCLNYToken, + setupRandomColony, + fundColonyWithTokens, + getMetaTransactionParameters, +} from "../../helpers/test-data-generator"; import { setupEtherRouter } from "../../helpers/upgradable-contracts"; const { expect } = chai; @@ -112,6 +118,33 @@ contract("One transaction payments", (accounts) => { await expectEvent(tx, "OneTxPaymentMade", [accounts[0], 1, 1]); }); + it("should allow a single-transaction payment of tokens to occur via metatransaction", async () => { + const balanceBefore = await token.balanceOf(USER1); + expect(balanceBefore).to.be.zero; + + const txData = await oneTxPayment.contract.methods + .makePaymentFundedFromDomain( + 1, + UINT256_MAX.toString(), + 1, + UINT256_MAX.toString(), + [USER1], + [token.address], + [10], + 1, + GLOBAL_SKILL_ID.toString() + ) + .encodeABI(); + const { r, s, v } = await getMetaTransactionParameters(txData, accounts[0], oneTxPayment.address); + + const tx = await oneTxPayment.executeMetaTransaction(accounts[0], txData, r, s, v, { from: USER2 }); + + const balanceAfter = await token.balanceOf(USER1); + expect(balanceAfter).to.eq.BN(9); + + await expectEvent(tx, "OneTxPaymentMade", [accounts[0], 1, 1]); + }); + it("should allow a single-transaction payment of ETH to occur", async () => { const balanceBefore = await web3.eth.getBalance(USER1); await colony.send(10); // NB 10 wei, not ten ether! diff --git a/test/extensions/token-supplier.js b/test/extensions/token-supplier.js index e21de7518c..f19fc5e9bc 100644 --- a/test/extensions/token-supplier.js +++ b/test/extensions/token-supplier.js @@ -8,7 +8,12 @@ import { soliditySha3 } from "web3-utils"; import { UINT256_MAX, WAD, SECONDS_PER_DAY } from "../../helpers/constants"; import { checkErrorRevert, currentBlockTime, makeTxAtTimestamp, getBlockTime, forwardTime } from "../../helpers/test-helper"; -import { setupColonyNetwork, setupRandomColony, setupMetaColonyWithLockedCLNYToken } from "../../helpers/test-data-generator"; +import { + setupColonyNetwork, + setupRandomColony, + setupMetaColonyWithLockedCLNYToken, + getMetaTransactionParameters, +} from "../../helpers/test-data-generator"; import { setupEtherRouter } from "../../helpers/upgradable-contracts"; const { expect } = chai; @@ -218,6 +223,24 @@ contract("Token Supplier", (accounts) => { expect(tokenSupply).to.eq.BN(WAD); }); + it("can claim tokenIssuanceRate tokens per day via metatransaction", async () => { + const balancePre = await token.balanceOf(colony.address); + + let time = await currentBlockTime(); + time = new BN(time).addn(SECONDS_PER_DAY); + + const txData = await tokenSupplier.contract.methods.issueTokens().encodeABI(); + const { r, s, v } = await getMetaTransactionParameters(txData, accounts[0], tokenSupplier.address); + + await makeTxAtTimestamp(tokenSupplier.executeMetaTransaction, [accounts[0], txData, r, s, v], time.toNumber(), this); + + const balancePost = await token.balanceOf(colony.address); + expect(balancePost.sub(balancePre)).to.eq.BN(WAD); + + const tokenSupply = await token.totalSupply(); + expect(tokenSupply).to.eq.BN(WAD); + }); + it("can claim tokenIssuanceRate at high frequencies", async () => { const balancePre = await token.balanceOf(colony.address); diff --git a/test/extensions/voting-rep.js b/test/extensions/voting-rep.js index f80c77198d..7a276880fe 100644 --- a/test/extensions/voting-rep.js +++ b/test/extensions/voting-rep.js @@ -26,6 +26,7 @@ import { setupMetaColonyWithLockedCLNYToken, setupRandomColony, giveUserCLNYTokensAndStake, + getMetaTransactionParameters, } from "../../helpers/test-data-generator"; import { setupEtherRouter } from "../../helpers/upgradable-contracts"; @@ -362,6 +363,21 @@ contract("Voting Reputation", (accounts) => { expect(motion.skillId).to.eq.BN(domain1.skillId); }); + it("can create a root motion via metatransaction", async () => { + const action = await encodeTxData(colony, "mintTokens", [WAD]); + const txData = await voting.contract.methods + .createMotion(1, UINT256_MAX.toString(), ADDRESS_ZERO, action, domain1Key, domain1Value, domain1Mask, domain1Siblings) + .encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER2, voting.address); + + await voting.executeMetaTransaction(USER2, txData, r, s, v, { from: USER1 }); + + const motionId = await voting.getMotionCount(); + const motion = await voting.getMotion(motionId); + expect(motion.skillId).to.eq.BN(domain1.skillId); + }); + it("can create a domain motion in the root domain", async () => { // Create motion in domain of action (1) const action = await encodeTxData(colony, "makeTask", [1, UINT256_MAX, FAKE, 1, 0, 0]); diff --git a/test/extensions/whitelist.js b/test/extensions/whitelist.js index 93bdf83f1a..1d881dcc10 100644 --- a/test/extensions/whitelist.js +++ b/test/extensions/whitelist.js @@ -8,7 +8,12 @@ import { soliditySha3 } from "web3-utils"; import { UINT256_MAX, IPFS_HASH } from "../../helpers/constants"; import { setupEtherRouter } from "../../helpers/upgradable-contracts"; import { checkErrorRevert, web3GetCode } from "../../helpers/test-helper"; -import { setupColonyNetwork, setupRandomColony, setupMetaColonyWithLockedCLNYToken } from "../../helpers/test-data-generator"; +import { + setupColonyNetwork, + setupRandomColony, + setupMetaColonyWithLockedCLNYToken, + getMetaTransactionParameters, +} from "../../helpers/test-data-generator"; const { expect } = chai; chai.use(bnChai(web3.utils.BN)); @@ -169,6 +174,31 @@ contract("Whitelist", (accounts) => { expect(status).to.be.true; }); + it("can make users sign an agreement via metatransaction", async () => { + await whitelist.initialise(false, IPFS_HASH); + + let status; + let signature; + + signature = await whitelist.getSignature(USER1); + expect(signature).to.be.false; + + status = await whitelist.isApproved(USER1); + expect(status).to.be.false; + + const txData = await whitelist.contract.methods.signAgreement(IPFS_HASH).encodeABI(); + + const { r, s, v } = await getMetaTransactionParameters(txData, USER1, whitelist.address); + + await whitelist.executeMetaTransaction(USER1, txData, r, s, v, { from: USER0 }); + + signature = await whitelist.getSignature(USER1); + expect(signature).to.be.true; + + status = await whitelist.isApproved(USER1); + expect(status).to.be.true; + }); + it("cannot accept a bad agreement", async () => { await whitelist.initialise(false, IPFS_HASH);