diff --git a/nucypher/blockchain/eth/agents.py b/nucypher/blockchain/eth/agents.py index 3768b29d0..05b28d680 100644 --- a/nucypher/blockchain/eth/agents.py +++ b/nucypher/blockchain/eth/agents.py @@ -22,6 +22,7 @@ from typing import Generator, List, Tuple, Union import math from constant_sorrow.constants import NO_CONTRACT_AVAILABLE from eth_utils.address import to_checksum_address +from eth_tester.exceptions import TransactionFailed from twisted.logger import Logger from web3.contract import Contract @@ -34,6 +35,7 @@ from nucypher.blockchain.eth.constants import ( STAKING_INTERFACE_ROUTER_CONTRACT_NAME, ADJUDICATOR_CONTRACT_NAME, NUCYPHER_TOKEN_CONTRACT_NAME, + MULTISIG_CONTRACT_NAME, ETH_ADDRESS_BYTE_LENGTH ) from nucypher.blockchain.eth.decorators import validate_checksum_address @@ -1126,6 +1128,8 @@ class SeederAgent(EthereumContractAgent): class MultiSigAgent(EthereumContractAgent): + registry_contract_name = MULTISIG_CONTRACT_NAME + Vector = List[str] @property @@ -1133,9 +1137,24 @@ class MultiSigAgent(EthereumContractAgent): nonce = self.contract.functions.nonce().call() return nonce - def get_owners(self) -> Tuple[str]: - result = self.contract.functions.owners().call() - return tuple(result) + def get_owner(self, index: int) -> str: + owner = self.contract.functions.owners(index).call() + return owner + + @property + def owners(self) -> Tuple[str]: + i = 0 + owners = list() + array_is_within_bounds = True + while array_is_within_bounds: + try: + owner = self.get_owner(i) + except (TransactionFailed, ValueError): + array_is_within_bounds = False + else: + owners.append(owner) + i += 1 + return tuple(owners) @property def threshold(self) -> int: @@ -1167,13 +1186,13 @@ class MultiSigAgent(EthereumContractAgent): data: bytes, nonce: int ) -> bytes: - transaction_hash = self.contract.functions.getUnsignedTransactionHash( - trustee_address, - target_address, - value, - data, - nonce - ).call() + transaction_args = (trustee_address, + target_address, + value, + data, + nonce) + + transaction_hash = self.contract.functions.getUnsignedTransactionHash(*transaction_args).call() return transaction_hash def execute(self, diff --git a/nucypher/blockchain/eth/constants.py b/nucypher/blockchain/eth/constants.py index 082841dad..ffeeffa33 100644 --- a/nucypher/blockchain/eth/constants.py +++ b/nucypher/blockchain/eth/constants.py @@ -13,6 +13,7 @@ STAKING_INTERFACE_CONTRACT_NAME = 'StakingInterface' PREALLOCATION_ESCROW_CONTRACT_NAME = 'PreallocationEscrow' ADJUDICATOR_CONTRACT_NAME = 'Adjudicator' WORKLOCK_CONTRACT_NAME = 'WorkLock' +MULTISIG_CONTRACT_NAME = 'MultiSig' # Ethereum diff --git a/nucypher/blockchain/eth/deployers.py b/nucypher/blockchain/eth/deployers.py index 787d41451..c2741a6e2 100644 --- a/nucypher/blockchain/eth/deployers.py +++ b/nucypher/blockchain/eth/deployers.py @@ -1176,8 +1176,19 @@ class MultiSigDeployer(BaseContractDeployer): agency = MultiSigAgent contract_name = agency.registry_contract_name deployment_steps = ('contract_deployment', ) + _upgradeable = False + + MAX_OWNER_COUNT = 50 # Hard-coded limit in MultiSig contract def _deploy_essential(self, threshold: int, owners: List[str], gas_limit: int = None): + if not (0 < threshold <= len(owners) <= self.MAX_OWNER_COUNT): + raise ValueError(f"Parameters threshold={threshold} and len(owners)={len(owners)} don't satisfy inequality " + f"0 < threshold <= len(owners) <= {self.MAX_OWNER_COUNT}") + if BlockchainDeployerInterface.NULL_ADDRESS in owners: + raise ValueError("The null address is not allowed as an owner") + if len(owners) != len(set(owners)): + raise ValueError("Can't use the same owner address more than once") + constructor_args = (threshold, owners) multisig_contract, deploy_receipt = self.blockchain.deploy_contract(self.deployer_address, @@ -1187,7 +1198,7 @@ class MultiSigDeployer(BaseContractDeployer): gas_limit=gas_limit) return multisig_contract, deploy_receipt - def deploy(self, gas_limit: int, progress=None, *args, **kwargs) -> dict: + def deploy(self, gas_limit: int = None, progress=None, *args, **kwargs) -> dict: self.check_deployment_readiness() multisig_contract, deploy_receipt = self._deploy_essential(gas_limit=gas_limit, *args, **kwargs) @@ -1197,7 +1208,7 @@ class MultiSigDeployer(BaseContractDeployer): progress.update(1) # Gather the transaction receipts - self.deployment_receipts.update({'deployment': deploy_receipt}) + self.deployment_receipts.update({self.deployment_steps[0]: deploy_receipt}) self._contract = multisig_contract - return deploy_receipt + return self.deployment_receipts diff --git a/tests/blockchain/eth/entities/deployers/test_multisig_deployer.py b/tests/blockchain/eth/entities/deployers/test_multisig_deployer.py new file mode 100644 index 000000000..aba3474a2 --- /dev/null +++ b/tests/blockchain/eth/entities/deployers/test_multisig_deployer.py @@ -0,0 +1,68 @@ +""" +This file is part of nucypher. + +nucypher is free software: you can redistribute it and/or modify +it under the terms of the GNU Affero General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +nucypher 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 Affero General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with nucypher. If not, see . +""" + +import pytest + +from nucypher.blockchain.eth.agents import MultiSigAgent +from nucypher.blockchain.eth.deployers import MultiSigDeployer +from nucypher.blockchain.eth.interfaces import BlockchainInterface + + +@pytest.mark.slow() +def test_multisig_deployer_and_agent(testerchain, + deployment_progress, + test_registry): + origin = testerchain.etherbase_account + multisig_deployer = MultiSigDeployer(deployer_address=origin, registry=test_registry) + + # Can't have a threshold of 0 + with pytest.raises(ValueError): + owners = testerchain.unassigned_accounts[0:3] + _ = multisig_deployer.deploy(threshold=0, owners=owners) + + # Can't have no owners + with pytest.raises(ValueError): + _ = multisig_deployer.deploy(threshold=1, owners=[]) + + # Can't have the zero address as an owner + with pytest.raises(ValueError): + owners = testerchain.unassigned_accounts[0:3] + [BlockchainInterface.NULL_ADDRESS] + _ = multisig_deployer.deploy(threshold=1, owners=owners) + + # Can't have repeated owners + with pytest.raises(ValueError): + owners = testerchain.unassigned_accounts[0] * 3 + _ = multisig_deployer.deploy(threshold=1, owners=owners) + + # At last, sane initialization arguments for the MultiSig + threshold = 2 + owners = testerchain.unassigned_accounts[0:3] + receipts = multisig_deployer.deploy(threshold=threshold, owners=owners) + for step in multisig_deployer.deployment_steps: + assert receipts[step]['status'] == 1 + + multisig_agent = multisig_deployer.make_agent() # type: MultiSigAgent + + assert multisig_agent.nonce == 0 + assert multisig_agent.threshold == threshold + for i, owner in enumerate(owners): + assert multisig_agent.get_owner(i) == owner + assert multisig_agent.is_owner(owner) + assert multisig_agent.owners == tuple(owners) + + +