@0x/contracts-zero-ex: Address audit feedback.

This commit is contained in:
Lawrence Forman 2020-06-04 16:15:38 -04:00
parent cb2cc05cac
commit ebfa62637e
26 changed files with 258 additions and 321 deletions

View File

@ -70,31 +70,20 @@ library LibTransformERC20RichErrors {
);
}
function UnauthorizedTransformerError(
function TransformerFailedError(
address transformer,
bytes memory rlpNonce
bytes memory transformerData,
bytes memory resultData
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("UnauthorizedTransformerError(address,bytes)")),
bytes4(keccak256("TransformerFailedError(address,bytes,bytes)")),
transformer,
rlpNonce
);
}
function InvalidRLPNonceError(
bytes memory rlpNonce
)
internal
pure
returns (bytes memory)
{
return abi.encodeWithSelector(
bytes4(keccak256("InvalidRLPNonceError(bytes)")),
rlpNonce
transformerData,
resultData
);
}

View File

@ -42,7 +42,6 @@ contract AllowanceTarget is
bytes calldata callData
)
external
payable
override
onlyAuthorized
returns (bytes memory resultData)

View File

@ -35,6 +35,5 @@ interface IAllowanceTarget is
bytes calldata callData
)
external
payable
returns (bytes memory resultData);
}

View File

@ -29,9 +29,10 @@ interface ITransformERC20 {
/// @dev Defines a transformation to run in `transformERC20()`.
struct Transformation {
// The transformation handler.
// Can receive the entire balance of `tokens`.
IERC20Transformer transformer;
// The deployment nonce for the transformer.
// The address of the transformer contract will be derived from this
// value.
uint32 deploymentNonce;
// Arbitrary data to pass to the transformer.
bytes data;
}
@ -52,6 +53,15 @@ interface ITransformERC20 {
uint256 outputTokenAmount
);
/// @dev Raised when `setTransformerDeployer()` is called.
/// @param transformerDeployer The new deployer address.
event TransformerDeployerUpdated(address transformerDeployer);
/// @dev Replace the allowed deployer for transformers.
/// Only callable by the owner.
function setTransformerDeployer(address transformerDeployer)
external;
/// @dev Deploy a new flash wallet instance and replace the current one with it.
/// Useful if we somehow break the current wallet instance.
/// Anyone can call this.

View File

@ -44,13 +44,19 @@ contract TransformERC20 is
FixinCommon
{
/// @dev Stack vars for `_transformERC20Private()`.
struct TransformERC20PrivateState {
IFlashWallet wallet;
address transformerDeployer;
uint256 takerOutputTokenBalanceBefore;
uint256 takerOutputTokenBalanceAfter;
}
// solhint-disable
/// @dev Name of this feature.
string public constant override FEATURE_NAME = "TransformERC20";
/// @dev Version of this feature.
uint256 public immutable override FEATURE_VERSION = _encodeVersion(1, 0, 0);
/// @dev The trusted deployer for all transformers.
address public immutable transformDeployer;
/// @dev The implementation address of this feature.
address private immutable _implementation;
// solhint-enable
@ -58,37 +64,51 @@ contract TransformERC20 is
using LibSafeMathV06 for uint256;
using LibRichErrorsV06 for bytes;
constructor(address trustedDeployer_) public {
constructor() public {
_implementation = address(this);
transformDeployer = trustedDeployer_;
}
/// @dev Initialize and register this feature.
/// Should be delegatecalled by `Migrate.migrate()`.
function migrate() external returns (bytes4 success) {
/// @param transformerDeployer The trusted deployer for transformers.
function migrate(address transformerDeployer) external returns (bytes4 success) {
ISimpleFunctionRegistry(address(this))
.extend(this.getTransformerDeployer.selector, _implementation);
ISimpleFunctionRegistry(address(this))
.extend(this.createTransformWallet.selector, _implementation);
ISimpleFunctionRegistry(address(this))
.extend(this.getTransformWallet.selector, _implementation);
ISimpleFunctionRegistry(address(this))
.extend(this.setTransformerDeployer.selector, _implementation);
ISimpleFunctionRegistry(address(this))
.extend(this.transformERC20.selector, _implementation);
ISimpleFunctionRegistry(address(this))
.extend(this._transformERC20.selector, _implementation);
createTransformWallet();
LibTransformERC20Storage.getStorage().transformerDeployer = transformerDeployer;
return LibMigrate.MIGRATE_SUCCESS;
}
/// @dev Replace the allowed deployer for transformers.
/// Only callable by the owner.
function setTransformerDeployer(address transformerDeployer)
external
override
onlyOwner
{
LibTransformERC20Storage.getStorage().transformerDeployer = transformerDeployer;
emit TransformerDeployerUpdated(transformerDeployer);
}
/// @dev Return the allowed deployer for transformers.
/// @return deployer The transform deployer address.
function getTransformerDeployer()
external
public
override
view
returns (address deployer)
{
return transformDeployer;
return LibTransformERC20Storage.getStorage().transformerDeployer;
}
/// @dev Deploy a new wallet instance and replace the current one with it.
@ -209,8 +229,7 @@ contract TransformERC20 is
uint256 minOutputTokenAmount,
Transformation[] memory transformations
)
public
payable
private
returns (uint256 outputTokenAmount)
{
// If the input token amount is -1, transform the taker's entire
@ -220,31 +239,44 @@ contract TransformERC20 is
.getSpendableERC20BalanceOf(inputToken, taker);
}
IFlashWallet wallet = getTransformWallet();
TransformERC20PrivateState memory state;
state.wallet = getTransformWallet();
state.transformerDeployer = getTransformerDeployer();
// Remember the initial output token balance of the taker.
uint256 takerOutputTokenBalanceBefore =
state.takerOutputTokenBalanceBefore =
LibERC20Transformer.getTokenBalanceOf(outputToken, taker);
// Pull input tokens from the taker to the wallet and transfer attached ETH.
_transferInputTokensAndAttachedEth(inputToken, taker, address(wallet), inputTokenAmount);
_transferInputTokensAndAttachedEth(
inputToken,
taker,
address(state.wallet),
inputTokenAmount
);
// Perform transformations.
for (uint256 i = 0; i < transformations.length; ++i) {
_executeTransformation(wallet, transformations[i], taker, callDataHash);
_executeTransformation(
state.wallet,
transformations[i],
state.transformerDeployer,
taker,
callDataHash
);
}
// Compute how much output token has been transferred to the taker.
uint256 takerOutputTokenBalanceAfter =
state.takerOutputTokenBalanceAfter =
LibERC20Transformer.getTokenBalanceOf(outputToken, taker);
if (takerOutputTokenBalanceAfter > takerOutputTokenBalanceBefore) {
outputTokenAmount = takerOutputTokenBalanceAfter.safeSub(
takerOutputTokenBalanceBefore
if (state.takerOutputTokenBalanceAfter > state.takerOutputTokenBalanceBefore) {
outputTokenAmount = state.takerOutputTokenBalanceAfter.safeSub(
state.takerOutputTokenBalanceBefore
);
} else if (takerOutputTokenBalanceAfter < takerOutputTokenBalanceBefore) {
} else if (state.takerOutputTokenBalanceAfter < state.takerOutputTokenBalanceBefore) {
LibTransformERC20RichErrors.NegativeTransformERC20OutputError(
address(outputToken),
takerOutputTokenBalanceBefore - takerOutputTokenBalanceAfter
state.takerOutputTokenBalanceBefore - state.takerOutputTokenBalanceAfter
).rrevert();
}
// Ensure enough output token has been sent to the taker.
@ -316,20 +348,27 @@ contract TransformERC20 is
/// @dev Executs a transformer in the context of `wallet`.
/// @param wallet The wallet instance.
/// @param transformation The transformation.
/// @param transformerDeployer The address of the transformer deployer.
/// @param taker The taker address.
/// @param callDataHash Hash of the calldata.
function _executeTransformation(
IFlashWallet wallet,
Transformation memory transformation,
address transformerDeployer,
address payable taker,
bytes32 callDataHash
)
private
{
// Derive the transformer address from the deployment nonce.
address payable transformer = LibERC20Transformer.getDeployedAddress(
transformerDeployer,
transformation.deploymentNonce
);
// Call `transformer.transform()` as the wallet.
bytes memory resultData = wallet.executeDelegateCall(
// Call target.
address(uint160(address(transformation.transformer))),
// The call target.
transformer,
// Call data.
abi.encodeWithSelector(
IERC20Transformer.transform.selector,
@ -338,39 +377,15 @@ contract TransformERC20 is
transformation.data
)
);
// Ensure the transformer returned its valid rlp-encoded deployment nonce.
bytes memory rlpNonce = resultData.length == 0
? new bytes(0)
: abi.decode(resultData, (bytes));
if (_getExpectedDeployment(rlpNonce) != address(transformation.transformer)) {
LibTransformERC20RichErrors.UnauthorizedTransformerError(
address(transformation.transformer),
rlpNonce
// Ensure the transformer returned the magic bytes.
if (resultData.length != 32 ||
abi.decode(resultData, (bytes4)) != LibERC20Transformer.TRANSFORMER_SUCCESS
) {
LibTransformERC20RichErrors.TransformerFailedError(
transformer,
transformation.data,
resultData
).rrevert();
}
}
/// @dev Compute the expected deployment address by `transformDeployer` at
/// the nonce given by `rlpNonce`.
/// @param rlpNonce The RLP-encoded nonce that
/// the deployer had when deploying a contract.
/// @return deploymentAddress The deployment address.
function _getExpectedDeployment(bytes memory rlpNonce)
private
view
returns (address deploymentAddress)
{
// See https://github.com/ethereum/wiki/wiki/RLP for RLP encoding rules.
// The RLP-encoded nonce may be prefixed with a length byte.
// We only support nonces up to 32-bits.
if (rlpNonce.length == 0 || rlpNonce.length > 5) {
LibTransformERC20RichErrors.InvalidRLPNonceError(rlpNonce).rrevert();
}
return address(uint160(uint256(keccak256(abi.encodePacked(
byte(uint8(0xC0 + 21 + rlpNonce.length)),
byte(uint8(0x80 + 20)),
transformDeployer,
rlpNonce
)))));
}
}

View File

@ -40,6 +40,11 @@ contract FullMigration {
TransformERC20 transformERC20;
}
/// @dev Parameters needed to initialize features.
struct MigrateOpts {
address transformerDeployer;
}
/// @dev The allowed caller of `deploy()`.
address public immutable deployer;
/// @dev The initial migration contract.
@ -62,9 +67,11 @@ contract FullMigration {
/// @param owner The owner of the contract.
/// @param features Features to add to the proxy.
/// @return zeroEx The deployed and configured `ZeroEx` contract.
/// @param migrateOpts Parameters needed to initialize features.
function deploy(
address payable owner,
Features memory features
Features memory features,
MigrateOpts memory migrateOpts
)
public
returns (ZeroEx zeroEx)
@ -81,7 +88,7 @@ contract FullMigration {
);
// Add features.
_addFeatures(zeroEx, owner, features);
_addFeatures(zeroEx, owner, features, migrateOpts);
// Transfer ownership to the real owner.
IOwnable(address(zeroEx)).transferOwnership(owner);
@ -106,10 +113,12 @@ contract FullMigration {
/// @param zeroEx The bootstrapped ZeroEx contract.
/// @param owner The ultimate owner of the ZeroEx contract.
/// @param features Features to add to the proxy.
/// @param migrateOpts Parameters needed to initialize features.
function _addFeatures(
ZeroEx zeroEx,
address owner,
Features memory features
Features memory features,
MigrateOpts memory migrateOpts
)
private
{
@ -138,7 +147,8 @@ contract FullMigration {
ownable.migrate(
address(features.transformERC20),
abi.encodeWithSelector(
TransformERC20.migrate.selector
TransformERC20.migrate.selector,
migrateOpts.transformerDeployer
),
address(this)
);

View File

@ -30,6 +30,8 @@ library LibTransformERC20Storage {
struct Storage {
// The current wallet instance.
IFlashWallet wallet;
// The transformer deployer address.
address transformerDeployer;
}
/// @dev Get the storage bucket for this contract.

View File

@ -93,10 +93,9 @@ contract FillQuoteTransformer is
/// @dev Create this contract.
/// @param exchange_ The Exchange V3 instance.
/// @param deploymentNonce_ The nonce of the deployer when deploying this contract.
constructor(IExchange exchange_, uint256 deploymentNonce_)
constructor(IExchange exchange_)
public
Transformer(deploymentNonce_)
Transformer()
{
exchange = exchange_;
erc20Proxy = exchange_.getAssetProxy(ERC20_ASSET_PROXY_ID);
@ -106,9 +105,7 @@ contract FillQuoteTransformer is
/// for `buyToken` by filling `orders`. Protocol fees should be attached
/// to this call. `buyToken` and excess ETH will be transferred back to the caller.
/// @param data_ ABI-encoded `TransformData`.
/// @return rlpDeploymentNonce RLP-encoded deployment nonce of the deployer
/// when this transformer was deployed. This is used to verify that
/// this transformer was deployed by a trusted contract.
/// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`).
function transform(
bytes32, // callDataHash,
address payable, // taker,
@ -116,7 +113,7 @@ contract FillQuoteTransformer is
)
external
override
returns (bytes memory rlpDeploymentNonce)
returns (bytes4 success)
{
TransformData memory data = abi.decode(data_, (TransformData));
@ -231,7 +228,7 @@ contract FillQuoteTransformer is
).rrevert();
}
}
return _getRLPEncodedDeploymentNonce();
return LibERC20Transformer.TRANSFORMER_SUCCESS;
}
/// @dev Try to sell up to `sellAmount` from an order.

View File

@ -30,14 +30,12 @@ interface IERC20Transformer {
/// @param callDataHash The hash of the `TransformERC20.transformERC20()` calldata.
/// @param taker The taker address (caller of `TransformERC20.transformERC20()`).
/// @param data Arbitrary data to pass to the transformer.
/// @return rlpDeploymentNonce RLP-encoded deployment nonce of the deployer
/// when this transformer was deployed. This is used to verify that
/// this transformer was deployed by a trusted contract.
/// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`).
function transform(
bytes32 callDataHash,
address payable taker,
bytes calldata data
)
external
returns (bytes memory rlpDeploymentNonce);
returns (bytes4 success);
}

View File

@ -29,6 +29,9 @@ library LibERC20Transformer {
/// @dev ETH pseudo-token address.
address constant internal ETH_TOKEN_ADDRESS = 0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE;
/// @dev Return value indicating success in `IERC20Transformer.transform()`.
/// This is just `keccak256('TRANSFORMER_SUCCESS')`.
bytes4 constant internal TRANSFORMER_SUCCESS = 0x13c9929e;
/// @dev Transfer ERC20 tokens and ETH.
/// @param token An ERC20 or the ETH pseudo-token address (`ETH_TOKEN_ADDRESS`).
@ -68,17 +71,21 @@ library LibERC20Transformer {
view
returns (uint256 tokenBalance)
{
return isTokenETH(token) ? owner.balance : token.balanceOf(owner);
if (isTokenETH(token)) {
return owner.balance
}
return token.balanceOf(owner);
}
/// @dev RLP-encode a 32-bit or less account nonce.
/// @param nonce A positive integer in the range 0 <= nonce < 2^32.
/// @return rlpNonce The RLP encoding.
function rlpEncodeNonce(uint256 nonce)
function rlpEncodeNonce(uint32 nonce)
internal
pure
returns (bytes memory rlpNonce)
{
// See https://github.com/ethereum/wiki/wiki/RLP for RLP encoding rules.
if (nonce == 0) {
rlpNonce = new bytes(1);
rlpNonce[0] = 0x80;
@ -100,15 +107,36 @@ library LibERC20Transformer {
rlpNonce[1] = byte(uint8((nonce & 0xFF0000) >> 16));
rlpNonce[2] = byte(uint8((nonce & 0xFF00) >> 8));
rlpNonce[3] = byte(uint8(nonce));
} else if (nonce <= 0xFFFFFFFF) {
} else {
rlpNonce = new bytes(5);
rlpNonce[0] = 0x84;
rlpNonce[1] = byte(uint8((nonce & 0xFF000000) >> 24));
rlpNonce[2] = byte(uint8((nonce & 0xFF0000) >> 16));
rlpNonce[3] = byte(uint8((nonce & 0xFF00) >> 8));
rlpNonce[4] = byte(uint8(nonce));
} else {
revert("LibERC20Transformer/INVALID_ENCODE_NONCE");
}
}
/// @dev Compute the expected deployment address by `deployer` at
/// the nonce given by `deploymentNonce`.
/// @param deployer The address of the deployer.
/// @param deploymentNonce The nonce that the deployer had when deploying
/// a contract.
/// @return deploymentAddress The deployment address.
function getDeployedAddress(address deployer, uint32 deploymentNonce)
internal
pure
returns (address payable deploymentAddress)
{
// The address of if a deployed contract is the lower 20 bytes of the
// hash of the RLP-encoded deployer's account address + account nonce.
// See: https://ethereum.stackexchange.com/questions/760/how-is-the-address-of-an-ethereum-contract-computed
bytes memory rlpNonce = rlpEncodeNonce(deploymentNonce);
return address(uint160(uint256(keccak256(abi.encodePacked(
byte(uint8(0xC0 + 21 + rlpNonce.length)),
byte(uint8(0x80 + 20)),
deployer,
rlpNonce
)))));
}
}

View File

@ -50,18 +50,15 @@ contract PayTakerTransformer is
uint256 private constant MAX_UINT256 = uint256(-1);
/// @dev Create this contract.
/// @param deploymentNonce_ The nonce of the deployer when deploying this contract.
constructor(uint256 deploymentNonce_)
constructor()
public
Transformer(deploymentNonce_)
Transformer()
{}
/// @dev Forwards tokens to the taker.
/// @param taker The taker address (caller of `TransformERC20.transformERC20()`).
/// @param data_ ABI-encoded `TransformData`, indicating which tokens to transfer.
/// @return rlpDeploymentNonce RLP-encoded deployment nonce of the deployer
/// when this transformer was deployed. This is used to verify that
/// this transformer was deployed by a trusted contract.
/// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`).
function transform(
bytes32, // callDataHash,
address payable taker,
@ -69,7 +66,7 @@ contract PayTakerTransformer is
)
external
override
returns (bytes memory rlpDeploymentNonce)
returns (bytes4 success)
{
TransformData memory data = abi.decode(data_, (TransformData));
@ -85,6 +82,6 @@ contract PayTakerTransformer is
data.tokens[i].transformerTransfer(taker, amount);
}
}
return _getRLPEncodedDeploymentNonce();
return LibERC20Transformer.TRANSFORMER_SUCCESS;
}
}

View File

@ -22,7 +22,6 @@ pragma experimental ABIEncoderV2;
import "@0x/contracts-utils/contracts/src/v06/errors/LibRichErrorsV06.sol";
import "../errors/LibTransformERC20RichErrors.sol";
import "./IERC20Transformer.sol";
import "./LibERC20Transformer.sol";
/// @dev Abstract base class for transformers.
@ -33,15 +32,11 @@ abstract contract Transformer is
/// @dev The address of the deployer.
address public immutable deployer;
/// @dev The nonce of the deployer when deploying this contract.
uint256 public immutable deploymentNonce;
/// @dev The original address of this contract.
address private immutable _implementation;
/// @dev Create this contract.
/// @param deploymentNonce_ The nonce of the deployer when deploying this contract.
constructor(uint256 deploymentNonce_) public {
deploymentNonce = deploymentNonce_;
constructor() public {
deployer = msg.sender;
_implementation = address(this);
}
@ -67,14 +62,4 @@ abstract contract Transformer is
}
selfdestruct(ethRecipient);
}
/// @dev Get the RLP-encoded deployment nonce of this contract.
/// @return rlpEncodedNonce The RLP-encoded deployment nonce.
function _getRLPEncodedDeploymentNonce()
internal
view
returns (bytes memory rlpEncodedNonce)
{
return LibERC20Transformer.rlpEncodeNonce(deploymentNonce);
}
}

View File

@ -51,19 +51,16 @@ contract WethTransformer is
/// @dev Construct the transformer and store the WETH address in an immutable.
/// @param weth_ The weth token.
/// @param deploymentNonce_ The nonce of the deployer when deploying this contract.
constructor(IEtherTokenV06 weth_, uint256 deploymentNonce_)
constructor(IEtherTokenV06 weth_)
public
Transformer(deploymentNonce_)
Transformer()
{
weth = weth_;
}
/// @dev Wraps and unwraps WETH.
/// @param data_ ABI-encoded `TransformData`, indicating which token to wrap/umwrap.
/// @return rlpDeploymentNonce RLP-encoded deployment nonce of the deployer
/// when this transformer was deployed. This is used to verify that
/// this transformer was deployed by a trusted contract.
/// @return success The success bytes (`LibERC20Transformer.TRANSFORMER_SUCCESS`).
function transform(
bytes32, // callDataHash,
address payable, // taker,
@ -71,7 +68,7 @@ contract WethTransformer is
)
external
override
returns (bytes memory rlpDeploymentNonce)
returns (bytes4 success)
{
TransformData memory data = abi.decode(data_, (TransformData));
if (!data.token.isTokenETH() && data.token != weth) {
@ -95,6 +92,6 @@ contract WethTransformer is
weth.withdraw(amount);
}
}
return _getRLPEncodedDeploymentNonce();
return LibERC20Transformer.TRANSFORMER_SUCCESS;
}
}

View File

@ -35,12 +35,11 @@ contract TestFillQuoteTransformerHost is
)
external
payable
returns (bytes memory rlpDeploymentNonce)
{
if (inputTokenAmount != 0) {
inputToken.mint(address(this), inputTokenAmount);
}
// Have to make this call externally because transformers aren't payable.
return this.rawExecuteTransform(transformer, bytes32(0), msg.sender, data);
this.rawExecuteTransform(transformer, bytes32(0), msg.sender, data);
}
}

View File

@ -34,7 +34,6 @@ contract TestMintTokenERC20Transformer is
uint256 burnAmount;
uint256 mintAmount;
uint256 feeAmount;
bytes deploymentNonce;
}
event MintTransform(
@ -54,7 +53,7 @@ contract TestMintTokenERC20Transformer is
)
external
override
returns (bytes memory rlpDeploymentNonce)
returns (bytes4 success)
{
TransformData memory data = abi.decode(data_, (TransformData));
emit MintTransform(
@ -79,6 +78,6 @@ contract TestMintTokenERC20Transformer is
// Burn fees from output.
data.outputToken.burn(taker, data.feeAmount);
}
return data.deploymentNonce;
return LibERC20Transformer.TRANSFORMER_SUCCESS;
}
}

View File

@ -26,8 +26,8 @@ contract TestTransformERC20 is
TransformERC20
{
// solhint-disable no-empty-blocks
constructor(address trustedDeployer)
TransformERC20(trustedDeployer)
constructor()
TransformERC20()
public
{}

View File

@ -20,17 +20,12 @@ pragma solidity ^0.6.5;
pragma experimental ABIEncoderV2;
import "../src/transformers/Transformer.sol";
import "../src/transformers/LibERC20Transformer.sol";
contract TestTransformerBase is
Transformer
{
// solhint-disable no-empty-blocks
constructor(uint256 deploymentNonce_)
public
Transformer(deploymentNonce_)
{}
function transform(
bytes32,
address payable,
@ -38,16 +33,8 @@ contract TestTransformerBase is
)
external
override
returns (bytes memory rlpDeploymentNonce)
returns (bytes4 success)
{
return hex"";
}
function getRLPEncodedDeploymentNonce()
external
view
returns (bytes memory)
{
return _getRLPEncodedDeploymentNonce();
return LibERC20Transformer.TRANSFORMER_SUCCESS;
}
}

View File

@ -37,19 +37,21 @@ contract TestTransformerHost {
bytes calldata data
)
external
returns (bytes memory)
{
(bool success, bytes memory resultData) =
(bool _success, bytes memory resultData) =
address(transformer).delegatecall(abi.encodeWithSelector(
transformer.transform.selector,
callDataHash,
taker,
data
));
if (!success) {
if (!_success) {
resultData.rrevert();
}
assembly { return(add(resultData, 32), mload(resultData)) }
require(
abi.decode(resultData, (bytes4)) == LibERC20Transformer.TRANSFORMER_SUCCESS,
"TestTransformerHost/INVALID_TRANSFORMER_RESULT"
);
}
// solhint-disable

View File

@ -43,12 +43,11 @@ contract TestWethTransformerHost is
)
external
payable
returns (bytes memory rlpDeploymentNonce)
{
if (wethAmount != 0) {
_weth.deposit{value: wethAmount}();
}
// Have to make this call externally because transformers aren't payable.
return this.rawExecuteTransform(transformer, bytes32(0), msg.sender, data);
this.rawExecuteTransform(transformer, bytes32(0), msg.sender, data);
}
}

View File

@ -8,10 +8,9 @@ import {
randomAddress,
verifyEventsFromLogs,
} from '@0x/contracts-test-utils';
import { AbiEncoder, hexUtils, ZeroExRevertErrors } from '@0x/utils';
import { AbiEncoder, hexUtils, OwnableRevertErrors, ZeroExRevertErrors } from '@0x/utils';
import { ETH_TOKEN_ADDRESS } from '../../src/constants';
import { getRLPEncodedAccountNonceAsync } from '../../src/nonce_utils';
import { artifacts } from '../artifacts';
import { abis } from '../utils/abis';
import { fullMigrateAsync } from '../utils/migration';
@ -27,6 +26,7 @@ import {
} from '../wrappers';
blockchainTests.resets('TransformERC20 feature', env => {
let owner: string;
let taker: string;
let transformerDeployer: string;
let zeroEx: ZeroExContract;
@ -35,17 +35,21 @@ blockchainTests.resets('TransformERC20 feature', env => {
let allowanceTarget: string;
before(async () => {
let owner;
[owner, taker, transformerDeployer] = await env.getAccountAddressesAsync();
zeroEx = await fullMigrateAsync(owner, env.provider, env.txDefaults, {
transformERC20: await TransformERC20Contract.deployFrom0xArtifactAsync(
artifacts.TestTransformERC20,
env.provider,
env.txDefaults,
artifacts,
transformerDeployer,
),
});
zeroEx = await fullMigrateAsync(
owner,
env.provider,
env.txDefaults,
{
transformERC20: await TransformERC20Contract.deployFrom0xArtifactAsync(
artifacts.TestTransformERC20,
env.provider,
env.txDefaults,
artifacts,
),
},
{ transformerDeployer },
);
feature = new TransformERC20Contract(zeroEx.address, env.provider, env.txDefaults, abis);
wallet = new FlashWalletContract(await feature.getTransformWallet().callAsync(), env.provider, env.txDefaults);
allowanceTarget = await new ITokenSpenderContract(zeroEx.address, env.provider, env.txDefaults)
@ -53,7 +57,7 @@ blockchainTests.resets('TransformERC20 feature', env => {
.callAsync();
});
const { MAX_UINT256, NULL_BYTES, ZERO_AMOUNT } = constants;
const { MAX_UINT256, ZERO_AMOUNT } = constants;
describe('wallets', () => {
it('createTransformWallet() replaces the current wallet', async () => {
@ -64,11 +68,39 @@ blockchainTests.resets('TransformERC20 feature', env => {
});
});
describe('transformer deployer', () => {
it('`getTransformerDeployer()` returns the transformer deployer', async () => {
const actualDeployer = await feature.getTransformerDeployer().callAsync();
expect(actualDeployer).to.eq(transformerDeployer);
});
it('owner can set the transformer deployer with `setTransformerDeployer()`', async () => {
const newDeployer = randomAddress();
const receipt = await feature
.setTransformerDeployer(newDeployer)
.awaitTransactionSuccessAsync({ from: owner });
verifyEventsFromLogs(
receipt.logs,
[{ transformerDeployer: newDeployer }],
TransformERC20Events.TransformerDeployerUpdated,
);
const actualDeployer = await feature.getTransformerDeployer().callAsync();
expect(actualDeployer).to.eq(newDeployer);
});
it('non-owner cannot set the transformer deployer with `setTransformerDeployer()`', async () => {
const newDeployer = randomAddress();
const notOwner = randomAddress();
const tx = feature.setTransformerDeployer(newDeployer).callAsync({ from: notOwner });
return expect(tx).to.revertWith(new OwnableRevertErrors.OnlyOwnerError(notOwner, owner));
});
});
describe('_transformERC20()', () => {
let inputToken: TestMintableERC20TokenContract;
let outputToken: TestMintableERC20TokenContract;
let mintTransformer: TestMintTokenERC20TransformerContract;
let rlpNonce: string;
let transformerNonce: number;
before(async () => {
inputToken = await TestMintableERC20TokenContract.deployFrom0xArtifactAsync(
@ -83,7 +115,7 @@ blockchainTests.resets('TransformERC20 feature', env => {
env.txDefaults,
artifacts,
);
rlpNonce = await getRLPEncodedAccountNonceAsync(env.web3Wrapper, transformerDeployer);
transformerNonce = await env.web3Wrapper.getAccountNonceAsync(transformerDeployer);
mintTransformer = await TestMintTokenERC20TransformerContract.deployFrom0xArtifactAsync(
artifacts.TestMintTokenERC20Transformer,
env.provider,
@ -97,7 +129,7 @@ blockchainTests.resets('TransformERC20 feature', env => {
});
interface Transformation {
transformer: string;
deploymentNonce: number;
data: string;
}
@ -111,7 +143,6 @@ blockchainTests.resets('TransformERC20 feature', env => {
{ name: 'burnAmount', type: 'uint256' },
{ name: 'mintAmount', type: 'uint256' },
{ name: 'feeAmount', type: 'uint256' },
{ name: 'deploymentNonce', type: 'bytes' },
],
},
]);
@ -124,21 +155,21 @@ blockchainTests.resets('TransformERC20 feature', env => {
inputTokenBurnAmunt: Numberish;
outputTokenMintAmount: Numberish;
outputTokenFeeAmount: Numberish;
rlpNonce: string;
deploymentNonce: number;
}> = {},
): Transformation {
const _opts = {
rlpNonce,
outputTokenAddress: outputToken.address,
inputTokenAddress: inputToken.address,
inputTokenBurnAmunt: ZERO_AMOUNT,
outputTokenMintAmount: ZERO_AMOUNT,
outputTokenFeeAmount: ZERO_AMOUNT,
transformer: mintTransformer.address,
deploymentNonce: transformerNonce,
...opts,
};
return {
transformer: _opts.transformer,
deploymentNonce: _opts.deploymentNonce,
data: transformDataEncoder.encode([
{
inputToken: _opts.inputTokenAddress,
@ -146,7 +177,6 @@ blockchainTests.resets('TransformERC20 feature', env => {
burnAmount: _opts.inputTokenBurnAmunt,
mintAmount: _opts.outputTokenMintAmount,
feeAmount: _opts.outputTokenFeeAmount,
deploymentNonce: _opts.rlpNonce,
},
]),
};
@ -443,7 +473,7 @@ blockchainTests.resets('TransformERC20 feature', env => {
);
});
it('fails with third-party transformer', async () => {
it('fails with invalid transformer nonce', async () => {
const startingOutputTokenBalance = getRandomInteger(0, '100e18');
const startingInputTokenBalance = getRandomInteger(2, '100e18');
await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
@ -452,32 +482,7 @@ blockchainTests.resets('TransformERC20 feature', env => {
const minOutputTokenAmount = getRandomInteger(2, '1e18');
const callValue = getRandomInteger(1, '1e18');
const callDataHash = hexUtils.random();
const transformations = [createMintTokenTransformation({ transformer: randomAddress() })];
const tx = feature
._transformERC20(
callDataHash,
taker,
inputToken.address,
outputToken.address,
inputTokenAmount,
minOutputTokenAmount,
transformations,
)
.awaitTransactionSuccessAsync({ value: callValue });
return expect(tx).to.revertWith(new ZeroExRevertErrors.TransformERC20.InvalidRLPNonceError(NULL_BYTES));
});
it('fails with incorrect transformer RLP nonce', async () => {
const startingOutputTokenBalance = getRandomInteger(0, '100e18');
const startingInputTokenBalance = getRandomInteger(2, '100e18');
await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
const minOutputTokenAmount = getRandomInteger(2, '1e18');
const callValue = getRandomInteger(1, '1e18');
const callDataHash = hexUtils.random();
const badRlpNonce = '0x00';
const transformations = [createMintTokenTransformation({ rlpNonce: badRlpNonce })];
const transformations = [createMintTokenTransformation({ deploymentNonce: 1337 })];
const tx = feature
._transformERC20(
callDataHash,
@ -490,36 +495,12 @@ blockchainTests.resets('TransformERC20 feature', env => {
)
.awaitTransactionSuccessAsync({ value: callValue });
return expect(tx).to.revertWith(
new ZeroExRevertErrors.TransformERC20.UnauthorizedTransformerError(
transformations[0].transformer,
badRlpNonce,
new ZeroExRevertErrors.TransformERC20.TransformerFailedError(
undefined,
transformations[0].data,
constants.NULL_BYTES,
),
);
});
it('fails with invalid transformer RLP nonce', async () => {
const startingOutputTokenBalance = getRandomInteger(0, '100e18');
const startingInputTokenBalance = getRandomInteger(2, '100e18');
await outputToken.mint(taker, startingOutputTokenBalance).awaitTransactionSuccessAsync();
await inputToken.mint(taker, startingInputTokenBalance).awaitTransactionSuccessAsync();
const inputTokenAmount = getRandomPortion(startingInputTokenBalance);
const minOutputTokenAmount = getRandomInteger(2, '1e18');
const callValue = getRandomInteger(1, '1e18');
const callDataHash = hexUtils.random();
const badRlpNonce = '0x010203040506';
const transformations = [createMintTokenTransformation({ rlpNonce: badRlpNonce })];
const tx = feature
._transformERC20(
callDataHash,
taker,
inputToken.address,
outputToken.address,
inputTokenAmount,
minOutputTokenAmount,
transformations,
)
.awaitTransactionSuccessAsync({ value: callValue });
return expect(tx).to.revertWith(new ZeroExRevertErrors.TransformERC20.InvalidRLPNonceError(badRlpNonce));
});
});
});

View File

@ -23,6 +23,7 @@ blockchainTests.resets('Full migration', env => {
let zeroEx: ZeroExContract;
let features: FullFeatures;
let migrator: TestFullMigrationContract;
const transformerDeployer = randomAddress();
before(async () => {
[owner] = await env.getAccountAddressesAsync();
@ -34,7 +35,7 @@ blockchainTests.resets('Full migration', env => {
artifacts,
env.txDefaults.from as string,
);
const deployCall = migrator.deploy(owner, toFeatureAdddresses(features));
const deployCall = migrator.deploy(owner, toFeatureAdddresses(features), { transformerDeployer });
zeroEx = new ZeroExContract(await deployCall.callAsync(), env.provider, env.txDefaults);
await deployCall.awaitTransactionSuccessAsync();
});
@ -52,7 +53,9 @@ blockchainTests.resets('Full migration', env => {
it('Non-deployer cannot call deploy()', async () => {
const notDeployer = randomAddress();
const tx = migrator.deploy(owner, toFeatureAdddresses(features)).callAsync({ from: notDeployer });
const tx = migrator
.deploy(owner, toFeatureAdddresses(features), { transformerDeployer })
.callAsync({ from: notDeployer });
return expect(tx).to.revertWith('FullMigration/INVALID_SENDER');
});
@ -63,7 +66,13 @@ blockchainTests.resets('Full migration', env => {
},
TransformERC20: {
contractType: ITransformERC20Contract,
fns: ['transformERC20', '_transformERC20', 'createTransformWallet', 'getTransformWallet'],
fns: [
'transformERC20',
'_transformERC20',
'createTransformWallet',
'getTransformWallet',
'setTransformerDeployer',
],
},
};
@ -162,4 +171,16 @@ blockchainTests.resets('Full migration', env => {
return expect(allowanceTarget.authorized(zeroEx.address).callAsync()).to.become(true);
});
});
describe('TransformERC20', () => {
let feature: ITransformERC20Contract;
before(async () => {
feature = new ITransformERC20Contract(zeroEx.address, env.provider, env.txDefaults);
});
it('has the correct transformer deployer', async () => {
return expect(feature.getTransformerDeployer().callAsync()).to.become(transformerDeployer);
});
});
});

View File

@ -12,7 +12,6 @@ import { Order } from '@0x/types';
import { BigNumber, hexUtils, ZeroExRevertErrors } from '@0x/utils';
import * as _ from 'lodash';
import { rlpEncodeNonce } from '../../src/nonce_utils';
import {
encodeFillQuoteTransformerData,
FillQuoteTransformerData,
@ -29,7 +28,6 @@ import {
const { NULL_ADDRESS, NULL_BYTES, MAX_UINT256, ZERO_AMOUNT } = constants;
blockchainTests.resets('FillQuoteTransformer', env => {
const deploymentNonce = _.random(0, 0xffffffff);
let maker: string;
let feeRecipient: string;
let exchange: TestFillQuoteTransformerExchangeContract;
@ -56,7 +54,6 @@ blockchainTests.resets('FillQuoteTransformer', env => {
env.txDefaults,
artifacts,
exchange.address,
new BigNumber(deploymentNonce),
);
host = await TestFillQuoteTransformerHostContract.deployFrom0xArtifactAsync(
artifacts.TestFillQuoteTransformerHost,
@ -585,24 +582,6 @@ blockchainTests.resets('FillQuoteTransformer', env => {
makerAssetBalance: qfr.makerAssetBought,
});
});
it('returns the RLP-encoded nonce', async () => {
const orders = _.times(1, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedSellQuoteFillResults(orders);
const r = await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
}),
)
.callAsync({ value: qfr.protocolFeePaid });
expect(r).to.eq(rlpEncodeNonce(deploymentNonce));
});
});
describe('buy quotes', () => {
@ -881,25 +860,5 @@ blockchainTests.resets('FillQuoteTransformer', env => {
makerAssetBalance: qfr.makerAssetBought,
});
});
it('returns the RLP-encoded nonce', async () => {
const orders = _.times(1, () => createOrder());
const signatures = orders.map(() => encodeExchangeBehavior());
const qfr = getExpectedBuyQuoteFillResults(orders);
const r = await host
.executeTransform(
transformer.address,
takerToken.address,
qfr.takerAssetSpent,
encodeTransformData({
orders,
signatures,
side: FillQuoteTransformerSide.Buy,
fillAmount: qfr.makerAssetBought,
}),
)
.callAsync({ value: qfr.protocolFeePaid });
expect(r).to.eq(rlpEncodeNonce(deploymentNonce));
});
});
});

View File

@ -3,7 +3,6 @@ import { BigNumber, hexUtils } from '@0x/utils';
import * as _ from 'lodash';
import { ETH_TOKEN_ADDRESS } from '../../src/constants';
import { rlpEncodeNonce } from '../../src/nonce_utils';
import { encodePayTakerTransformerData } from '../../src/transformer_data_encoders';
import { artifacts } from '../artifacts';
import { PayTakerTransformerContract, TestMintableERC20TokenContract, TestTransformerHostContract } from '../wrappers';
@ -12,7 +11,6 @@ const { MAX_UINT256, ZERO_AMOUNT } = constants;
blockchainTests.resets('PayTakerTransformer', env => {
const taker = randomAddress();
const deploymentNonce = _.random(0, 0xffffffff);
let caller: string;
let token: TestMintableERC20TokenContract;
let transformer: PayTakerTransformerContract;
@ -31,7 +29,6 @@ blockchainTests.resets('PayTakerTransformer', env => {
env.provider,
env.txDefaults,
artifacts,
new BigNumber(deploymentNonce),
);
host = await TestTransformerHostContract.deployFrom0xArtifactAsync(
artifacts.TestTransformerHost,
@ -147,16 +144,4 @@ blockchainTests.resets('PayTakerTransformer', env => {
ethBalance: amounts[1].dividedToIntegerBy(2),
});
});
it('returns the RLP-encoded nonce', async () => {
const amounts = _.times(2, () => getRandomInteger(1, '1e18'));
const data = encodePayTakerTransformerData({
amounts,
tokens: [token.address, ETH_TOKEN_ADDRESS],
});
await mintHostTokensAsync(amounts[0]);
await sendEtherAsync(host.address, amounts[1]);
const r = await host.rawExecuteTransform(transformer.address, hexUtils.random(), taker, data).callAsync();
expect(r).to.eq(rlpEncodeNonce(deploymentNonce));
});
});

View File

@ -1,13 +1,11 @@
import { blockchainTests, constants, expect, randomAddress } from '@0x/contracts-test-utils';
import { BigNumber, ZeroExRevertErrors } from '@0x/utils';
import { ZeroExRevertErrors } from '@0x/utils';
import * as _ from 'lodash';
import { rlpEncodeNonce } from '../../src/nonce_utils';
import { artifacts } from '../artifacts';
import { TestDelegateCallerContract, TestTransformerBaseContract } from '../wrappers';
blockchainTests.resets('Transformer (base)', env => {
const deploymentNonce = _.random(0, 0xffffffff);
let deployer: string;
let delegateCaller: TestDelegateCallerContract;
let transformer: TestTransformerBaseContract;
@ -28,17 +26,9 @@ blockchainTests.resets('Transformer (base)', env => {
from: deployer,
},
artifacts,
new BigNumber(deploymentNonce),
);
});
describe('_getRLPEncodedDeploymentNonce()', () => {
it('returns the RLP encoded deployment nonce', async () => {
const r = await transformer.getRLPEncodedDeploymentNonce().callAsync();
expect(r).to.eq(rlpEncodeNonce(deploymentNonce));
});
});
describe('die()', () => {
it('cannot be called by non-deployer', async () => {
const notDeployer = randomAddress();

View File

@ -3,7 +3,6 @@ import { BigNumber, ZeroExRevertErrors } from '@0x/utils';
import * as _ from 'lodash';
import { ETH_TOKEN_ADDRESS } from '../../src/constants';
import { rlpEncodeNonce } from '../../src/nonce_utils';
import { encodeWethTransformerData } from '../../src/transformer_data_encoders';
import { artifacts } from '../artifacts';
import { TestWethContract, TestWethTransformerHostContract, WethTransformerContract } from '../wrappers';
@ -11,7 +10,6 @@ import { TestWethContract, TestWethTransformerHostContract, WethTransformerContr
const { MAX_UINT256, ZERO_AMOUNT } = constants;
blockchainTests.resets('WethTransformer', env => {
const deploymentNonce = _.random(0, 0xffffffff);
let weth: TestWethContract;
let transformer: WethTransformerContract;
let host: TestWethTransformerHostContract;
@ -29,7 +27,6 @@ blockchainTests.resets('WethTransformer', env => {
env.txDefaults,
artifacts,
weth.address,
new BigNumber(deploymentNonce),
);
host = await TestWethTransformerHostContract.deployFrom0xArtifactAsync(
artifacts.TestWethTransformerHost,
@ -152,14 +149,4 @@ blockchainTests.resets('WethTransformer', env => {
wethBalance: amount.dividedToIntegerBy(2),
});
});
it('returns the RLP-encoded nonce', async () => {
const amount = getRandomInteger(1, '1e18');
const data = encodeWethTransformerData({
amount,
token: weth.address,
});
const r = await host.executeTransform(amount, transformer.address, data).callAsync({ value: amount });
expect(r).to.eq(rlpEncodeNonce(deploymentNonce));
});
});

View File

@ -67,14 +67,13 @@ export interface FullFeatures extends BootstrapFeatures {
}
export interface FullMigrationOpts {
transformDeployer: string;
transformerDeployer: string;
}
export async function deployFullFeaturesAsync(
provider: SupportedProvider,
txDefaults: Partial<TxData>,
features: Partial<FullFeatures> = {},
opts: Partial<FullMigrationOpts> = {},
): Promise<FullFeatures> {
return {
...(await deployBootstrapFeaturesAsync(provider, txDefaults)),
@ -93,7 +92,6 @@ export async function deployFullFeaturesAsync(
provider,
txDefaults,
artifacts,
opts.transformDeployer || (txDefaults.from as string),
)),
};
}
@ -105,7 +103,7 @@ export async function fullMigrateAsync(
features: Partial<FullFeatures> = {},
opts: Partial<FullMigrationOpts> = {},
): Promise<ZeroExContract> {
const _features = await deployFullFeaturesAsync(provider, txDefaults, features, opts);
const _features = await deployFullFeaturesAsync(provider, txDefaults, features);
const migrator = await FullMigrationContract.deployFrom0xArtifactAsync(
artifacts.FullMigration,
provider,
@ -113,7 +111,11 @@ export async function fullMigrateAsync(
artifacts,
txDefaults.from as string,
);
const deployCall = migrator.deploy(owner, toFeatureAdddresses(_features));
const _opts = {
transformerDeployer: txDefaults.from as string,
...opts,
};
const deployCall = migrator.deploy(owner, toFeatureAdddresses(_features), _opts);
const zeroEx = new ZeroExContract(await deployCall.callAsync(), provider, {});
await deployCall.awaitTransactionSuccessAsync();
return zeroEx;