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..3e3243605c --- /dev/null +++ b/contracts/zero-ex/contracts/src/external/TransformerDeployer.sol @@ -0,0 +1,82 @@ +/* + + 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 "@0x/contracts-utils/contracts/src/v06/AuthorizableV06.sol"; + + +/// @dev A contract with a `die()` function. +interface IKillable { + function die() external; +} + +/// @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()`. + /// @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()`. + /// @param target The address of the contract being killed.. + /// @param sender The caller of `kill()`. + event Killed(address target, address sender); + + // @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 Deploy a new contract. Only callable by an authority. + /// Any attached ETH will also be forwarded. + function deploy(bytes memory bytecode) + public + payable + onlyAuthorized + returns (address deployedAddress) + { + uint256 deploymentNonce = nonce; + nonce += 1; + assembly { + deployedAddress := create(callvalue(), add(bytecode, 32), mload(bytecode)) + } + toDeploymentNonce[deployedAddress] = deploymentNonce; + emit Deployed(deployedAddress, deploymentNonce, msg.sender); + } + + /// @dev Call `die()` on a contract. Only callable by an authority. + function kill(IKillable target) + public + onlyAuthorized + { + target.die(); + 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" ],