@0x/contracts-zero-ex: Make TransformerDeployer boring.

This commit is contained in:
Lawrence Forman
2020-06-06 00:30:30 -04:00
parent 0fed48630c
commit 87ed0071c4
7 changed files with 203 additions and 178 deletions

View File

@@ -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);
}
}

View File

@@ -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);
}
}

View File

@@ -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",

View File

@@ -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,

View File

@@ -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);
});
});
});

View File

@@ -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';

View File

@@ -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"
],