From 0fed48630cb1c9ea99eef2802a368a9e1f64d6ad Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Tue, 2 Jun 2020 11:39:28 -0400 Subject: [PATCH 1/2] `@0x/contracts-zero-ex`: Add `TransformDeployer` contract. --- .../src/external/TransformerDeployer.sol | 226 ++++++++++++++++++ 1 file changed, 226 insertions(+) create mode 100644 contracts/zero-ex/contracts/src/external/TransformerDeployer.sol diff --git a/contracts/zero-ex/contracts/src/external/TransformerDeployer.sol b/contracts/zero-ex/contracts/src/external/TransformerDeployer.sol new file mode 100644 index 0000000000..95294b8cff --- /dev/null +++ b/contracts/zero-ex/contracts/src/external/TransformerDeployer.sol @@ -0,0 +1,226 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + + +/// @dev A contract with a `die()` function. +interface IKillable { + function die() external; +} + +/// @dev Shared deployer contract for ERC20 transformers implementing some +/// basic membership governance. +/// All members can create or kill transformers individually. +/// A majority vote is required to add or remove memebers. +contract TransformerDeployer { + + /// @dev Emitted when a contract is deployed via `deploy()`. + event Deployed(address deployedAddress, address member); + /// @dev Emitted when a contract is killed via `kill()`. + event Kill(address target, address member); + + /// @dev Represents a member (de)registration vote. + /// Should be passed into `addMembers()` and `removeMembers()` with + /// accompanying signature. + struct Vote { + // Members to add or remove. + address[] members; + // How long this vote is valid for. + uint256 expirationTime; + } + + /// @dev The EIP712 typehash for `addMembers()` votes. + bytes32 private immutable ADD_MEMBERS_VOTE_TYPEHASH = keccak256( + "AddMembersVote(address[] member,uint256 nonce,bytes signature)" + ); + /// @dev The EIP712 typehash for `removeMembers()` votes. + bytes32 private immutable REMOVE_MEMBERS_VOTE_TYPEHASH = keccak256( + "RemoveMembersVote(address[] member,uint256 nonce,bytes signature)" + ); + + /// @dev The EIP712 domain separator. + bytes32 public immutable EIP712_DOMAIN_SEPARATOR; + /// @dev The current deployment nonce of this contract. + /// Unlike EOAs, contracts start with a nonce of 1. + uint256 public deploymentNonce = 1; + /// @dev The number of registered members. + uint256 public memberCount; + /// @dev Whether an address is a member. + mapping(address => bool) public isMember; + /// @dev Whether a vote was consumed. + mapping(bytes32 => bool) public isVoteConsumed; + + /// @dev Only a valid member can call the function. + modifier onlyMember() { + require(isMember[msg.sender], "TransformerDeployer/ONLY_CALLABLE_By_MEMBER"); + _; + } + + /// @dev Create this contract and seed the initial members. + constructor(address[] memory members) public { + uint256 chainId; + assembly { chainId := chainid() } + EIP712_DOMAIN_SEPARATOR = keccak256(abi.encode( + keccak256( + "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" + ), + keccak256(bytes('TransformerDeployer')), + keccak256(bytes('1.0.0')), + chainId, + address(this) + )); + _addMembers(members); + } + + /// @dev Registers new members. Must attach majority votes. + function addMembers(Vote[] memory votes, bytes[] memory signatures) + public + { + _consumeVotes(ADD_MEMBERS_VOTE_TYPEHASH, votes, signatures); + _addMembers(votes[0].members); + } + + /// @dev Removes existing members. Must attach majority votes. + function removeMembers(Vote[] memory votes, bytes[] memory signatures) + public + { + _consumeVotes(REMOVE_MEMBERS_VOTE_TYPEHASH, votes, signatures); + _removeMembers(votes[0].members); + } + + /// @dev Deploy a new contract. Only callable by a member. + /// Any attached ETH will also be forwarded. + function deploy(bytes memory bytecode) + public + payable + onlyMember + returns (address deployedAddress) + { + deploymentNonce += 1; + assembly { + deployedAddress := create(callvalue(), add(bytecode, 32), mload(bytecode)) + } + emit Deployed(deployedAddress, msg.sender); + } + + /// @dev Call `die()` on a contract. Only callable by a member. + function kill(IKillable target) + public + onlyMember + { + target.die(); + } + + /// @dev Check that votes are valid and have majority and consumes them. + function _consumeVotes( + bytes32 typeHash, + Vote[] memory votes, + bytes[] memory signatures + ) + internal + { + require(votes.length >= memberCount / 2 + 1, "TransformerDeployer/INSUFFICIENT_VOTES"); + bytes32 membersHash = keccak256(abi.encode(votes[0].members)); + address[] memory signers = new address[](votes.length); + for (uint256 i = 0; i < votes.length; ++i) { + // Ensure the vote isn't expired. + require(votes[i].expirationTime > block.timestamp, "TransformerDeployer/VOTE_EXPIRED"); + // Ensure the members are the same across all votes. + require( + membersHash == keccak256(abi.encode(votes[i].members)), + "TransformerDeployer/NONHOMOGENOUS_VOTE" + ); + bytes32 voteHash = _getVoteHash(typeHash, votes[i]); + // Get the signer of the vote. + address signer = signers[i] = _getVoteSigner(voteHash, signatures[i]); + // Check for duplicates. + for (uint256 j = 0; j < i; ++j) { + require(signers[j] != signer, "TransformerDeployer/DUPLICATE_SIGNER"); + } + // Ensure signer is a member. + require(isMember[signer], "TransformerDeployer/NOT_A_MEMBER"); + // Ensure the vote wasn't already consumed. + require(!isVoteConsumed[voteHash], "TransformerDeployer/ALREADY_VOTED"); + // Mark the vote consumed. + isVoteConsumed[voteHash] = true; + } + } + + /// @dev Get the signer given a vote and signature. + function _getVoteSigner( + bytes32 voteHash, + bytes memory signature + ) + internal + pure + returns (address signer) + { + require(signature.length == 65, "TransformerDeployer/INVALID_SIGNATURE"); + bytes32 r; + bytes32 s; + uint8 v; + assembly { + r := mload(add(signature, 32)) + s := mload(add(signature, 64)) + v := and(mload(add(signature, 65)), 0x00000000000000000000000000000000000000000000000000000000000000ff) + } + return ecrecover(voteHash, v, r, s); + } + + /// @dev Get the EIP712 hash of a vote. + function _getVoteHash(bytes32 typeHash, Vote memory vote) + internal + view + returns (bytes32 voteHash) + { + bytes32 messageHash = keccak256(abi.encode( + typeHash, + vote.members, + vote.expirationTime + )); + return keccak256(abi.encodePacked( + '\x19\x01', + EIP712_DOMAIN_SEPARATOR, + messageHash + )); + } + + /// @dev Register new members. + function _addMembers(address[] memory members) + private + { + for (uint256 i = 0; i < members.length; ++i) { + require(!isMember[members[i]], "TransformerDeployer/ALREADY_MEMBER"); + isMember[members[i]] = true; + } + memberCount += members.length; + } + + /// @dev Deregister existing members. + function _removeMembers(address[] memory members) + private + { + for (uint256 i = 0; i < members.length; ++i) { + require(isMember[members[i]], "TransformerDeployer/NOT_A_MEMBER"); + isMember[members[i]] = false; + } + memberCount -= members.length; + } +} From 87ed0071c4361ae5f511ce89c10381ac2ad21c1e Mon Sep 17 00:00:00 2001 From: Lawrence Forman Date: Sat, 6 Jun 2020 00:30:30 -0400 Subject: [PATCH 2/2] `@0x/contracts-zero-ex`: Make `TransformerDeployer` boring. --- .../src/external/TransformerDeployer.sol | 210 +++--------------- .../TestTransformerDeployerTransformer.sol | 52 +++++ contracts/zero-ex/package.json | 2 +- contracts/zero-ex/test/artifacts.ts | 4 + .../zero-ex/test/transformer_deployer_test.ts | 109 +++++++++ contracts/zero-ex/test/wrappers.ts | 2 + contracts/zero-ex/tsconfig.json | 2 + 7 files changed, 203 insertions(+), 178 deletions(-) create mode 100644 contracts/zero-ex/contracts/test/TestTransformerDeployerTransformer.sol create mode 100644 contracts/zero-ex/test/transformer_deployer_test.ts diff --git a/contracts/zero-ex/contracts/src/external/TransformerDeployer.sol b/contracts/zero-ex/contracts/src/external/TransformerDeployer.sol index 95294b8cff..3e3243605c 100644 --- a/contracts/zero-ex/contracts/src/external/TransformerDeployer.sol +++ b/contracts/zero-ex/contracts/src/external/TransformerDeployer.sol @@ -19,208 +19,64 @@ pragma solidity ^0.6.5; pragma experimental ABIEncoderV2; +import "@0x/contracts-utils/contracts/src/v06/AuthorizableV06.sol"; + /// @dev A contract with a `die()` function. interface IKillable { function die() external; } -/// @dev Shared deployer contract for ERC20 transformers implementing some -/// basic membership governance. -/// All members can create or kill transformers individually. -/// A majority vote is required to add or remove memebers. -contract TransformerDeployer { - +/// @dev Deployer contract for ERC20 transformers. +/// Only authorities may call `deploy()` and `kill()`. +contract TransformerDeployer is + AuthorizableV06 +{ /// @dev Emitted when a contract is deployed via `deploy()`. - event Deployed(address deployedAddress, address member); + /// @param deployedAddress The address of the deployed contract. + /// @param nonce The deployment nonce. + /// @param sender The caller of `deploy()`. + event Deployed(address deployedAddress, uint256 nonce, address sender); /// @dev Emitted when a contract is killed via `kill()`. - event Kill(address target, address member); + /// @param target The address of the contract being killed.. + /// @param sender The caller of `kill()`. + event Killed(address target, address sender); - /// @dev Represents a member (de)registration vote. - /// Should be passed into `addMembers()` and `removeMembers()` with - /// accompanying signature. - struct Vote { - // Members to add or remove. - address[] members; - // How long this vote is valid for. - uint256 expirationTime; + // @dev The current nonce of this contract. + uint256 public nonce = 1; + // @dev Mapping of deployed contract address to deployment nonce. + mapping (address => uint256) public toDeploymentNonce; + + /// @dev Create this contract and register authorities. + constructor(address[] memory authorities) public { + for (uint256 i = 0; i < authorities.length; ++i) { + _addAuthorizedAddress(authorities[i]); + } } - /// @dev The EIP712 typehash for `addMembers()` votes. - bytes32 private immutable ADD_MEMBERS_VOTE_TYPEHASH = keccak256( - "AddMembersVote(address[] member,uint256 nonce,bytes signature)" - ); - /// @dev The EIP712 typehash for `removeMembers()` votes. - bytes32 private immutable REMOVE_MEMBERS_VOTE_TYPEHASH = keccak256( - "RemoveMembersVote(address[] member,uint256 nonce,bytes signature)" - ); - - /// @dev The EIP712 domain separator. - bytes32 public immutable EIP712_DOMAIN_SEPARATOR; - /// @dev The current deployment nonce of this contract. - /// Unlike EOAs, contracts start with a nonce of 1. - uint256 public deploymentNonce = 1; - /// @dev The number of registered members. - uint256 public memberCount; - /// @dev Whether an address is a member. - mapping(address => bool) public isMember; - /// @dev Whether a vote was consumed. - mapping(bytes32 => bool) public isVoteConsumed; - - /// @dev Only a valid member can call the function. - modifier onlyMember() { - require(isMember[msg.sender], "TransformerDeployer/ONLY_CALLABLE_By_MEMBER"); - _; - } - - /// @dev Create this contract and seed the initial members. - constructor(address[] memory members) public { - uint256 chainId; - assembly { chainId := chainid() } - EIP712_DOMAIN_SEPARATOR = keccak256(abi.encode( - keccak256( - "EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" - ), - keccak256(bytes('TransformerDeployer')), - keccak256(bytes('1.0.0')), - chainId, - address(this) - )); - _addMembers(members); - } - - /// @dev Registers new members. Must attach majority votes. - function addMembers(Vote[] memory votes, bytes[] memory signatures) - public - { - _consumeVotes(ADD_MEMBERS_VOTE_TYPEHASH, votes, signatures); - _addMembers(votes[0].members); - } - - /// @dev Removes existing members. Must attach majority votes. - function removeMembers(Vote[] memory votes, bytes[] memory signatures) - public - { - _consumeVotes(REMOVE_MEMBERS_VOTE_TYPEHASH, votes, signatures); - _removeMembers(votes[0].members); - } - - /// @dev Deploy a new contract. Only callable by a member. + /// @dev Deploy a new contract. Only callable by an authority. /// Any attached ETH will also be forwarded. function deploy(bytes memory bytecode) public payable - onlyMember + onlyAuthorized returns (address deployedAddress) { - deploymentNonce += 1; + uint256 deploymentNonce = nonce; + nonce += 1; assembly { deployedAddress := create(callvalue(), add(bytecode, 32), mload(bytecode)) } - emit Deployed(deployedAddress, msg.sender); + toDeploymentNonce[deployedAddress] = deploymentNonce; + emit Deployed(deployedAddress, deploymentNonce, msg.sender); } - /// @dev Call `die()` on a contract. Only callable by a member. + /// @dev Call `die()` on a contract. Only callable by an authority. function kill(IKillable target) public - onlyMember + onlyAuthorized { target.die(); - } - - /// @dev Check that votes are valid and have majority and consumes them. - function _consumeVotes( - bytes32 typeHash, - Vote[] memory votes, - bytes[] memory signatures - ) - internal - { - require(votes.length >= memberCount / 2 + 1, "TransformerDeployer/INSUFFICIENT_VOTES"); - bytes32 membersHash = keccak256(abi.encode(votes[0].members)); - address[] memory signers = new address[](votes.length); - for (uint256 i = 0; i < votes.length; ++i) { - // Ensure the vote isn't expired. - require(votes[i].expirationTime > block.timestamp, "TransformerDeployer/VOTE_EXPIRED"); - // Ensure the members are the same across all votes. - require( - membersHash == keccak256(abi.encode(votes[i].members)), - "TransformerDeployer/NONHOMOGENOUS_VOTE" - ); - bytes32 voteHash = _getVoteHash(typeHash, votes[i]); - // Get the signer of the vote. - address signer = signers[i] = _getVoteSigner(voteHash, signatures[i]); - // Check for duplicates. - for (uint256 j = 0; j < i; ++j) { - require(signers[j] != signer, "TransformerDeployer/DUPLICATE_SIGNER"); - } - // Ensure signer is a member. - require(isMember[signer], "TransformerDeployer/NOT_A_MEMBER"); - // Ensure the vote wasn't already consumed. - require(!isVoteConsumed[voteHash], "TransformerDeployer/ALREADY_VOTED"); - // Mark the vote consumed. - isVoteConsumed[voteHash] = true; - } - } - - /// @dev Get the signer given a vote and signature. - function _getVoteSigner( - bytes32 voteHash, - bytes memory signature - ) - internal - pure - returns (address signer) - { - require(signature.length == 65, "TransformerDeployer/INVALID_SIGNATURE"); - bytes32 r; - bytes32 s; - uint8 v; - assembly { - r := mload(add(signature, 32)) - s := mload(add(signature, 64)) - v := and(mload(add(signature, 65)), 0x00000000000000000000000000000000000000000000000000000000000000ff) - } - return ecrecover(voteHash, v, r, s); - } - - /// @dev Get the EIP712 hash of a vote. - function _getVoteHash(bytes32 typeHash, Vote memory vote) - internal - view - returns (bytes32 voteHash) - { - bytes32 messageHash = keccak256(abi.encode( - typeHash, - vote.members, - vote.expirationTime - )); - return keccak256(abi.encodePacked( - '\x19\x01', - EIP712_DOMAIN_SEPARATOR, - messageHash - )); - } - - /// @dev Register new members. - function _addMembers(address[] memory members) - private - { - for (uint256 i = 0; i < members.length; ++i) { - require(!isMember[members[i]], "TransformerDeployer/ALREADY_MEMBER"); - isMember[members[i]] = true; - } - memberCount += members.length; - } - - /// @dev Deregister existing members. - function _removeMembers(address[] memory members) - private - { - for (uint256 i = 0; i < members.length; ++i) { - require(isMember[members[i]], "TransformerDeployer/NOT_A_MEMBER"); - isMember[members[i]] = false; - } - memberCount -= members.length; + emit Killed(address(target), msg.sender); } } diff --git a/contracts/zero-ex/contracts/test/TestTransformerDeployerTransformer.sol b/contracts/zero-ex/contracts/test/TestTransformerDeployerTransformer.sol new file mode 100644 index 0000000000..f234d33c03 --- /dev/null +++ b/contracts/zero-ex/contracts/test/TestTransformerDeployerTransformer.sol @@ -0,0 +1,52 @@ +/* + + Copyright 2020 ZeroEx Intl. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +*/ + +pragma solidity ^0.6.5; +pragma experimental ABIEncoderV2; + +import "../src/transformers/LibERC20Transformer.sol"; + + +contract TestTransformerDeployerTransformer { + + address payable public immutable deployer; + + constructor() public payable { + deployer = msg.sender; + } + + modifier onlyDeployer() { + require(msg.sender == deployer, "TestTransformerDeployerTransformer/ONLY_DEPLOYER"); + _; + } + + function die() + external + onlyDeployer + { + selfdestruct(deployer); + } + + function isDeployedByDeployer(uint32 nonce) + external + view + returns (bool) + { + return LibERC20Transformer.getDeployedAddress(deployer, nonce) == address(this); + } +} diff --git a/contracts/zero-ex/package.json b/contracts/zero-ex/package.json index d94206f4e9..febccd8d1c 100644 --- a/contracts/zero-ex/package.json +++ b/contracts/zero-ex/package.json @@ -40,7 +40,7 @@ "config": { "publicInterfaceContracts": "ZeroEx,FullMigration,InitialMigration,IFlashWallet,IAllowanceTarget,IERC20Transformer,IOwnable,ISimpleFunctionRegistry,ITokenSpender,ITransformERC20,FillQuoteTransformer,PayTakerTransformer,WethTransformer", "abis:comment": "This list is auto-generated by contracts-gen. Don't edit manually.", - "abis": "./test/generated-artifacts/@(AllowanceTarget|Bootstrap|FillQuoteTransformer|FixinCommon|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Transformer|IExchange|IFeature|IFlashWallet|IOwnable|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITransformERC20|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|Ownable|PayTakerTransformer|SimpleFunctionRegistry|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpender|TransformERC20|Transformer|WethTransformer|ZeroEx).json" + "abis": "./test/generated-artifacts/@(AllowanceTarget|Bootstrap|FillQuoteTransformer|FixinCommon|FlashWallet|FullMigration|IAllowanceTarget|IBootstrap|IERC20Transformer|IExchange|IFeature|IFlashWallet|IOwnable|ISimpleFunctionRegistry|ITestSimpleFunctionRegistryFeature|ITokenSpender|ITransformERC20|InitialMigration|LibBootstrap|LibCommonRichErrors|LibERC20Transformer|LibMigrate|LibOwnableRichErrors|LibOwnableStorage|LibProxyRichErrors|LibProxyStorage|LibSimpleFunctionRegistryRichErrors|LibSimpleFunctionRegistryStorage|LibSpenderRichErrors|LibStorage|LibTokenSpenderStorage|LibTransformERC20RichErrors|LibTransformERC20Storage|LibWalletRichErrors|Ownable|PayTakerTransformer|SimpleFunctionRegistry|TestCallTarget|TestDelegateCaller|TestFillQuoteTransformerExchange|TestFillQuoteTransformerHost|TestFullMigration|TestInitialMigration|TestMigrator|TestMintTokenERC20Transformer|TestMintableERC20Token|TestSimpleFunctionRegistryFeatureImpl1|TestSimpleFunctionRegistryFeatureImpl2|TestTokenSpender|TestTokenSpenderERC20Token|TestTransformERC20|TestTransformerBase|TestTransformerDeployerTransformer|TestTransformerHost|TestWeth|TestWethTransformerHost|TestZeroExFeature|TokenSpender|TransformERC20|Transformer|TransformerDeployer|WethTransformer|ZeroEx).json" }, "repository": { "type": "git", diff --git a/contracts/zero-ex/test/artifacts.ts b/contracts/zero-ex/test/artifacts.ts index 4774071dca..0d76a254d4 100644 --- a/contracts/zero-ex/test/artifacts.ts +++ b/contracts/zero-ex/test/artifacts.ts @@ -57,6 +57,7 @@ import * as TestTokenSpender from '../test/generated-artifacts/TestTokenSpender. import * as TestTokenSpenderERC20Token from '../test/generated-artifacts/TestTokenSpenderERC20Token.json'; import * as TestTransformerBase from '../test/generated-artifacts/TestTransformerBase.json'; import * as TestTransformERC20 from '../test/generated-artifacts/TestTransformERC20.json'; +import * as TestTransformerDeployerTransformer from '../test/generated-artifacts/TestTransformerDeployerTransformer.json'; import * as TestTransformerHost from '../test/generated-artifacts/TestTransformerHost.json'; import * as TestWeth from '../test/generated-artifacts/TestWeth.json'; import * as TestWethTransformerHost from '../test/generated-artifacts/TestWethTransformerHost.json'; @@ -64,6 +65,7 @@ import * as TestZeroExFeature from '../test/generated-artifacts/TestZeroExFeatur import * as TokenSpender from '../test/generated-artifacts/TokenSpender.json'; import * as Transformer from '../test/generated-artifacts/Transformer.json'; import * as TransformERC20 from '../test/generated-artifacts/TransformERC20.json'; +import * as TransformerDeployer from '../test/generated-artifacts/TransformerDeployer.json'; import * as WethTransformer from '../test/generated-artifacts/WethTransformer.json'; import * as ZeroEx from '../test/generated-artifacts/ZeroEx.json'; export const artifacts = { @@ -79,6 +81,7 @@ export const artifacts = { FlashWallet: FlashWallet as ContractArtifact, IAllowanceTarget: IAllowanceTarget as ContractArtifact, IFlashWallet: IFlashWallet as ContractArtifact, + TransformerDeployer: TransformerDeployer as ContractArtifact, Bootstrap: Bootstrap as ContractArtifact, IBootstrap: IBootstrap as ContractArtifact, IFeature: IFeature as ContractArtifact, @@ -124,6 +127,7 @@ export const artifacts = { TestTokenSpenderERC20Token: TestTokenSpenderERC20Token as ContractArtifact, TestTransformERC20: TestTransformERC20 as ContractArtifact, TestTransformerBase: TestTransformerBase as ContractArtifact, + TestTransformerDeployerTransformer: TestTransformerDeployerTransformer as ContractArtifact, TestTransformerHost: TestTransformerHost as ContractArtifact, TestWeth: TestWeth as ContractArtifact, TestWethTransformerHost: TestWethTransformerHost as ContractArtifact, diff --git a/contracts/zero-ex/test/transformer_deployer_test.ts b/contracts/zero-ex/test/transformer_deployer_test.ts new file mode 100644 index 0000000000..c3d7e15658 --- /dev/null +++ b/contracts/zero-ex/test/transformer_deployer_test.ts @@ -0,0 +1,109 @@ +import { blockchainTests, constants, expect, randomAddress, verifyEventsFromLogs } from '@0x/contracts-test-utils'; +import { AuthorizableRevertErrors, BigNumber } from '@0x/utils'; + +import { artifacts } from './artifacts'; +import { + TestTransformerDeployerTransformerContract, + TransformerDeployerContract, + TransformerDeployerEvents, +} from './wrappers'; + +blockchainTests.resets('TransformerDeployer', env => { + let owner: string; + let authority: string; + let deployer: TransformerDeployerContract; + const deployBytes = artifacts.TestTransformerDeployerTransformer.compilerOutput.evm.bytecode.object; + + before(async () => { + [owner, authority] = await env.getAccountAddressesAsync(); + deployer = await TransformerDeployerContract.deployFrom0xArtifactAsync( + artifacts.TransformerDeployer, + env.provider, + env.txDefaults, + artifacts, + [authority], + ); + }); + + describe('deploy()', () => { + it('non-authority cannot call', async () => { + const nonAuthority = randomAddress(); + const tx = deployer.deploy(deployBytes).callAsync({ from: nonAuthority }); + return expect(tx).to.revertWith(new AuthorizableRevertErrors.SenderNotAuthorizedError(nonAuthority)); + }); + + it('authority can deploy', async () => { + const targetAddress = await deployer.deploy(deployBytes).callAsync({ from: authority }); + const target = new TestTransformerDeployerTransformerContract(targetAddress, env.provider); + const receipt = await deployer.deploy(deployBytes).awaitTransactionSuccessAsync({ from: authority }); + expect(await target.deployer().callAsync()).to.eq(deployer.address); + verifyEventsFromLogs( + receipt.logs, + [{ deployedAddress: targetAddress, nonce: new BigNumber(1), sender: authority }], + TransformerDeployerEvents.Deployed, + ); + }); + + it('authority can deploy with value', async () => { + const targetAddress = await deployer.deploy(deployBytes).callAsync({ from: authority, value: 1 }); + const target = new TestTransformerDeployerTransformerContract(targetAddress, env.provider); + const receipt = await deployer + .deploy(deployBytes) + .awaitTransactionSuccessAsync({ from: authority, value: 1 }); + expect(await target.deployer().callAsync()).to.eq(deployer.address); + verifyEventsFromLogs( + receipt.logs, + [{ deployedAddress: targetAddress, nonce: new BigNumber(1), sender: authority }], + TransformerDeployerEvents.Deployed, + ); + expect(await env.web3Wrapper.getBalanceInWeiAsync(targetAddress)).to.bignumber.eq(1); + }); + + it('updates nonce', async () => { + expect(await deployer.nonce().callAsync()).to.bignumber.eq(1); + await deployer.deploy(deployBytes).awaitTransactionSuccessAsync({ from: authority }); + expect(await deployer.nonce().callAsync()).to.bignumber.eq(2); + }); + + it('nonce can predict deployment address', async () => { + const nonce = await deployer.nonce().callAsync(); + const targetAddress = await deployer.deploy(deployBytes).callAsync({ from: authority }); + const target = new TestTransformerDeployerTransformerContract(targetAddress, env.provider); + await deployer.deploy(deployBytes).awaitTransactionSuccessAsync({ from: authority }); + expect(await target.isDeployedByDeployer(nonce).callAsync()).to.eq(true); + }); + + it('can retrieve deployment nonce from contract address', async () => { + const nonce = await deployer.nonce().callAsync(); + const targetAddress = await deployer.deploy(deployBytes).callAsync({ from: authority }); + await deployer.deploy(deployBytes).awaitTransactionSuccessAsync({ from: authority }); + expect(await deployer.toDeploymentNonce(targetAddress).callAsync()).to.bignumber.eq(nonce); + }); + }); + + describe('kill()', () => { + let target: TestTransformerDeployerTransformerContract; + + before(async () => { + const targetAddress = await deployer.deploy(deployBytes).callAsync({ from: authority }); + target = new TestTransformerDeployerTransformerContract(targetAddress, env.provider); + await deployer.deploy(deployBytes).awaitTransactionSuccessAsync({ from: authority }); + }); + + it('authority cannot call', async () => { + const nonAuthority = randomAddress(); + const tx = deployer.kill(target.address).callAsync({ from: nonAuthority }); + return expect(tx).to.revertWith(new AuthorizableRevertErrors.SenderNotAuthorizedError(nonAuthority)); + }); + + it('authority can kill a contract', async () => { + const receipt = await deployer.kill(target.address).awaitTransactionSuccessAsync({ from: authority }); + verifyEventsFromLogs( + receipt.logs, + [{ target: target.address, sender: authority }], + TransformerDeployerEvents.Killed, + ); + return expect(env.web3Wrapper.getContractCodeAsync(target.address)).to.become(constants.NULL_BYTES); + }); + }); +}); diff --git a/contracts/zero-ex/test/wrappers.ts b/contracts/zero-ex/test/wrappers.ts index 561f332dab..9df36914d7 100644 --- a/contracts/zero-ex/test/wrappers.ts +++ b/contracts/zero-ex/test/wrappers.ts @@ -55,6 +55,7 @@ export * from '../test/generated-wrappers/test_token_spender'; export * from '../test/generated-wrappers/test_token_spender_erc20_token'; export * from '../test/generated-wrappers/test_transform_erc20'; export * from '../test/generated-wrappers/test_transformer_base'; +export * from '../test/generated-wrappers/test_transformer_deployer_transformer'; export * from '../test/generated-wrappers/test_transformer_host'; export * from '../test/generated-wrappers/test_weth'; export * from '../test/generated-wrappers/test_weth_transformer_host'; @@ -62,5 +63,6 @@ export * from '../test/generated-wrappers/test_zero_ex_feature'; export * from '../test/generated-wrappers/token_spender'; export * from '../test/generated-wrappers/transform_erc20'; export * from '../test/generated-wrappers/transformer'; +export * from '../test/generated-wrappers/transformer_deployer'; export * from '../test/generated-wrappers/weth_transformer'; export * from '../test/generated-wrappers/zero_ex'; diff --git a/contracts/zero-ex/tsconfig.json b/contracts/zero-ex/tsconfig.json index 17110f5474..7bc3fb6e6a 100644 --- a/contracts/zero-ex/tsconfig.json +++ b/contracts/zero-ex/tsconfig.json @@ -68,6 +68,7 @@ "test/generated-artifacts/TestTokenSpenderERC20Token.json", "test/generated-artifacts/TestTransformERC20.json", "test/generated-artifacts/TestTransformerBase.json", + "test/generated-artifacts/TestTransformerDeployerTransformer.json", "test/generated-artifacts/TestTransformerHost.json", "test/generated-artifacts/TestWeth.json", "test/generated-artifacts/TestWethTransformerHost.json", @@ -75,6 +76,7 @@ "test/generated-artifacts/TokenSpender.json", "test/generated-artifacts/TransformERC20.json", "test/generated-artifacts/Transformer.json", + "test/generated-artifacts/TransformerDeployer.json", "test/generated-artifacts/WethTransformer.json", "test/generated-artifacts/ZeroEx.json" ],